FEATURE: on mobile take users to full page search
UX: improve styling on full page search page FEATURE: allow search context in full page search FEATURE: visited color link for full page search FIX: broken search help on fulls page search page FEATURE: allow preload store to return a null FEATURE: "mobileAction" for the header buttons
This commit is contained in:
parent
e37dd5a393
commit
e13ed24122
15 changed files with 203 additions and 43 deletions
app
assets
javascripts
stylesheets
controllers
|
@ -13,6 +13,12 @@ export default Ember.Component.extend({
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
toggle() {
|
toggle() {
|
||||||
|
|
||||||
|
if (Discourse.Mobile.mobileView && this.get('mobileAction')) {
|
||||||
|
this.sendAction('mobileAction');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.siteSettings.login_required && !this.currentUser) {
|
if (this.siteSettings.login_required && !this.currentUser) {
|
||||||
this.sendAction('loginAction');
|
this.sendAction('loginAction');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import searchForTerm from 'discourse/lib/search-for-term';
|
import {searchForTerm, searchContextDescription} from 'discourse/lib/search';
|
||||||
import DiscourseURL from 'discourse/lib/url';
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
import showModal from 'discourse/lib/show-modal';
|
import showModal from 'discourse/lib/show-modal';
|
||||||
|
@ -48,18 +48,7 @@ export default Ember.Component.extend({
|
||||||
|
|
||||||
@computed('searchService.searchContext')
|
@computed('searchService.searchContext')
|
||||||
searchContextDescription(ctx) {
|
searchContextDescription(ctx) {
|
||||||
if (ctx) {
|
return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name'));
|
||||||
switch(Em.get(ctx, 'type')) {
|
|
||||||
case 'topic':
|
|
||||||
return I18n.t('search.context.topic');
|
|
||||||
case 'user':
|
|
||||||
return I18n.t('search.context.user', {username: Em.get(ctx, 'user.username')});
|
|
||||||
case 'category':
|
|
||||||
return I18n.t('search.context.category', {category: Em.get(ctx, 'category.name')});
|
|
||||||
case 'private_messages':
|
|
||||||
return I18n.t('search.context.private_messages');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@observes('searchService.searchContextEnabled')
|
@observes('searchService.searchContextEnabled')
|
||||||
|
|
|
@ -1,30 +1,65 @@
|
||||||
import { translateResults } from "discourse/lib/search-for-term";
|
import { translateResults, searchContextDescription } from "discourse/lib/search";
|
||||||
|
import showModal from 'discourse/lib/show-modal';
|
||||||
|
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||||
|
import Category from 'discourse/models/category';
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
needs: ["application"],
|
needs: ["application"],
|
||||||
|
|
||||||
loading: Em.computed.not("model"),
|
loading: Em.computed.not("model"),
|
||||||
queryParams: ["q"],
|
queryParams: ["q", "context_id", "context", "skip_context"],
|
||||||
q: null,
|
q: null,
|
||||||
selected: [],
|
selected: [],
|
||||||
|
context_id: null,
|
||||||
|
context: null,
|
||||||
|
|
||||||
modelChanged: function() {
|
@computed('skip_context', 'context')
|
||||||
|
searchContextEnabled: {
|
||||||
|
get(skip,context){
|
||||||
|
return (!skip && context) || skip === "false";
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.set('skip_context', val ? "false" : "true" )
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('context', 'context_id')
|
||||||
|
searchContextDescription(context, id){
|
||||||
|
var name = id;
|
||||||
|
if (context === 'category') {
|
||||||
|
var category = Category.findById(id);
|
||||||
|
if (!category) {return;}
|
||||||
|
|
||||||
|
name = category.get('name');
|
||||||
|
}
|
||||||
|
return searchContextDescription(context, name);
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('q')
|
||||||
|
searchActive(q){
|
||||||
|
return q && q.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
@observes('model')
|
||||||
|
modelChanged() {
|
||||||
if (this.get("searchTerm") !== this.get("q")) {
|
if (this.get("searchTerm") !== this.get("q")) {
|
||||||
this.set("searchTerm", this.get("q"));
|
this.set("searchTerm", this.get("q"));
|
||||||
}
|
}
|
||||||
}.observes("model"),
|
},
|
||||||
|
|
||||||
qChanged: function() {
|
@observes('q')
|
||||||
|
qChanged() {
|
||||||
const model = this.get("model");
|
const model = this.get("model");
|
||||||
if (model && this.get("model.q") !== this.get("q")) {
|
if (model && this.get("model.q") !== this.get("q")) {
|
||||||
this.set("searchTerm", this.get("q"));
|
this.set("searchTerm", this.get("q"));
|
||||||
this.send("search");
|
this.send("search");
|
||||||
}
|
}
|
||||||
}.observes("q"),
|
},
|
||||||
|
|
||||||
_showFooter: function() {
|
@observes('loading')
|
||||||
|
_showFooter() {
|
||||||
this.set("controllers.application.showFooter", !this.get("loading"));
|
this.set("controllers.application.showFooter", !this.get("loading"));
|
||||||
}.observes("loading"),
|
},
|
||||||
|
|
||||||
canBulkSelect: Em.computed.alias('currentUser.staff'),
|
canBulkSelect: Em.computed.alias('currentUser.staff'),
|
||||||
|
|
||||||
|
@ -32,9 +67,19 @@ export default Ember.Controller.extend({
|
||||||
this.set("q", this.get("searchTerm"));
|
this.set("q", this.get("searchTerm"));
|
||||||
this.set("model", null);
|
this.set("model", null);
|
||||||
|
|
||||||
Discourse.ajax("/search", { data: { q: this.get("searchTerm") } }).then(results => {
|
var args = { q: this.get("searchTerm") };
|
||||||
|
|
||||||
|
const skip = this.get("skip_context");
|
||||||
|
if ((!skip && this.get('context')) || skip==="false"){
|
||||||
|
args.search_context = {
|
||||||
|
type: this.get('context'),
|
||||||
|
id: this.get('context_id')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Discourse.ajax("/search", { data: args }).then(results => {
|
||||||
this.set("model", translateResults(results) || {});
|
this.set("model", translateResults(results) || {});
|
||||||
this.set("model.q", this.get("q"));
|
// this.set("model.q", this.get("q"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -51,6 +96,13 @@ export default Ember.Controller.extend({
|
||||||
this.search();
|
this.search();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showSearchHelp() {
|
||||||
|
// TODO: dupe code should be centralized
|
||||||
|
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => {
|
||||||
|
showModal('searchHelp', { model });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import DiscourseURL from 'discourse/lib/url';
|
||||||
|
|
||||||
const HeaderController = Ember.Controller.extend({
|
const HeaderController = Ember.Controller.extend({
|
||||||
topic: null,
|
topic: null,
|
||||||
showExtraInfo: null,
|
showExtraInfo: null,
|
||||||
|
@ -18,6 +20,17 @@ const HeaderController = Ember.Controller.extend({
|
||||||
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
fullPageSearch() {
|
||||||
|
const searchService = this.container.lookup('search-service:main');
|
||||||
|
const context = searchService.get('searchContext');
|
||||||
|
var params = "";
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
params = `?context=${context.type}&context_id=${context.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscourseURL.routeTo('/search' + params);
|
||||||
|
},
|
||||||
toggleMenuPanel(visibleProp) {
|
toggleMenuPanel(visibleProp) {
|
||||||
this.toggleProperty(visibleProp);
|
this.toggleProperty(visibleProp);
|
||||||
this.appEvents.trigger('dropdowns:closeAll');
|
this.appEvents.trigger('dropdowns:closeAll');
|
||||||
|
|
|
@ -86,4 +86,19 @@ function searchForTerm(term, opts) {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default searchForTerm;
|
const searchContextDescription = function(type, name){
|
||||||
|
if (type) {
|
||||||
|
switch(type) {
|
||||||
|
case 'topic':
|
||||||
|
return I18n.t('search.context.topic');
|
||||||
|
case 'user':
|
||||||
|
return I18n.t('search.context.user', {username: name});
|
||||||
|
case 'category':
|
||||||
|
return I18n.t('search.context.category', {category: name});
|
||||||
|
case 'private_messages':
|
||||||
|
return I18n.t('search.context.private_messages');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { searchForTerm, searchContextDescription };
|
|
@ -1,15 +1,24 @@
|
||||||
import { translateResults } from "discourse/lib/search-for-term";
|
import { translateResults } from "discourse/lib/search";
|
||||||
|
|
||||||
export default Discourse.Route.extend({
|
export default Discourse.Route.extend({
|
||||||
queryParams: { q: {} },
|
queryParams: { q: {}, "context-id": {}, context: {} },
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
return PreloadStore.getAndRemove("search", function() {
|
return PreloadStore.getAndRemove("search", function() {
|
||||||
return Discourse.ajax("/search", { data: { q: params.q } });
|
if (params.q && params.q.length > 2) {
|
||||||
|
var args = { q: params.q };
|
||||||
|
if (params.context_id && !args.skip_context) {
|
||||||
|
args.search_context = {
|
||||||
|
type: params.context,
|
||||||
|
id: params.context_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Discourse.ajax("/search", { data: args });
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}).then(results => {
|
}).then(results => {
|
||||||
const model = translateResults(results) || {};
|
return (results && translateResults(results)) || {};
|
||||||
model.q = params.q;
|
|
||||||
return model;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,22 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if context}}
|
||||||
|
<div class='fps-search-context'>
|
||||||
|
<label>
|
||||||
|
{{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#conditional-loading-spinner condition=loading}}
|
{{#conditional-loading-spinner condition=loading}}
|
||||||
|
|
||||||
{{#unless model.posts}}
|
{{#unless model.posts}}
|
||||||
<h3>
|
<h3>
|
||||||
{{i18n "search.no_results"}} <a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
|
{{#if searchActive}}
|
||||||
|
{{i18n "search.no_results"}}
|
||||||
|
{{/if}}
|
||||||
|
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
|
||||||
</h3>
|
</h3>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
{{#header-dropdown iconId="search-button"
|
{{#header-dropdown iconId="search-button"
|
||||||
icon="search"
|
icon="search"
|
||||||
toggleVisible=searchVisible
|
toggleVisible=searchVisible
|
||||||
|
mobileAction="fullPageSearch"
|
||||||
loginAction="showLogin"
|
loginAction="showLogin"
|
||||||
title="search.title"}}
|
title="search.title"}}
|
||||||
{{/header-dropdown}}
|
{{/header-dropdown}}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import debounce from 'discourse/lib/debounce';
|
import debounce from 'discourse/lib/debounce';
|
||||||
import searchForTerm from 'discourse/lib/search-for-term';
|
import { searchForTerm } from 'discourse/lib/search';
|
||||||
|
|
||||||
export default Ember.View.extend({
|
export default Ember.View.extend({
|
||||||
templateName: 'choose_topic',
|
templateName: 'choose_topic',
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
//= require_tree ./discourse/mixins
|
//= require_tree ./discourse/mixins
|
||||||
//= require ./discourse/lib/ajax-error
|
//= require ./discourse/lib/ajax-error
|
||||||
//= require ./discourse/lib/markdown
|
//= require ./discourse/lib/markdown
|
||||||
//= require ./discourse/lib/search-for-term
|
//= require ./discourse/lib/search
|
||||||
//= require ./discourse/lib/user-search
|
//= require ./discourse/lib/user-search
|
||||||
//= require ./discourse/lib/export-csv
|
//= require ./discourse/lib/export-csv
|
||||||
//= require ./discourse/lib/autocomplete
|
//= require ./discourse/lib/autocomplete
|
||||||
|
|
|
@ -42,7 +42,7 @@ window.PreloadStore = {
|
||||||
var result = finder();
|
var result = finder();
|
||||||
|
|
||||||
// If the finder returns a promise, we support that too
|
// If the finder returns a promise, we support that too
|
||||||
if (result.then) {
|
if (result && result.then) {
|
||||||
result.then(function(result) {
|
result.then(function(result) {
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
|
|
|
@ -18,10 +18,20 @@
|
||||||
top: -3px;
|
top: -3px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
a.search-link:visited .topic-title {
|
||||||
|
color: scale-color($tertiary, $lightness: 15%);
|
||||||
|
}
|
||||||
.search-link {
|
.search-link {
|
||||||
.topic-statuses, .topic-title {
|
.topic-statuses, .topic-title {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topic-statuses {
|
||||||
|
float: none;
|
||||||
|
display: inline-block;
|
||||||
|
color: $primary;
|
||||||
|
font-size: 1.0em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.blurb {
|
.blurb {
|
||||||
font-size: 1.0em;
|
font-size: 1.0em;
|
||||||
|
@ -50,3 +60,7 @@
|
||||||
.search-footer {
|
.search-footer {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-body-contents .search-context label {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
@import "mobile/history";
|
@import "mobile/history";
|
||||||
@import "mobile/directory";
|
@import "mobile/directory";
|
||||||
@import "mobile/menu-panel";
|
@import "mobile/menu-panel";
|
||||||
|
@import "mobile/search";
|
||||||
|
|
||||||
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
||||||
|
|
||||||
|
|
16
app/assets/stylesheets/mobile/search.scss
Normal file
16
app/assets/stylesheets/mobile/search.scss
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.search button.btn-primary, .search button.btn {
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search.row {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search.row input.search {
|
||||||
|
height: 25px;
|
||||||
|
width: 69%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fps-search-context {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
|
@ -9,7 +9,20 @@ class SearchController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
search = Search.new(params[:q], type_filter: 'topic', guardian: guardian, include_blurbs: true, blurb_length: 300)
|
search_args = {
|
||||||
|
type_filter: 'topic',
|
||||||
|
guardian: guardian,
|
||||||
|
include_blurbs: true,
|
||||||
|
blurb_length: 300
|
||||||
|
}
|
||||||
|
|
||||||
|
context, type = lookup_search_context
|
||||||
|
if context
|
||||||
|
search_args[:search_context] = context
|
||||||
|
search_args[:type_filter] = type if type
|
||||||
|
end
|
||||||
|
|
||||||
|
search = Search.new(params[:q], search_args)
|
||||||
result = search.execute
|
result = search.execute
|
||||||
|
|
||||||
serializer = serialize_data(result, GroupedSearchResultSerializer, result: result)
|
serializer = serialize_data(result, GroupedSearchResultSerializer, result: result)
|
||||||
|
@ -34,7 +47,29 @@ class SearchController < ApplicationController
|
||||||
search_args[:include_blurbs] = params[:include_blurbs] == "true" if params[:include_blurbs].present?
|
search_args[:include_blurbs] = params[:include_blurbs] == "true" if params[:include_blurbs].present?
|
||||||
search_args[:search_for_id] = true if params[:search_for_id].present?
|
search_args[:search_for_id] = true if params[:search_for_id].present?
|
||||||
|
|
||||||
|
context,type = lookup_search_context
|
||||||
|
if context
|
||||||
|
search_args[:search_context] = context
|
||||||
|
search_args[:type_filter] = type if type
|
||||||
|
end
|
||||||
|
|
||||||
|
search = Search.new(params[:term], search_args.symbolize_keys)
|
||||||
|
result = search.execute
|
||||||
|
render_serialized(result, GroupedSearchResultSerializer, result: result)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def lookup_search_context
|
||||||
|
|
||||||
|
return if params[:skip_context] == "true"
|
||||||
|
|
||||||
search_context = params[:search_context]
|
search_context = params[:search_context]
|
||||||
|
unless search_context
|
||||||
|
if (context = params[:context]) && (id = params[:context_id])
|
||||||
|
search_context = {type: context, id: id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if search_context.present?
|
if search_context.present?
|
||||||
raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type])
|
raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type])
|
||||||
|
@ -43,23 +78,21 @@ class SearchController < ApplicationController
|
||||||
# A user is found by username
|
# A user is found by username
|
||||||
context_obj = nil
|
context_obj = nil
|
||||||
if ['user','private_messages'].include? search_context[:type]
|
if ['user','private_messages'].include? search_context[:type]
|
||||||
context_obj = User.find_by(username_lower: params[:search_context][:id].downcase)
|
context_obj = User.find_by(username_lower: search_context[:id].downcase)
|
||||||
else
|
else
|
||||||
klass = search_context[:type].classify.constantize
|
klass = search_context[:type].classify.constantize
|
||||||
context_obj = klass.find_by(id: params[:search_context][:id])
|
context_obj = klass.find_by(id: search_context[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
type_filter = nil
|
||||||
if search_context[:type] == 'private_messages'
|
if search_context[:type] == 'private_messages'
|
||||||
search_args[:type_filter] = 'private_messages'
|
type_filter = 'private_messages'
|
||||||
end
|
end
|
||||||
|
|
||||||
guardian.ensure_can_see!(context_obj)
|
guardian.ensure_can_see!(context_obj)
|
||||||
search_args[:search_context] = context_obj
|
|
||||||
end
|
|
||||||
|
|
||||||
search = Search.new(params[:term], search_args.symbolize_keys)
|
[context_obj, type_filter]
|
||||||
result = search.execute
|
end
|
||||||
render_serialized(result, GroupedSearchResultSerializer, result: result)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Reference in a new issue