Merge pull request #4467 from cpradio/advanced-search-ui

FEATURE: Advanced Search UI
This commit is contained in:
Sam 2016-10-11 10:02:35 +11:00 committed by GitHub
commit f6ac914376
16 changed files with 900 additions and 19 deletions

View File

@ -0,0 +1,48 @@
import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
@computed('placeholderKey')
placeholder(placeholderKey) {
return placeholderKey ? I18n.t(placeholderKey) : '';
},
@observes('badgeNames')
_update() {
if (this.get('canReceiveUpdates') === 'true')
this._initializeAutocomplete({updateData: true});
},
@on('didInsertElement')
_initializeAutocomplete(opts) {
var self = this;
var selectedBadges;
var template = this.container.lookup('template:badge-selector-autocomplete.raw');
self.$('input').autocomplete({
allowAny: false,
items: _.isArray(this.get('badgeNames')) ? this.get('badgeNames') : [this.get('badgeNames')],
single: this.get('single'),
updateData: (opts && opts.updateData) ? opts.updateData : false,
onChangeItems: function(items){
selectedBadges = items;
self.set("badgeNames", items.join(","));
},
transformComplete: function(g) {
return g.name;
},
dataSource: function(term) {
return self.get("badgeFinder")(term).then(function(badges){
if(!selectedBadges){
return badges;
}
return badges.filter(function(badge){
return !selectedBadges.any(function(s){return s === badge.name;});
});
});
},
template: template
});
}
});

View File

