From 41ceff84308a8bf00cd9f83279c1519a87b77a42 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 27 Jul 2015 16:13:11 +1000 Subject: [PATCH] UX: move search to its own route previously search was bundled with discovery, something that makes stuff confusing internally --- .../controllers/full-page-search.js.es6 | 33 +++++ .../discourse/lib/search-for-term.js.es6 | 120 +++++++++--------- .../discourse/routes/app-route-map.js.es6 | 2 + .../discourse/routes/full-page-search.js.es6 | 18 +++ .../discourse/templates/full-page-search.hbs | 38 ++++++ .../discourse/views/full-page-search.js.es6 | 16 +++ .../stylesheets/common/base/search.scss | 43 +++++++ app/controllers/search_controller.rb | 20 +++ app/views/search/show.html.erb | 0 config/locales/client.en.yml | 2 + config/routes.rb | 1 + lib/search.rb | 3 +- lib/search/grouped_search_results.rb | 11 +- 13 files changed, 244 insertions(+), 63 deletions(-) create mode 100644 app/assets/javascripts/discourse/controllers/full-page-search.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/full-page-search.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/full-page-search.hbs create mode 100644 app/assets/javascripts/discourse/views/full-page-search.js.es6 create mode 100644 app/assets/stylesheets/common/base/search.scss create mode 100644 app/views/search/show.html.erb diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 new file mode 100644 index 000000000..7deae7bd2 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -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')); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 b/app/assets/javascripts/discourse/lib/search-for-term.js.es6 index a63eacad7..e5348a6a1 100644 --- a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 +++ b/app/assets/javascripts/discourse/lib/search-for-term.js.es6 @@ -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); }); } diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 98e1194df..530f4845e 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -99,4 +99,6 @@ export default function() { }); this.resource('queued-posts', { path: '/queued-posts' }); + + this.route('full-page-search', {path: '/search2'}); } diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 new file mode 100644 index 000000000..935437404 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 @@ -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; + }); + } + +}); diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs new file mode 100644 index 000000000..b12f43734 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -0,0 +1,38 @@ + + +{{#conditional-loading-spinner condition=loading}} + +{{#unless model.posts}} +

{{i18n "search.no_results"}} {{i18n "search.search_help"}} +

+{{/unless}} + +{{#each model.posts as |result|}} +
+
+ {{avatar result imageSize="tiny"}} + + {{topic-status topic=result.topic disableActions=true}}{{unbound result.topic.title}} + {{category-link result.topic.category}} +
+
+ {{format-age result.created_at}}{{#if result.blurb}} + – + {{{unbound result.blurb}}} + {{/if}} +
+
+{{/each}} + +{{#if model.posts}} + +{{/if}} + +{{/conditional-loading-spinner}} + diff --git a/app/assets/javascripts/discourse/views/full-page-search.js.es6 b/app/assets/javascripts/discourse/views/full-page-search.js.es6 new file mode 100644 index 000000000..231203470 --- /dev/null +++ b/app/assets/javascripts/discourse/views/full-page-search.js.es6 @@ -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') +}); diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss new file mode 100644 index 000000000..b4f830319 --- /dev/null +++ b/app/assets/stylesheets/common/base/search.scss @@ -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; +} diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f78087a4f..7430d9f41 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -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) diff --git a/app/views/search/show.html.erb b/app/views/search/show.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 76613cb5d..e1b971722 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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}}" diff --git a/config/routes.rb b/config/routes.rb index e3964d642..7a26597a0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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" diff --git a/lib/search.rb b/lib/search.rb index 5eb8a722b..0a07dcf9f 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -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) diff --git a/lib/search/grouped_search_results.rb b/lib/search/grouped_search_results.rb index c3f661313..fe594cf1e 100644 --- a/lib/search/grouped_search_results.rb +++ b/lib/search/grouped_search_results.rb @@ -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