FEATURE: Allow scoping search to tag (#8345)

* When viewing a tag, the search widget will now show a checkbox to scope the search by tag, which will limit search results to that tag on desktop and mobile
This commit is contained in:
Martin Brennan 2019-11-14 10:40:26 +10:00 committed by GitHub
parent 6e1fe22a9d
commit e7226a8c84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 204 additions and 7 deletions

View File

@ -145,7 +145,8 @@ export function searchForTerm(term, opts) {
if (opts.searchContext) {
data.search_context = {
type: opts.searchContext.type,
id: opts.searchContext.id
id: opts.searchContext.id,
name: opts.searchContext.name
};
}
@ -167,6 +168,8 @@ export function searchContextDescription(type, name) {
return I18n.t("search.context.user", { username: name });
case "category":
return I18n.t("search.context.category", { category: name });
case "tag":
return I18n.t("search.context.tag", { tag: name });
case "private_messages":
return I18n.t("search.context.private_messages");
}

View File

@ -10,5 +10,10 @@ export default RestModel.extend({
@discourseComputed("count", "pm_count")
pmOnly(count, pmCount) {
return count === 0 && pmCount > 0;
},
@discourseComputed("id")
searchContext(id) {
return { type: "tag", id, tag: this, name: id };
}
});

View File

@ -165,6 +165,12 @@ export default DiscourseRoute.extend({
tagNotification: this.tagNotification,
noSubcategories: this.noSubcategories
});
this.searchService.set("searchContext", model.get("searchContext"));
},
deactivate() {
this._super(...arguments);
this.searchService.set("searchContext", null);
},
actions: {

View File

@ -17,6 +17,7 @@
{{/if}}
</div>
{{!-- context is only provided when searching from mobile view --}}
<div class="search-context">
{{#if context}}
<div class='fps-search-context'>

View File

@ -267,7 +267,7 @@ createWidget("header-cloak", {
scheduleRerender() {}
});
const forceContextEnabled = ["category", "user", "private_messages"];
const forceContextEnabled = ["category", "user", "private_messages", "tag"];
let additionalPanels = [];
export function attachAdditionalPanel(name, toggle, transformAttrs) {

View File

@ -54,7 +54,7 @@ createWidget("search-context", {
if (ctx) {
const description = searchContextDescription(
get(ctx, "type"),
get(ctx, "user.username") || get(ctx, "category.name")
get(ctx, "user.username") || get(ctx, "category.name") || get(ctx, "tag.id")
);
result.push(
h("label", [

View File

@ -7,7 +7,7 @@ class SearchController < ApplicationController
before_action :cancel_overloaded_search, only: [:query]
def self.valid_context_types
%w{user topic category private_messages}
%w{user topic category private_messages tag}
end
def show
@ -169,6 +169,8 @@ class SearchController < ApplicationController
context_obj = Category.find_by(id: search_context[:id].to_i)
elsif 'topic' == search_context[:type]
context_obj = Topic.find_by(id: search_context[:id].to_i)
elsif 'tag' == search_context[:type]
context_obj = Tag.where_name(search_context[:name]).first
end
type_filter = nil

View File

@ -1867,6 +1867,7 @@ en:
context:
user: "Search posts by @{{username}}"
category: "Search the #{{category}} category"
tag: "Search the #{{tag}} tag"
topic: "Search this topic"
private_messages: "Search messages"

View File

@ -862,6 +862,11 @@ class Search
elsif @search_context.is_a?(Topic)
posts.where("topics.id = #{@search_context.id}")
.order("posts.post_number #{@order == :latest ? "DESC" : ""}")
elsif @search_context.is_a?(Tag)
posts = posts
.joins("LEFT JOIN topic_tags ON topic_tags.topic_id = topics.id")
.joins("LEFT JOIN tags ON tags.id = topic_tags.tag_id")
posts.where("tags.id = #{@search_context.id}")
end
else
posts = categories_ignored(posts) unless @category_filter_matched

View File

@ -784,14 +784,29 @@ describe Search do
sub_topic = Fabricate(:topic, category: subcategory)
post = Fabricate(:post, topic: topic, user: topic.user)
_another_post = Fabricate(:post, topic: topic_no_cat, user: topic.user)
Fabricate(:post, topic: topic_no_cat, user: topic.user)
sub_post = Fabricate(:post, raw: 'I am saying hello from a subcategory', topic: sub_topic, user: topic.user)
search = Search.execute('hello', search_context: category)
expect(search.posts.map(&:id).sort).to eq([post.id, sub_post.id].sort)
expect(search.posts.map(&:id)).to match_array([post.id, sub_post.id])
expect(search.posts.length).to eq(2)
end
it 'can use tag as a search context' do
tag = Fabricate(:tag, name: 'important-stuff')
topic = Fabricate(:topic)
topic_no_tag = Fabricate(:topic)
Fabricate(:topic_tag, tag: tag, topic: topic)
post = Fabricate(:post, topic: topic, user: topic.user, raw: 'This is my hello')
Fabricate(:post, topic: topic_no_tag, user: topic.user)
search = Search.execute('hello', search_context: tag)
expect(search.posts.map(&:id)).to contain_exactly(post.id)
expect(search.posts.length).to eq(1)
end
end
describe 'Chinese search' do

View File

@ -251,6 +251,23 @@ describe SearchController do
end
end
context "with a tag" do
it "raises an error if the tag does not exist" do
get "/search/query.json", params: {
term: 'test', search_context: { type: 'tag', id: 'important-tag', name: 'important-tag' }
}
expect(response).to be_forbidden
end
it 'performs the query with a search context' do
Fabricate(:tag, name: 'important-tag')
get "/search/query.json", params: {
term: 'test', search_context: { type: 'tag', id: 'important-tag', name: 'important-tag' }
}
expect(response.status).to eq(200)
end
end
end
context "#click" do

View File

@ -50,6 +50,14 @@ QUnit.test("search for a tag", async assert => {
});
QUnit.test("search scope checkbox", async assert => {
await visit("/tags/important");
await click("#search-button");
assert.ok(
exists(".search-context input:checked"),
"scope to tag checkbox is checked"
);
await click("#search-button");
await visit("/c/bug");
await click("#search-button");
assert.ok(

View File

@ -3756,6 +3756,113 @@ export default {
]
}
},
"/tags/important/l/latest.json": {
users: [{ id: 1, username: "sam", avatar_template: "/images/avatar.png" }],
primary_groups: [],
topic_list: {
can_create_topic: true,
draft: null,
draft_key: "new_topic",
draft_sequence: 4,
per_page: 30,
tags: [
{
id: 1,
name: "test",
topic_count: 2,
staff: false
}
],
topics: [
{
id: 16,
title: "Dinosaurs are the best",
fancy_title: "Dinosaurs are the best",
slug: "dinosaurs-are-the-best",
posts_count: 1,
reply_count: 0,
highest_post_number: 1,
image_url: null,
created_at: "2019-11-12T05:19:52.300Z",
last_posted_at: "2019-11-12T05:19:52.848Z",
bumped: true,
bumped_at: "2019-11-12T05:19:52.848Z",
unseen: false,
last_read_post_number: 1,
unread: 0,
new_posts: 0,
pinned: false,
unpinned: null,
visible: true,
closed: false,
archived: false,
notification_level: 3,
bookmarked: false,
liked: false,
tags: ["test"],
views: 2,
like_count: 0,
has_summary: false,
archetype: "regular",
last_poster_username: "sam",
category_id: 1,
pinned_globally: false,
featured_link: null,
posters: [
{
extras: "latest single",
description: "Original Poster, Most Recent Poster",
user_id: 1,
primary_group_id: null
}
]
},
{
id: 15,
title: "This is a test tagged post",
fancy_title: "This is a test tagged post",
slug: "this-is-a-test-tagged-post",
posts_count: 1,
reply_count: 0,
highest_post_number: 1,
image_url: null,
created_at: "2019-11-12T05:19:32.032Z",
last_posted_at: "2019-11-12T05:19:32.516Z",
bumped: true,
bumped_at: "2019-11-12T05:19:32.516Z",
unseen: false,
last_read_post_number: 1,
unread: 0,
new_posts: 0,
pinned: false,
unpinned: null,
visible: true,
closed: false,
archived: false,
notification_level: 3,
bookmarked: false,
liked: false,
tags: ["test"],
views: 1,
like_count: 0,
has_summary: false,
archetype: "regular",
last_poster_username: "sam",
category_id: 3,
pinned_globally: false,
featured_link: null,
posters: [
{
extras: "latest single",
description: "Original Poster, Most Recent Poster",
user_id: 1,
primary_group_id: null
}
]
}
]
}
},
"/c/feature/l/latest.json": {
users: [
{ id: 1, username: "sam", avatar_template: "/images/avatar.png" },

View File

@ -1,4 +1,7 @@
import { translateResults } from "discourse/lib/search";
import {
translateResults,
searchContextDescription
} from "discourse/lib/search";
QUnit.module("lib:search");
@ -31,3 +34,27 @@ QUnit.test("unescapesEmojisInBlurbs", assert => {
assert.ok(blurb.indexOf("<img src") === 0);
assert.ok(blurb.indexOf(":thinking:") === -1);
});
QUnit.test("searchContextDescription", assert => {
assert.equal(
searchContextDescription("topic"),
I18n.t("search.context.topic")
);
assert.equal(
searchContextDescription("user", "silvio.dante"),
I18n.t("search.context.user", { username: "silvio.dante" })
);
assert.equal(
searchContextDescription("category", "staff"),
I18n.t("search.context.category", { category: "staff" })
);
assert.equal(
searchContextDescription("tag", "important"),
I18n.t("search.context.tag", { tag: "important" })
);
assert.equal(
searchContextDescription("private_messages"),
I18n.t("search.context.private_messages")
);
assert.equal(searchContextDescription("bad_type"), null);
});