@ -1,18 +1,25 @@
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Category from 'discourse/models/category';
import { on } from 'ember-addons/ember-computed-decorators';
import { on, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
@observes('categories')
_update() {
if (this.get('canReceiveUpdates') === 'true')
this._initializeAutocomplete({updateData: true});
},
@on('didInsertElement')
_initializeAutocomplete() {
_initializeAutocomplete(opts) {
const self = this,
template = this.container.lookup('template:category-selector-autocomplete.raw'),
regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
this.$('input').autocomplete({
items: this.get('categories'),
single: false,
single: this.get('single'),
allowAny: false,
updateData: (opts && opts.updateData) ? opts.updateData : false,
dataSource(term) {
return Category.list().filter(category => {
const regex = new RegExp(term, 'i');
@ -26,7 +33,12 @@ export default Ember.Component.extend({
const slug = link.match(regexp)[1];
return Category.findSingleBySlug(slug);
});
Em.run.next(() => self.set('categories', categories));
Em.run.next(() => {
let existingCategory = _.isArray(self.get('categories')) ? self.get('categories') : [self.get('categories')];
const result = _.intersection(existingCategory.map(itm => itm.id), categories.map(itm => itm.id));
if (result.length !== categories.length || existingCategory.length !== categories.length)
self.set('categories', categories);
});
},
template,
transformComplete(category) {

View File

@ -1,4 +1,4 @@
import { on, default as computed } from 'ember-addons/ember-computed-decorators';
import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
@computed('placeholderKey')
@ -6,15 +6,24 @@ export default Ember.Component.extend({
return placeholderKey ? I18n.t(placeholderKey) : '';
},
@observes('groupNames')
_update() {
if (this.get('canReceiveUpdates') === 'true')
this._initializeAutocomplete({updateData: true});
},
@on('didInsertElement')
_initializeAutocomplete() {
_initializeAutocomplete(opts) {
var self = this;
var selectedGroups;
var groupNames = this.get('groupNames');
var template = this.container.lookup('template:group-selector-autocomplete.raw');
self.$('input').autocomplete({
allowAny: false,
items: this.get('groupNames'),
items: _.isArray(groupNames) ? groupNames : (Ember.isEmpty(groupNames)) ? [] : [groupNames],
single: this.get('single'),
updateData: (opts && opts.updateData) ? opts.updateData : false,
onChangeItems: function(items){
selectedGroups = items;
self.set("groupNames", items.join(","));

View File

@ -0,0 +1,403 @@
import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators';
const REGEXP_FILTER_PREFIXES = /\s?(user:|@|category:|#|group:|badge:|tags?:|in:|status:|posts_count:|(before|after):)/ig;
const REGEXP_USERNAME_PREFIX = /\s?(user:|@)/ig;
const REGEXP_CATEGORY_PREFIX = /\s?(category:|#)/ig;
const REGEXP_GROUP_PREFIX = /\s?group:/ig;
const REGEXP_BADGE_PREFIX = /\s?badge:/ig;
const REGEXP_TAGS_PREFIX = /\s?tags?:/ig;
const REGEXP_IN_PREFIX = /\s?in:/ig;
const REGEXP_STATUS_PREFIX = /\s?status:/ig;
const REGEXP_POST_COUNT_PREFIX = /\s?posts_count:/ig;
const REGEXP_POST_TIME_PREFIX = /\s?(before|after):/ig;
const REGEXP_CATEGORY_SLUG = /\s?(\#[a-zA-Z0-9\-:]+)/ig;
const REGEXP_CATEGORY_ID = /\s?(category:[0-9]+)/ig;
const REGEXP_POST_TIME_WHEN = /(before|after)/ig;
export default Em.Component.extend({
tagName: 'div',
classNames: ['search-advanced', 'row'],
searchedTerms: {username: [], category: null, group: [], badge: [], tags: [],
in: '', status: '', posts_count: '', time: {when: 'before', days: ''}},
inOptions: [
{name: I18n.t('search.advanced.filters.likes'), value: "likes"},
{name: I18n.t('search.advanced.filters.posted'), value: "posted"},
{name: I18n.t('search.advanced.filters.watching'), value: "watching"},
{name: I18n.t('search.advanced.filters.tracking'), value: "tracking"},
{name: I18n.t('search.advanced.filters.private'), value: "private"},
{name: I18n.t('search.advanced.filters.bookmarks'), value: "bookmarks"},
{name: I18n.t('search.advanced.filters.first'), value: "first"},
{name: I18n.t('search.advanced.filters.pinned'), value: "pinned"},
{name: I18n.t('search.advanced.filters.unpinned'), value: "unpinned"},
{name: I18n.t('search.advanced.filters.wiki'), value: "wiki"}
],
statusOptions: [
{name: I18n.t('search.advanced.statuses.open'), value: "open"},
{name: I18n.t('search.advanced.statuses.closed'), value: "closed"},
{name: I18n.t('search.advanced.statuses.archived'), value: "archived"},
{name: I18n.t('search.advanced.statuses.noreplies'), value: "noreplies"},
{name: I18n.t('search.advanced.statuses.single_user'), value: "single_user"},
],
postTimeOptions: [
{name: I18n.t('search.advanced.post.time.before'), value: "before"},
{name: I18n.t('search.advanced.post.time.after'), value: "after"}
],
@on('init')
@observes('searchTerm')
_init() {
let searchTerm = this.get('searchTerm');
if (!searchTerm)
return;
this.findUsername(searchTerm);
this.findCategory(searchTerm);
this.findGroup(searchTerm);
this.findBadge(searchTerm);
this.findTags(searchTerm);
this.findIn(searchTerm);
this.findStatus(searchTerm);
this.findPostsCount(searchTerm);
this.findPostTime(searchTerm);
},
findSearchTerm(EXPRESSION, searchTerm) {
if (!searchTerm)
return "";
const expression_location = searchTerm.search(EXPRESSION);
if (expression_location === -1)
return "";
const remaining_phrase = searchTerm.substring(expression_location + 2);
let next_expression_location = remaining_phrase.search(REGEXP_FILTER_PREFIXES);
if (next_expression_location === -1)
next_expression_location = remaining_phrase.length;
return searchTerm.substring(expression_location, next_expression_location + expression_location + 2);
},
findUsername(searchTerm) {
const match = this.findSearchTerm(REGEXP_USERNAME_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = _.isArray(this.get('searchedTerms.username')) ? this.get('searchedTerms.username')[0] : this.get('searchedTerms.username');
let userInput = match.replace(REGEXP_USERNAME_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.username', [userInput]);
} else
this.set('searchedTerms.username', []);
},
@observes('searchedTerms.username')
updateUsername() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_USERNAME_PREFIX, searchTerm);
const userFilter = this.get('searchedTerms.username');
if (userFilter && userFilter.length !== 0)
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` user:${userFilter}`);
else
searchTerm += ` user:${userFilter}`;
else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findCategory(searchTerm) {
const match = this.findSearchTerm(REGEXP_CATEGORY_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = _.isArray(this.get('searchedTerms.category')) ? this.get('searchedTerms.category')[0] : this.get('searchedTerms.category');
const subcategories = match.replace(REGEXP_CATEGORY_PREFIX, '').split(':');
if (subcategories.length > 1) {
let userInput = Discourse.Category.findBySlug(subcategories[1], subcategories[0]);
if ((!existingInput && userInput)
|| (existingInput && userInput && existingInput.id !== userInput.id))
this.set('searchedTerms.category', userInput.id);
} else
if (isNaN(subcategories)) {
let userInput = Discourse.Category.findSingleBySlug(subcategories[0]);
if ((!existingInput && userInput)
|| (existingInput && userInput && existingInput.id !== userInput.id))
this.set('searchedTerms.category', userInput.id);
} else {
let userInput = Discourse.Category.findById(subcategories[0]);
if ((!existingInput && userInput)
|| (existingInput && userInput && existingInput.id !== userInput.id))
this.set('searchedTerms.category', userInput.id);
}
} else
this.set('searchedTerms.category', null);
},
@observes('searchedTerms.category')
updateCategory() {
let searchTerm = this.get('searchTerm');
const categoryFilter = Discourse.Category.findById(this.get('searchedTerms.category'));
const match = this.findSearchTerm(REGEXP_CATEGORY_PREFIX, searchTerm);
const slugCategoryMatches = match.match(REGEXP_CATEGORY_SLUG);
const idCategoryMatches = match.match(REGEXP_CATEGORY_ID);
if (categoryFilter && categoryFilter.length !== 0) {
const id = categoryFilter.id;
const slug = categoryFilter.slug;
if (categoryFilter && categoryFilter.parentCategory) {
const parentSlug = categoryFilter.parentCategory.slug;
if (slugCategoryMatches)
searchTerm = searchTerm.replace(slugCategoryMatches[0], ` #${parentSlug}:${slug}`);
else if (idCategoryMatches)
searchTerm = searchTerm.replace(idCategoryMatches[0], ` category:${id}`);
else
searchTerm += ` #${parentSlug}:${slug}`;
} else if (categoryFilter) {
if (slugCategoryMatches)
searchTerm = searchTerm.replace(slugCategoryMatches[0], ` #${slug}`);
else if (idCategoryMatches)
searchTerm = searchTerm.replace(idCategoryMatches[0], ` category:${id}`);
else
searchTerm += ` #${slug}`;
}
} else {
if (slugCategoryMatches)
searchTerm = searchTerm.replace(slugCategoryMatches[0], '');
if (idCategoryMatches)
searchTerm = searchTerm.replace(idCategoryMatches[0], '');
}
this.set('searchTerm', searchTerm);
},
findGroup(searchTerm) {
const match = this.findSearchTerm(REGEXP_GROUP_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = _.isArray(this.get('searchedTerms.group')) ? this.get('searchedTerms.group')[0] : this.get('searchedTerms.group');
let userInput = match.replace(REGEXP_GROUP_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.group', [userInput]);
} else
this.set('searchedTerms.group', []);
},
@observes('searchedTerms.group')
updateGroup() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_GROUP_PREFIX, searchTerm);
const groupFilter = this.get('searchedTerms.group');
if (groupFilter && groupFilter.length !== 0)
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` group:${groupFilter}`);
else
searchTerm += ` group:${groupFilter}`;
else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findBadge(searchTerm) {
const match = this.findSearchTerm(REGEXP_BADGE_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = _.isArray(this.get('searchedTerms.badge')) ? this.get('searchedTerms.badge')[0] : this.get('searchedTerms.badge');
let userInput = match.replace(REGEXP_BADGE_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.badge', [match.replace(REGEXP_BADGE_PREFIX, '')]);
} else
this.set('searchedTerms.badge', []);
},
@observes('searchedTerms.badge')
updateBadge() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_BADGE_PREFIX, searchTerm);
const badgeFilter = this.get('searchedTerms.badge');
if (badgeFilter && badgeFilter.length !== 0)
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` badge:${badgeFilter}`);
else
searchTerm += ` badge:${badgeFilter}`;
else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findTags(searchTerm) {
const match = this.findSearchTerm(REGEXP_TAGS_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = _.isArray(this.get('searchedTerms.tags')) ? this.get('searchedTerms.tags').join(',') : this.get('searchedTerms.tags');
let userInput = match.replace(REGEXP_TAGS_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.tags', userInput.split(','));
} else
this.set('searchedTerms.tags', []);
},
@observes('searchedTerms.tags')
updateTags() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_TAGS_PREFIX, searchTerm);
const tagFilter = this.get('searchedTerms.tags');
if (tagFilter && tagFilter.length !== 0) {
const tags = tagFilter.join(',');
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` tags:${tags}`);
else
searchTerm += ` tags:${tags}`;
} else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findIn(searchTerm) {
const match = this.findSearchTerm(REGEXP_IN_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = this.get('searchedTerms.in');
let userInput = match.replace(REGEXP_IN_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.in', userInput);
} else
this.set('searchedTerms.in', '');
},
@observes('searchedTerms.in')
updateIn() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_IN_PREFIX, searchTerm);
const inFilter = this.get('searchedTerms.in');
if (inFilter)
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` in:${inFilter}`);
else
searchTerm += ` in:${inFilter}`;
else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findStatus(searchTerm) {
const match = this.findSearchTerm(REGEXP_STATUS_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = this.get('searchedTerms.status');
let userInput = match.replace(REGEXP_STATUS_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.status', userInput);
} else
this.set('searchedTerms.status', '');
},
@observes('searchedTerms.status')
updateStatus() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_STATUS_PREFIX, searchTerm);
const statusFilter = this.get('searchedTerms.status');
if (statusFilter)
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` status:${statusFilter}`);
else
searchTerm += ` status:${statusFilter}`;
else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findPostsCount(searchTerm) {
const match = this.findSearchTerm(REGEXP_POST_COUNT_PREFIX, searchTerm);
if (match.length !== 0) {
let existingInput = this.get('searchedTerms.posts_count');
let userInput = match.replace(REGEXP_POST_COUNT_PREFIX, '');
if (userInput.length !== 0 && existingInput !== userInput)
this.set('searchedTerms.posts_count', userInput);
} else
this.set('searchedTerms.posts_count', '');
},
@observes('searchedTerms.posts_count')
updatePostsCount() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_POST_COUNT_PREFIX, searchTerm);
const postsCountFilter = this.get('searchedTerms.posts_count');
if (postsCountFilter)
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` posts_count:${postsCountFilter}`);
else
searchTerm += ` posts_count:${postsCountFilter}`;
else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
findPostTime(searchTerm) {
const match = this.findSearchTerm(REGEXP_POST_TIME_WHEN, searchTerm);
if (match.length !== 0) {
let existingInputWhen = this.get('searchedTerms.time.when');
let userInputWhen = match.match(REGEXP_POST_TIME_WHEN)[0];
if (userInputWhen.length !== 0 && existingInputWhen !== userInputWhen)
this.set('searchedTerms.time.when', userInputWhen);
let existingInputDays = this.get('searchedTerms.time.days');
let userInputDays = match.replace(REGEXP_POST_TIME_PREFIX, '');
if (userInputDays.length !== 0 && existingInputDays !== userInputDays)
this.set('searchedTerms.time.days', userInputDays);
} else
this.set('searchedTerms.time.days', '');
},
@observes('searchedTerms.time.when', 'searchedTerms.time.days')
updatePostTime() {
let searchTerm = this.get('searchTerm');
const match = this.findSearchTerm(REGEXP_POST_TIME_PREFIX, searchTerm);
const timeDaysFilter = this.get('searchedTerms.time.days');
if (timeDaysFilter) {
const when = this.get('searchedTerms.time.when');
if (match.length !== 0)
searchTerm = searchTerm.replace(match, ` ${when}:${timeDaysFilter}`);
else
searchTerm += ` ${when}:${timeDaysFilter}`;
} else if (match.length !== 0)
searchTerm = searchTerm.replace(match, '');
this.set('searchTerm', searchTerm);
},
groupFinder(term) {
const Group = require('discourse/models/group').default;
return Group.findAll({search: term, ignore_automatic: false});
},
badgeFinder(term) {
const Badge = require('discourse/models/badge').default;
return Badge.findAll({search: term});
},
collapsedClassName: function() {
return (this.get('isExpanded')) ? "fa-caret-down" : "fa-caret-right";
}.property('isExpanded'),
@computed('isExpanded')
isCollapsed(isExpanded){
return !isExpanded;
},
actions: {
expandOptions() {
this.set('isExpanded', !this.get('isExpanded'));
if (this.get('isExpanded'))
this._init();
}
}
});

View File

@ -3,8 +3,13 @@ import TextField from 'discourse/components/text-field';
import userSearch from 'discourse/lib/user-search';
export default TextField.extend({
@observes('usernames')
_update() {
if (this.get('canReceiveUpdates') === 'true')
this.didInsertElement({updateData: true});
},
didInsertElement() {
didInsertElement(opts) {
this._super();
var self = this,
selected = [],
@ -29,6 +34,7 @@ export default TextField.extend({
disabled: this.get('disabled'),
single: this.get('single'),
allowAny: this.get('allowAny'),
updateData: (opts && opts.updateData) ? opts.updateData : false,
dataSource: function(term) {
var results = userSearch({

View File

@ -42,7 +42,7 @@ export default function(options) {
if (this.length === 0) return;
if (options === 'destroy') {
if (options === 'destroy' || options.updateData) {
Ember.run.cancel(inputTimeout);
$(this).off('keyup.autocomplete')
@ -50,6 +50,7 @@ export default function(options) {
.off('paste.autocomplete')
.off('click.autocomplete');
if (options === 'destroy')
return;
}
@ -162,28 +163,46 @@ export default function(options) {
if (isInput) {
const width = this.width();
wrap = this.wrap("<div class='ac-wrap clearfix" + (disabled ? " disabled": "") + "'/>").parent();
if (options.updateData) {
wrap = this.parent();
wrap.find('.item').remove();
me.show();
} else {
wrap = this.wrap("<div class='ac-wrap clearfix" + (disabled ? " disabled" : "") + "'/>").parent();
wrap.width(width);
}
if(options.single) {
this.css("width","100%");
} else {
this.width(150);
}
this.attr('name', this.attr('name') + "-renamed");
this.attr('name', (options.updateData) ? this.attr('name') : this.attr('name') + "-renamed");
var vals = this.val().split(",");
_.each(vals,function(x) {
if (x !== "") {
if (options.reverseTransform) {
x = options.reverseTransform(x);
}
if(options.single){
me.hide();
}
addInputSelectedItem(x);
}
});
if(options.items) {
_.each(options.items, function(item){
if(options.single){
me.hide();
}
addInputSelectedItem(item);
});
}
this.val("");
completeStart = 0;
wrap.click(function() {

View File

@ -135,7 +135,7 @@ Badge.reopenClass({
if(opts && opts.onlyListable){
listable = "?only_listable=true";
}
return ajax('/badges.json' + listable).then(function(badgesJson) {
return ajax('/badges.json' + listable, { data: opts }).then(function(badgesJson) {
return Badge.createFromJson(badgesJson);
});
},

View File

@ -0,0 +1,7 @@
<div class='autocomplete'>
<ul>
{{#each options as |option|}}
<li><a href>{{option.name}}</a></li>
{{/each}}
</ul>
</div>

View File

@ -0,0 +1 @@
<input class='ember-text-field badge-names' type="text" placeholder={{placeholder}} name="badges">

View File

@ -0,0 +1,78 @@
<h3 class="panel-title" {{action "expandOptions"}}>
<i class="fa {{collapsedClassName}}"></i> {{i18n "search.advanced.title"}}
</h3>
{{#unless isCollapsed}}
<div class="search-options">
<div class="container">
<div class="control-group pull-left">
<label class="control-label" for="search-posted-by">{{i18n "search.advanced.posted_by.label"}}</label>
<div class="controls">
{{user-selector excludeCurrentUser=false usernames=searchedTerms.username class="user-selector" single="true" canReceiveUpdates="true"}}
</div>
</div>
<div class="control-group pull-left">
<label class="control-label" for="search-in-category">{{i18n "search.advanced.in_category.label"}}</label>
<div class="controls">
{{category-chooser value=searchedTerms.category}}
</div>
</div>
</div>
<div class="container">
<div class="control-group pull-left">
<label class="control-label" for="search-in-group">{{i18n "search.advanced.in_group.label"}}</label>
<div class="controls">
{{group-selector groupFinder=groupFinder groupNames=searchedTerms.group single="true" canReceiveUpdates="true"}}
</div>
</div>
<div class="control-group pull-left">
<label class="control-label" for="search-with-badge">{{i18n "search.advanced.with_badge.label"}}</label>
<div class="controls">
{{badge-selector badgeFinder=badgeFinder badgeNames=searchedTerms.badge single="true" canReceiveUpdates="true"}}
</div>
</div>
</div>
{{#if siteSettings.tagging_enabled}}
<div class="container">
<div class="control-group">
<label class="control-label" for="search-with-tags">{{i18n "search.advanced.with_tags.label"}}</label>
<div class="controls">
{{tag-chooser tags=searchedTerms.tags blacklist=searchedTerms.tags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
</div>
</div>
</div>
{{/if}}
<div class="container">
<div class="control-group pull-left">
<label class="control-label" for="search-in-options">{{i18n "search.advanced.filters.label"}}</label>
<div class="controls">
{{combo-box id="in" valueAttribute="value" content=inOptions value=searchedTerms.in none="user.locale.any"}}
</div>
</div>
<div class="control-group pull-left">
<label class="control-label" for="search-status-options">{{i18n "search.advanced.statuses.label"}}</label>
<div class="controls">
{{combo-box id="status" valueAttribute="value" content=statusOptions value=searchedTerms.status none="user.locale.any"}}
</div>
</div>
</div>
<div class="container">
<div class="control-group pull-left">
<label class="control-label" for="search-post-date">{{i18n "search.advanced.post.time.label"}}</label>
<div class="controls">
{{combo-box id="postTime" valueAttribute="value" content=postTimeOptions value=searchedTerms.time.when}}
{{input type="text" value=searchedTerms.time.days class="input-small" id='search-post-date'}}
</div>
</div>
<div class="control-group pull-left">
<label class="control-label" for="search-posts-count">{{i18n "search.advanced.post.count.label"}}</label>
<div class="controls">
{{input type="number" value=searchedTerms.posts_count class="input-small" id='search-posts-count'}}
</div>
</div>
</div>
</div>
{{/unless}}

View File

@ -2,6 +2,8 @@
{{search-text-field value=searchTerm class="full-page-search input-xxlarge search no-blur" action="search" hasAutofocus=hasAutofocus}}
{{d-button action="search" icon="search" class="btn-primary" disabled=searching}}
{{search-advanced-options searchTerm=searchTerm}}
{{#if canCreateTopic}}
<span class="new-topic-btn">{{d-button id="create-topic" class="btn-default" action="createTopic" actionParam=searchTerm icon="plus" label="topic.create"}}</span>
{{/if}}

View File

@ -97,6 +97,24 @@
}
}
.search-advanced {
margin-bottom: 15px;
.panel-title {
background-color: dark-light-diff($primary, $secondary, 90%, -75%);
padding: 5px 10px 5px 10px;
}
.search-options {
border: 1px solid dark-light-diff($primary, $secondary, 90%, -75%);
padding: 10px;
.control-group.pull-left {
width: 50%;
}
}
}
.search-footer {
margin-bottom: 30px;
}

View File

@ -6,6 +6,11 @@ class BadgesController < ApplicationController
badges = Badge.all
if search = params[:search]
search = search.to_s
badges = badges.where("name ILIKE ?", "%#{search}%")
end
if (params[:only_listable] == "true") || !request.xhr?
# NOTE: this is sorted client side if needed
badges = badges.includes(:badge_grouping)

View File

@ -674,6 +674,7 @@ en:
title: "Interface language"
instructions: "User interface language. It will change when you refresh the page."
default: "(default)"
any: "any"
password_confirmation:
title: "Password Again"
@ -1217,6 +1218,45 @@ en:
topic: "Search this topic"
private_messages: "Search messages"
advanced:
title: Advanced Search
posted_by:
label: Posted by
in_category:
label: In Category
in_group:
label: In Group
with_badge:
label: With Badge
with_tags:
label: With Tags
filters:
label: Only return topics/posts that...
likes: I liked
posted: I posted in
watching: I'm watching
tracking: I'm tracking
private: are in my messages
bookmarks: I've bookmarked
first: are the very first post
pinned: are pinned
unpinned: are not pinned
wiki: are wiki
statuses:
label: Where topics
open: are open
closed: are closed
archived: are archived
noreplies: have zero replies
single_user: contain a single user
post:
count:
label: Minimum Post Count
time:
label: Posted
before: before
after: after
hamburger_menu: "go to another topic list or category"
new_item: "new"
go_back: 'go back'

View File

@ -1,5 +1,43 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Search - Full Page");
import { acceptance, waitFor } from "helpers/qunit-helpers";
acceptance("Search - Full Page", {
settings: {tagging_enabled: true},
setup() {
const response = (object) => {
return [
200,
{"Content-Type": "application/json"},
object
];
};
server.get('/tags/filter/search', () => { //eslint-disable-line
return response({results: [{text: 'monkey', count: 1}]});
});
server.get('/users/search/users', () => { //eslint-disable-line
return response({users: [{username: "admin", name: "admin",
avatar_template: "/letter_avatar_proxy/v2/letter/a/3ec8ea/{size}.png"}]});
});
server.get('/admin/groups.json', () => { //eslint-disable-line
return response([{id: 2, automatic: true, name: "moderators", user_count: 4, alias_level: 0,
visible: true, automatic_membership_email_domains: null, automatic_membership_retroactive: false,
primary_group: false, title: null, grant_trust_level: null, incoming_email: null,
notification_level: null, has_messages: true, is_member: true, mentionable: false,
flair_url: null, flair_bg_color: null, flair_color: null}]);
});
server.get('/badges.json', () => { //eslint-disable-line
return response({badge_types: [{id: 3, name: "Bronze", sort_order: 7}],
badge_groupings: [{id: 1, name: "Getting Started", description: null, position: 10, system: true}],
badges: [{id: 17, name: "Reader", description: "Read every reply in a topic with more than 100 replies",
grant_count: 0, allow_title: false, multiple_grant: false, icon: "fa-certificate", image: null,
listable: true, enabled: true, badge_grouping_id: 1, system: true,
long_description: "This badge is granted the first time you read a long topic with more than 100 replies. Reading a conversation closely helps you follow the discussion, understand different viewpoints, and leads to more interesting conversations. The more you read, the better the conversation gets. As we like to say, Reading is Fundamental! :slight_smile:\n",
slug: "reader", has_badge: false, badge_type_id: 3}]});
});
}
});
test("perform various searches", assert => {
visit("/search");
@ -9,13 +47,198 @@ test("perform various searches", assert => {
assert.ok(find('.fps-topic').length === 0);
});
fillIn('.search input', 'none');
fillIn('.search input.full-page-search', 'none');
click('.search .btn-primary');
andThen(() => assert.ok(find('.fps-topic').length === 0), 'has no results');
fillIn('.search input', 'posts');
fillIn('.search input.full-page-search', 'posts');
click('.search .btn-primary');
andThen(() => assert.ok(find('.fps-topic').length === 1, 'has one post'));
});
test("open advanced search", assert => {
visit("/search");
andThen(() => assert.ok(exists('.search .search-advanced'), 'shows advanced search panel'));
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
andThen(() => assert.ok(visible('.search-advanced .search-options'), '"search-options" is visible'));
});
test("validate population of advanced search", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'test user:admin #bug group:moderators badge:Reader tags:monkey in:likes status:open after:5 posts_count:10');
click('.search-advanced h3.panel-title');
andThen(() => {
assert.ok(exists('.search-options span:contains("admin")'), 'has "admin" pre-populated');
assert.ok(exists('.search-options .category-combobox .select2-choice .select2-chosen:contains("bug")'), 'has "bug" pre-populated');
assert.ok(exists('.search-options span:contains("moderators")'), 'has "moderators" pre-populated');
assert.ok(exists('.search-options span:contains("Reader")'), 'has "Reader" pre-populated');
assert.ok(exists('.search-options .tag-chooser .tag-monkey'), 'has "monkey" pre-populated');
assert.ok(exists('.search-options .combobox .select2-choice .select2-chosen:contains("I liked")'), 'has "I liked" pre-populated');
assert.ok(exists('.search-options .combobox .select2-choice .select2-chosen:contains("are open")'), 'has "are open" pre-populated');
assert.ok(exists('.search-options .combobox .select2-choice .select2-chosen:contains("after")'), 'has "after" pre-populated');
assert.equal(find('.search-options #search-post-date').val(), "5", 'has "5" pre-populated');
assert.equal(find('.search-options #search-posts-count').val(), "10", 'has "10" pre-populated');
});
});
test("update username through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
fillIn('.search-options .user-selector', 'admin');
click('.search-options .user-selector');
keyEvent('.search-options .user-selector', 'keydown', 8);
andThen(() => {
waitFor(() => {
assert.ok(visible('.search-options .autocomplete'), '"autocomplete" popup is visible');
assert.ok(exists('.search-options .autocomplete ul li a span.username:contains("admin")'), '"autocomplete" popup has an entry for "admin"');
click('.search-options .autocomplete ul li a:first');
andThen(() => {
assert.ok(exists('.search-options span:contains("admin")'), 'has "admin" pre-populated');
assert.equal(find('.search input.full-page-search').val(), "none user:admin", 'has updated search term to "none user:admin"');
});
});
});
});
test("update category through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
selectDropdown('.search-options .category-combobox', 4);
click('.search-options'); // need to click off the combobox for the search-term to get updated
andThen(() => {
assert.ok(exists('.search-options .category-combobox .select2-choice .select2-chosen:contains("faq")'), 'has "faq" populated');
assert.equal(find('.search input.full-page-search').val(), "none #faq", 'has updated search term to "none #faq"');
});
});
test("update group through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
fillIn('.search-options .group-selector', 'moderators');
click('.search-options .group-selector');
keyEvent('.search-options .group-selector', 'keydown', 8);
andThen(() => {
waitFor(() => {
assert.ok(visible('.search-options .autocomplete'), '"autocomplete" popup is visible');
assert.ok(exists('.search-options .autocomplete ul li a:contains("moderators")'), '"autocomplete" popup has an entry for "moderators"');
click('.search-options .autocomplete ul li a:first');
andThen(() => {
assert.ok(exists('.search-options span:contains("moderators")'), 'has "moderators" pre-populated');
assert.equal(find('.search input.full-page-search').val(), "none group:moderators", 'has updated search term to "none group:moderators"');
});
});
});
});
test("update badges through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
fillIn('.search-options .badge-names', 'Reader');
click('.search-options .badge-names');
keyEvent('.search-options .badge-names', 'keydown', 8);
andThen(() => {
waitFor(() => {
assert.ok(visible('.search-options .autocomplete'), '"autocomplete" popup is visible');
assert.ok(exists('.search-options .autocomplete ul li a:contains("Reader")'), '"autocomplete" popup has an entry for "Reader"');
click('.search-options .autocomplete ul li a:first');
andThen(() => {
assert.ok(exists('.search-options span:contains("Reader")'), 'has "Reader" pre-populated');
assert.equal(find('.search input.full-page-search').val(), "none badge:Reader", 'has updated search term to "none badge:Reader"');
});
});
});
});
// test("update tags through advanced search ui", assert => {
// visit("/search");
// fillIn('.search input.full-page-search', 'none');
// click('.search-advanced h3.panel-title');
//
// keyEvent('.search-options .tag-chooser input.select2-input', 'keydown', 110);
// fillIn('.search-options .tag-chooser input.select2-input', 'monkey');
// keyEvent('.search-options .tag-chooser input.select2-input', 'keyup', 110);
//
// andThen(() => {
// waitFor(() => {
// click('li.select2-result-selectable:first');
// andThen(() => {
// assert.ok(exists('.search-options .tag-chooser .tag-monkey'), 'has "monkey" pre-populated');
// assert.equal(find('.search input.full-page-search').val(), "none tags:monkey", 'has updated search term to "none tags:monkey"');
// });
// });
// });
// });
test("update in filter through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
selectDropdown('.search-options #s2id_in', 'likes');
fillIn('.search-options #in', 'likes');
andThen(() => {
assert.ok(exists('.search-options #s2id_in .select2-choice .select2-chosen:contains("I liked")'), 'has "I liked" populated');
assert.equal(find('.search input.full-page-search').val(), "none in:likes", 'has updated search term to "none in:likes"');
});
});
test("update status through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
selectDropdown('.search-options #s2id_status', 'closed');
fillIn('.search-options #status', 'closed');
andThen(() => {
assert.ok(exists('.search-options #s2id_status .select2-choice .select2-chosen:contains("are closed")'), 'has "are closed" populated');
assert.equal(find('.search input.full-page-search').val(), "none status:closed", 'has updated search term to "none status:closed"');
});
});
test("update post time through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
fillIn('#search-post-date', '5');
selectDropdown('.search-options #s2id_postTime', 'after');
fillIn('.search-options #postTime', 'after');
andThen(() => {
assert.ok(exists('.search-options #s2id_postTime .select2-choice .select2-chosen:contains("after")'), 'has "after" populated');
assert.equal(find('.search-options #search-post-date').val(), "5", 'has "5" populated');
assert.equal(find('.search input.full-page-search').val(), "none after:5", 'has updated search term to "none after:5"');
});
});
test("update posts count through advanced search ui", assert => {
visit("/search");
fillIn('.search input.full-page-search', 'none');
click('.search-advanced h3.panel-title');
fillIn('#search-posts-count', '5');
andThen(() => {
assert.equal(find('.search-options #search-posts-count').val(), "5", 'has "5" populated');
assert.equal(find('.search input.full-page-search').val(), "none posts_count:5", 'has updated search term to "none posts_count:5"');
});
});

View File

@ -109,6 +109,15 @@ function blank(obj, text) {
ok(Ember.isEmpty(obj), text);
}
function waitFor(callback, timeout) {
timeout = timeout || 500;
stop();
Ember.run.later(() => {
callback();
start();
}, timeout);
}
export { acceptance,
controllerFor,
asyncTestDiscourse,
@ -116,4 +125,5 @@ export { acceptance,
logIn,
currentUser,
blank,
present };
present,
waitFor };