mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 15:16:08 +08:00
UX: Revamp quick search (#14499)
Co-authored-by: Robin Ward <robin.ward@gmail.com> Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
This commit is contained in:
parent
98d2836eb4
commit
e9b1d29d8b
|
@ -117,11 +117,8 @@ function translateGroupedSearchResults(results, opts) {
|
|||
const name = pair[1];
|
||||
if (results[name].length > 0) {
|
||||
const componentName =
|
||||
opts.searchContext &&
|
||||
opts.searchContext.type === "topic" &&
|
||||
type === "topic"
|
||||
? "post"
|
||||
: type;
|
||||
opts.showPosts && type === "topic" ? "post" : type;
|
||||
|
||||
const result = {
|
||||
results: results[name],
|
||||
componentName: `search-result-${componentName}`,
|
||||
|
@ -157,12 +154,8 @@ export function searchForTerm(term, opts) {
|
|||
data.restrict_to_archetype = opts.restrictToArchetype;
|
||||
}
|
||||
|
||||
if (opts.searchContext) {
|
||||
data.search_context = {
|
||||
type: opts.searchContext.type,
|
||||
id: opts.searchContext.id,
|
||||
name: opts.searchContext.name,
|
||||
};
|
||||
if (term.includes("topic:")) {
|
||||
opts.showPosts = true;
|
||||
}
|
||||
|
||||
let ajaxPromise = ajax("/search/query", { data });
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import EmberObject, { get } from "@ember/object";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
export default EmberObject.extend({
|
||||
searchContextEnabled: false, // checkbox to scope search
|
||||
searchContext: null,
|
||||
term: null,
|
||||
highlightTerm: null,
|
||||
|
||||
@observes("term")
|
||||
_sethighlightTerm() {
|
||||
this.set("highlightTerm", this.term);
|
||||
},
|
||||
|
||||
@discourseComputed("searchContext")
|
||||
contextType: {
|
||||
get(searchContext) {
|
||||
|
|
|
@ -1,34 +1,21 @@
|
|||
import I18n from "I18n";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { get } from "@ember/object";
|
||||
import { h } from "virtual-dom";
|
||||
import { searchContextDescription } from "discourse/lib/search";
|
||||
|
||||
createWidget("search-term", {
|
||||
tagName: "input",
|
||||
buildId: () => "search-term",
|
||||
buildKey: () => "search-term",
|
||||
|
||||
defaultState() {
|
||||
return { afterAutocomplete: false };
|
||||
},
|
||||
|
||||
buildAttributes(attrs) {
|
||||
return {
|
||||
type: "text",
|
||||
value: attrs.value || "",
|
||||
autocomplete: "discourse",
|
||||
placeholder: attrs.contextEnabled ? "" : I18n.t("search.title"),
|
||||
autocomplete: "off",
|
||||
placeholder: I18n.t("search.title"),
|
||||
"aria-label": I18n.t("search.title"),
|
||||
};
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
if (e.key === "Enter" && !this.state.afterAutocomplete) {
|
||||
return this.sendWidgetAction("fullSearch");
|
||||
}
|
||||
},
|
||||
|
||||
input(e) {
|
||||
const val = this.attrs.value;
|
||||
|
||||
|
@ -41,47 +28,9 @@ createWidget("search-term", {
|
|||
},
|
||||
});
|
||||
|
||||
// TODO: No longer used, remove in December 2021
|
||||
createWidget("search-context", {
|
||||
tagName: "div.search-context",
|
||||
|
||||
html(attrs) {
|
||||
const service = this.register.lookup("search-service:main");
|
||||
const ctx = service.get("searchContext");
|
||||
|
||||
const result = [];
|
||||
if (ctx) {
|
||||
const description = searchContextDescription(
|
||||
get(ctx, "type"),
|
||||
get(ctx, "user.username") ||
|
||||
get(ctx, "category.name") ||
|
||||
get(ctx, "tag.id")
|
||||
);
|
||||
result.push(
|
||||
h("label", [
|
||||
h("input", { type: "checkbox", checked: attrs.contextEnabled }),
|
||||
" ",
|
||||
description,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (!attrs.contextEnabled) {
|
||||
result.push(
|
||||
this.attach("link", {
|
||||
href: attrs.url,
|
||||
label: "show_help",
|
||||
className: "show-help",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
click() {
|
||||
const val = $(".search-context input").is(":checked");
|
||||
if (val !== this.attrs.contextEnabled) {
|
||||
this.sendWidgetAction("searchContextChanged", val);
|
||||
}
|
||||
html() {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { escapeExpression, formatUsername } from "discourse/lib/utilities";
|
||||
import { deepMerge } from "discourse-common/lib/object";
|
||||
import I18n from "I18n";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { avatarImg } from "discourse/widgets/post";
|
||||
|
@ -10,6 +11,10 @@ import { h } from "virtual-dom";
|
|||
import highlightSearch from "discourse/lib/highlight-search";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import renderTag from "discourse/lib/render-tag";
|
||||
import {
|
||||
MODIFIER_REGEXP,
|
||||
TOPIC_REPLACE_REGEXP,
|
||||
} from "discourse/widgets/search-menu";
|
||||
|
||||
const suggestionShortcuts = [
|
||||
"in:title",
|
||||
|
@ -24,6 +29,29 @@ const suggestionShortcuts = [
|
|||
"order:latest_topic",
|
||||
];
|
||||
|
||||
const QUICK_TIPS = [
|
||||
{
|
||||
label: "#",
|
||||
description: I18n.t("search.tips.category_tag"),
|
||||
},
|
||||
{
|
||||
label: "@",
|
||||
description: I18n.t("search.tips.author"),
|
||||
},
|
||||
{
|
||||
label: "in:",
|
||||
description: I18n.t("search.tips.in"),
|
||||
},
|
||||
{
|
||||
label: "status:",
|
||||
description: I18n.t("search.tips.status"),
|
||||
},
|
||||
{
|
||||
label: I18n.t("search.tips.full_search_key", { modifier: "Ctrl" }),
|
||||
description: I18n.t("search.tips.full_search"),
|
||||
},
|
||||
];
|
||||
|
||||
export function addSearchSuggestion(value) {
|
||||
if (suggestionShortcuts.indexOf(value) === -1) {
|
||||
suggestionShortcuts.push(value);
|
||||
|
@ -33,7 +61,7 @@ export function addSearchSuggestion(value) {
|
|||
class Highlighted extends RawHtml {
|
||||
constructor(html, term) {
|
||||
super({ html: `<span>${html}</span>` });
|
||||
this.term = term;
|
||||
this.term = term.replace(TOPIC_REPLACE_REGEXP, "");
|
||||
}
|
||||
|
||||
decorate($html) {
|
||||
|
@ -63,7 +91,6 @@ function createSearchResult({ type, linkField, builder }) {
|
|||
className: "search-link",
|
||||
searchResultId,
|
||||
searchResultType: type,
|
||||
searchContextEnabled: attrs.searchContextEnabled,
|
||||
searchLogId: attrs.searchLogId,
|
||||
})
|
||||
);
|
||||
|
@ -95,7 +122,10 @@ createSearchResult({
|
|||
linkField: "url",
|
||||
builder(t) {
|
||||
const tag = escapeExpression(t.id);
|
||||
return new RawHtml({ html: renderTag(tag, { tagName: "span" }) });
|
||||
return [
|
||||
iconNode("tag"),
|
||||
new RawHtml({ html: renderTag(tag, { tagName: "span" }) }),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -227,84 +257,82 @@ createWidget("search-menu-results", {
|
|||
tagName: "div.results",
|
||||
|
||||
html(attrs) {
|
||||
if (attrs.suggestionKeyword) {
|
||||
const { term, suggestionKeyword, results, searchTopics } = attrs;
|
||||
|
||||
if (suggestionKeyword) {
|
||||
return this.attach("search-menu-assistant", {
|
||||
fullTerm: attrs.term,
|
||||
suggestionKeyword: attrs.suggestionKeyword,
|
||||
term,
|
||||
suggestionKeyword,
|
||||
results: attrs.suggestionResults || [],
|
||||
});
|
||||
}
|
||||
|
||||
if (attrs.invalidTerm) {
|
||||
if (searchTopics && attrs.invalidTerm) {
|
||||
return h("div.no-results", I18n.t("search.too_short"));
|
||||
}
|
||||
|
||||
if (attrs.noResults) {
|
||||
if (searchTopics && attrs.noResults) {
|
||||
return h("div.no-results", I18n.t("search.no_results"));
|
||||
}
|
||||
|
||||
const results = attrs.results;
|
||||
if (!term) {
|
||||
return this.attach("search-menu-initial-options", {
|
||||
term,
|
||||
});
|
||||
}
|
||||
|
||||
const resultTypes = results.resultTypes || [];
|
||||
|
||||
const mainResultsContent = [];
|
||||
const usersAndGroups = [];
|
||||
const categoriesAndTags = [];
|
||||
const usersAndGroupsMore = [];
|
||||
const categoriesAndTagsMore = [];
|
||||
|
||||
const buildMoreNode = (result) => {
|
||||
const more = [];
|
||||
|
||||
const moreArgs = {
|
||||
className: "filter",
|
||||
className: "filter search-link",
|
||||
contents: () => [I18n.t("more"), "..."],
|
||||
};
|
||||
|
||||
if (result.moreUrl) {
|
||||
more.push(
|
||||
this.attach("link", $.extend(moreArgs, { href: result.moreUrl }))
|
||||
return this.attach(
|
||||
"link",
|
||||
deepMerge(moreArgs, {
|
||||
href: result.moreUrl,
|
||||
})
|
||||
);
|
||||
} else if (result.more) {
|
||||
more.push(
|
||||
this.attach(
|
||||
"link",
|
||||
$.extend(moreArgs, {
|
||||
action: "moreOfType",
|
||||
actionParam: result.type,
|
||||
className: "filter filter-type",
|
||||
})
|
||||
)
|
||||
return this.attach(
|
||||
"link",
|
||||
deepMerge(moreArgs, {
|
||||
action: "moreOfType",
|
||||
actionParam: result.type,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (more.length) {
|
||||
return more;
|
||||
}
|
||||
};
|
||||
|
||||
const assignContainer = (result, node) => {
|
||||
if (["topic"].includes(result.type)) {
|
||||
mainResultsContent.push(node);
|
||||
}
|
||||
if (searchTopics) {
|
||||
if (["topic"].includes(result.type)) {
|
||||
mainResultsContent.push(node);
|
||||
}
|
||||
} else {
|
||||
if (["user", "group"].includes(result.type)) {
|
||||
usersAndGroups.push(node);
|
||||
}
|
||||
|
||||
if (["user", "group"].includes(result.type)) {
|
||||
usersAndGroups.push(node);
|
||||
usersAndGroupsMore.push(buildMoreNode(result));
|
||||
}
|
||||
|
||||
if (["category", "tag"].includes(result.type)) {
|
||||
categoriesAndTags.push(node);
|
||||
categoriesAndTagsMore.push(buildMoreNode(result));
|
||||
if (["category", "tag"].includes(result.type)) {
|
||||
categoriesAndTags.push(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
resultTypes.forEach((rt) => {
|
||||
const resultNodeContents = [
|
||||
this.attach(rt.componentName, {
|
||||
searchContextEnabled: attrs.searchContextEnabled,
|
||||
searchLogId: attrs.results.grouped_search_result.search_log_id,
|
||||
results: rt.results,
|
||||
term: attrs.term,
|
||||
term,
|
||||
}),
|
||||
];
|
||||
|
||||
|
@ -320,31 +348,19 @@ createWidget("search-menu-results", {
|
|||
|
||||
const content = [];
|
||||
|
||||
if (mainResultsContent.length) {
|
||||
content.push(h("div.main-results", mainResultsContent));
|
||||
}
|
||||
|
||||
if (usersAndGroups.length || categoriesAndTags.length) {
|
||||
const secondaryResultsContents = [];
|
||||
|
||||
secondaryResultsContents.push(usersAndGroups);
|
||||
secondaryResultsContents.push(usersAndGroupsMore);
|
||||
|
||||
if (usersAndGroups.length && categoriesAndTags.length) {
|
||||
secondaryResultsContents.push(h("div.separator"));
|
||||
if (!searchTopics) {
|
||||
content.push(this.attach("search-menu-initial-options", { term }));
|
||||
} else {
|
||||
if (mainResultsContent.length) {
|
||||
content.push(mainResultsContent);
|
||||
} else {
|
||||
return h("div.no-results", I18n.t("search.no_results"));
|
||||
}
|
||||
|
||||
secondaryResultsContents.push(categoriesAndTags);
|
||||
secondaryResultsContents.push(categoriesAndTagsMore);
|
||||
|
||||
const secondaryResults = h(
|
||||
"div.secondary-results",
|
||||
secondaryResultsContents
|
||||
);
|
||||
|
||||
content.push(secondaryResults);
|
||||
}
|
||||
|
||||
content.push(categoriesAndTags);
|
||||
content.push(usersAndGroups);
|
||||
|
||||
return content;
|
||||
},
|
||||
});
|
||||
|
@ -369,8 +385,8 @@ createWidget("search-menu-assistant", {
|
|||
}
|
||||
|
||||
const content = [];
|
||||
const { fullTerm, suggestionKeyword } = attrs;
|
||||
let prefix = fullTerm.split(suggestionKeyword)[0].trim() || "";
|
||||
const { suggestionKeyword, term } = attrs;
|
||||
let prefix = term?.split(suggestionKeyword)[0].trim() || "";
|
||||
|
||||
if (prefix.length) {
|
||||
prefix = `${prefix} `;
|
||||
|
@ -388,7 +404,8 @@ createWidget("search-menu-assistant", {
|
|||
this.attach("search-menu-assistant-item", {
|
||||
prefix,
|
||||
category: item.model,
|
||||
slug: `${prefix}${fullSlug} `,
|
||||
slug: `${prefix}${fullSlug}`,
|
||||
withInLabel: attrs.withInLabel,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
@ -396,7 +413,8 @@ createWidget("search-menu-assistant", {
|
|||
this.attach("search-menu-assistant-item", {
|
||||
prefix,
|
||||
tag: item.name,
|
||||
slug: `${prefix}#${item.name} `,
|
||||
slug: `${prefix}#${item.name}`,
|
||||
withInLabel: attrs.withInLabel,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -408,17 +426,17 @@ createWidget("search-menu-assistant", {
|
|||
this.attach("search-menu-assistant-item", {
|
||||
prefix,
|
||||
user,
|
||||
slug: `${prefix}@${user.username} `,
|
||||
slug: `${prefix}@${user.username}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
suggestionShortcuts.forEach((item) => {
|
||||
if (item.includes(suggestionKeyword)) {
|
||||
if (item.includes(suggestionKeyword) || !suggestionKeyword) {
|
||||
content.push(
|
||||
this.attach("search-menu-assistant-item", {
|
||||
slug: `${prefix}${item} `,
|
||||
slug: `${prefix}${item}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -430,42 +448,146 @@ createWidget("search-menu-assistant", {
|
|||
},
|
||||
});
|
||||
|
||||
createWidget("search-menu-initial-options", {
|
||||
tagName: "ul.search-menu-initial-options",
|
||||
|
||||
html(attrs) {
|
||||
if (attrs.term?.match(MODIFIER_REGEXP)) {
|
||||
return this.defaultRow(attrs.term);
|
||||
}
|
||||
|
||||
const service = this.register.lookup("search-service:main");
|
||||
const ctx = service.get("searchContext");
|
||||
|
||||
const content = [];
|
||||
if (attrs.term) {
|
||||
if (ctx) {
|
||||
const term = attrs.term ? `${attrs.term} ` : "";
|
||||
|
||||
switch (ctx.type) {
|
||||
case "topic":
|
||||
content.push(
|
||||
this.attach("search-menu-assistant-item", {
|
||||
slug: `${term}topic:${ctx.id}`,
|
||||
label: [
|
||||
h("span", term),
|
||||
h("span.label-suffix", I18n.t("search.in_this_topic")),
|
||||
],
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case "private_messages":
|
||||
content.push(
|
||||
this.attach("search-menu-assistant-item", {
|
||||
slug: `${term}in:personal`,
|
||||
})
|
||||
);
|
||||
break;
|
||||
|
||||
case "category":
|
||||
const fullSlug = ctx.category.parentCategory
|
||||
? `#${ctx.category.parentCategory.slug}:${ctx.category.slug}`
|
||||
: `#${ctx.category.slug}`;
|
||||
|
||||
content.push(
|
||||
this.attach("search-menu-assistant", {
|
||||
term: `${term}${fullSlug}`,
|
||||
suggestionKeyword: "#",
|
||||
results: [{ model: ctx.category }],
|
||||
withInLabel: true,
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
case "tag":
|
||||
content.push(
|
||||
this.attach("search-menu-assistant", {
|
||||
term: `${term}#${ctx.name}`,
|
||||
suggestionKeyword: "#",
|
||||
results: [{ name: ctx.name }],
|
||||
withInLabel: true,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "user":
|
||||
content.push(
|
||||
this.attach("search-menu-assistant-item", {
|
||||
slug: `${term}@${ctx.user.username}`,
|
||||
label: [
|
||||
h("span", term),
|
||||
h(
|
||||
"span.label-suffix",
|
||||
I18n.t("search.in_posts_by", {
|
||||
username: ctx.user.username,
|
||||
})
|
||||
),
|
||||
],
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const rowOptions = { withLabel: true };
|
||||
content.push(this.defaultRow(attrs.term, rowOptions));
|
||||
return content;
|
||||
}
|
||||
|
||||
if (content.length === 0) {
|
||||
content.push(this.attach("random-quick-tip"));
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
defaultRow(term, opts = { withLabel: false }) {
|
||||
return this.attach("search-menu-assistant-item", {
|
||||
slug: term,
|
||||
label: [
|
||||
h("span", `${term} `),
|
||||
h(
|
||||
"span.label-suffix",
|
||||
opts.withLabel ? I18n.t("search.in_topics_posts") : null
|
||||
),
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("search-menu-assistant-item", {
|
||||
tagName: "li.search-menu-assistant-item",
|
||||
|
||||
html(attrs) {
|
||||
const prefix = attrs.prefix?.trim();
|
||||
const attributes = {};
|
||||
attributes.href = "#";
|
||||
|
||||
let content = [iconNode("search")];
|
||||
|
||||
if (prefix) {
|
||||
content.push(h("span.search-item-prefix", `${prefix} `));
|
||||
}
|
||||
|
||||
if (attrs.withInLabel) {
|
||||
content.push(h("span.label-suffix", `${I18n.t("search.in")} `));
|
||||
}
|
||||
|
||||
if (attrs.category) {
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: attrs.category.url,
|
||||
},
|
||||
},
|
||||
[
|
||||
h("span.search-item-prefix", prefix),
|
||||
this.attach("category-link", {
|
||||
category: attrs.category,
|
||||
allowUncategorized: true,
|
||||
recursive: true,
|
||||
}),
|
||||
]
|
||||
attributes.href = attrs.category.url;
|
||||
|
||||
content.push(
|
||||
this.attach("category-link", {
|
||||
category: attrs.category,
|
||||
allowUncategorized: true,
|
||||
recursive: true,
|
||||
})
|
||||
);
|
||||
} else if (attrs.tag) {
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: getURL(`/tag/${attrs.tag}`),
|
||||
},
|
||||
},
|
||||
[
|
||||
h("span.search-item-prefix", prefix),
|
||||
iconNode("tag"),
|
||||
h("span.search-item-tag", attrs.tag),
|
||||
]
|
||||
);
|
||||
attributes.href = getURL(`/tag/${attrs.tag}`);
|
||||
|
||||
content.push(iconNode("tag"));
|
||||
content.push(h("span.search-item-tag", attrs.tag));
|
||||
} else if (attrs.user) {
|
||||
const userResult = [
|
||||
avatarImg("small", {
|
||||
|
@ -474,30 +596,11 @@ createWidget("search-menu-assistant-item", {
|
|||
}),
|
||||
h("span.username", formatUsername(attrs.user.username)),
|
||||
];
|
||||
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: "#",
|
||||
},
|
||||
},
|
||||
[
|
||||
h("span.search-item-prefix", prefix),
|
||||
h("span.search-item-user", userResult),
|
||||
]
|
||||
);
|
||||
content.push(h("span.search-item-user", userResult));
|
||||
} else {
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: "#",
|
||||
},
|
||||
},
|
||||
h("span.search-item-slug", attrs.slug)
|
||||
);
|
||||
content.push(h("span.search-item-slug", attrs.label || attrs.slug));
|
||||
}
|
||||
return h("a.widget-link.search-link", { attributes }, content);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
|
@ -509,3 +612,15 @@ createWidget("search-menu-assistant-item", {
|
|||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("random-quick-tip", {
|
||||
tagName: "li.search-random-quick-tip",
|
||||
|
||||
html() {
|
||||
const item = QUICK_TIPS[Math.floor(Math.random() * QUICK_TIPS.length)];
|
||||
return [
|
||||
h("span.tip-label", item.label),
|
||||
h("span.tip-description", item.description),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,9 +2,11 @@ import { isValidSearchTerm, searchForTerm } from "discourse/lib/search";
|
|||
import DiscourseURL from "discourse/lib/url";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { get } from "@ember/object";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { h } from "virtual-dom";
|
||||
import I18n from "I18n";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { isiPad } from "discourse/lib/utilities";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { Promise } from "rsvp";
|
||||
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
|
||||
|
@ -14,6 +16,9 @@ import { CANCELLED_STATUS } from "discourse/lib/autocomplete";
|
|||
const CATEGORY_SLUG_REGEXP = /(\#[a-zA-Z0-9\-:]*)$/gi;
|
||||
const USERNAME_REGEXP = /(\@[a-zA-Z0-9\-\_]*)$/gi;
|
||||
const SUGGESTIONS_REGEXP = /(in:|status:|order:|:)([a-zA-Z]*)$/gi;
|
||||
export const TOPIC_REPLACE_REGEXP = /\stopic:\d+/i;
|
||||
export const MODIFIER_REGEXP = /.*(\#|\@|:).*$/gi;
|
||||
export const DEFAULT_TYPE_FILTER = "exclude_topics";
|
||||
|
||||
const searchData = {};
|
||||
|
||||
|
@ -22,10 +27,8 @@ export function initSearchData() {
|
|||
searchData.results = {};
|
||||
searchData.noResults = false;
|
||||
searchData.term = undefined;
|
||||
searchData.typeFilter = null;
|
||||
searchData.typeFilter = DEFAULT_TYPE_FILTER;
|
||||
searchData.invalidTerm = false;
|
||||
searchData.topicId = null;
|
||||
searchData.afterAutocomplete = false;
|
||||
searchData.suggestionResults = [];
|
||||
}
|
||||
|
||||
|
@ -46,8 +49,7 @@ const SearchHelper = {
|
|||
perform(widget) {
|
||||
this.cancel();
|
||||
|
||||
const { term, typeFilter, contextEnabled } = searchData;
|
||||
const searchContext = contextEnabled ? widget.searchContext() : null;
|
||||
const { term, typeFilter } = searchData;
|
||||
const fullSearchUrl = widget.fullSearchUrl();
|
||||
const matchSuggestions = this.matchesSuggestions();
|
||||
|
||||
|
@ -105,7 +107,14 @@ const SearchHelper = {
|
|||
|
||||
searchData.suggestionKeyword = false;
|
||||
|
||||
if (!isValidSearchTerm(term, widget.siteSettings)) {
|
||||
if (!term) {
|
||||
searchData.noResults = false;
|
||||
searchData.results = [];
|
||||
searchData.loading = false;
|
||||
searchData.invalidTerm = false;
|
||||
|
||||
widget.scheduleRerender();
|
||||
} else if (!isValidSearchTerm(term, widget.siteSettings)) {
|
||||
searchData.noResults = true;
|
||||
searchData.results = [];
|
||||
searchData.loading = false;
|
||||
|
@ -114,9 +123,9 @@ const SearchHelper = {
|
|||
widget.scheduleRerender();
|
||||
} else {
|
||||
searchData.invalidTerm = false;
|
||||
|
||||
this._activeSearch = searchForTerm(term, {
|
||||
typeFilter,
|
||||
searchContext,
|
||||
fullSearchUrl,
|
||||
});
|
||||
this._activeSearch
|
||||
|
@ -124,49 +133,50 @@ const SearchHelper = {
|
|||
// we ensure the current search term is the one used
|
||||
// when starting the query
|
||||
if (results && term === searchData.term) {
|
||||
if (term.includes("topic:")) {
|
||||
widget.appEvents.trigger("post-stream:refresh", { force: true });
|
||||
}
|
||||
|
||||
searchData.noResults = results.resultTypes.length === 0;
|
||||
searchData.results = results;
|
||||
|
||||
if (searchContext && searchContext.type === "topic") {
|
||||
widget.appEvents.trigger("post-stream:refresh", { force: true });
|
||||
searchData.topicId = searchContext.id;
|
||||
} else {
|
||||
searchData.topicId = null;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
searchData.loading = false;
|
||||
searchData.afterAutocomplete = false;
|
||||
widget.scheduleRerender();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
matchesSuggestions() {
|
||||
if (searchData.term === undefined) {
|
||||
if (searchData.term === undefined || this.includesTopics()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const categoriesMatch = searchData.term.match(CATEGORY_SLUG_REGEXP);
|
||||
const term = searchData.term.trim();
|
||||
const categoriesMatch = term.match(CATEGORY_SLUG_REGEXP);
|
||||
|
||||
if (categoriesMatch) {
|
||||
return { type: "category", categoriesMatch };
|
||||
}
|
||||
|
||||
const usernamesMatch = searchData.term.match(USERNAME_REGEXP);
|
||||
const usernamesMatch = term.match(USERNAME_REGEXP);
|
||||
if (usernamesMatch) {
|
||||
return { type: "username", usernamesMatch };
|
||||
}
|
||||
|
||||
const suggestionsMatch = searchData.term.match(SUGGESTIONS_REGEXP);
|
||||
const suggestionsMatch = term.match(SUGGESTIONS_REGEXP);
|
||||
if (suggestionsMatch) {
|
||||
return suggestionsMatch;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
includesTopics() {
|
||||
return searchData.typeFilter !== DEFAULT_TYPE_FILTER;
|
||||
},
|
||||
};
|
||||
|
||||
export default createWidget("search-menu", {
|
||||
|
@ -174,11 +184,6 @@ export default createWidget("search-menu", {
|
|||
searchData,
|
||||
|
||||
fullSearchUrl(opts) {
|
||||
const contextEnabled = searchData.contextEnabled;
|
||||
|
||||
const ctx = contextEnabled ? this.searchContext() : null;
|
||||
const type = ctx ? get(ctx, "type") : null;
|
||||
|
||||
let url = "/search";
|
||||
const params = [];
|
||||
|
||||
|
@ -187,24 +192,6 @@ export default createWidget("search-menu", {
|
|||
|
||||
query += `q=${encodeURIComponent(searchData.term)}`;
|
||||
|
||||
if (contextEnabled && ctx) {
|
||||
if (type === "private_messages") {
|
||||
if (
|
||||
this.currentUser &&
|
||||
ctx.id.toString().toLowerCase() ===
|
||||
this.currentUser.get("username_lower")
|
||||
) {
|
||||
query += " in:personal";
|
||||
} else {
|
||||
query += encodeURIComponent(
|
||||
` personal_messages:${ctx.id.toString().toLowerCase()}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query += encodeURIComponent(" " + type + ":" + ctx.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (query) {
|
||||
params.push(query);
|
||||
}
|
||||
|
@ -222,37 +209,47 @@ export default createWidget("search-menu", {
|
|||
},
|
||||
|
||||
panelContents() {
|
||||
const { contextEnabled, afterAutocomplete } = searchData;
|
||||
|
||||
let searchInput = [
|
||||
this.attach(
|
||||
"search-term",
|
||||
{ value: searchData.term, contextEnabled },
|
||||
{ state: { afterAutocomplete } }
|
||||
),
|
||||
];
|
||||
if (searchData.term && searchData.loading) {
|
||||
let searchInput = [this.attach("search-term", { value: searchData.term })];
|
||||
if (searchData.loading) {
|
||||
searchInput.push(h("div.searching", h("div.spinner")));
|
||||
} else {
|
||||
const clearButton = this.attach("link", {
|
||||
attributes: {
|
||||
title: I18n.t("search.clear_search"),
|
||||
},
|
||||
action: "clearSearch",
|
||||
className: "clear-search",
|
||||
contents: () => iconNode("times"),
|
||||
});
|
||||
|
||||
const advancedSearchButton = this.attach("link", {
|
||||
href: this.fullSearchUrl({ expanded: true }),
|
||||
contents: () => iconNode("sliders-h"),
|
||||
className: "show-advanced-search",
|
||||
title: I18n.t("search.open_advanced"),
|
||||
});
|
||||
|
||||
if (searchData.term) {
|
||||
searchInput.push(
|
||||
h("div.searching", [clearButton, advancedSearchButton])
|
||||
);
|
||||
} else {
|
||||
searchInput.push(h("div.searching", advancedSearchButton));
|
||||
}
|
||||
}
|
||||
|
||||
const results = [
|
||||
h("div.search-input", searchInput),
|
||||
this.attach("search-context", {
|
||||
contextEnabled,
|
||||
url: this.fullSearchUrl({ expanded: true }),
|
||||
}),
|
||||
];
|
||||
const results = [h("div.search-input", searchInput)];
|
||||
|
||||
if (searchData.term && !searchData.loading) {
|
||||
if (!searchData.loading) {
|
||||
results.push(
|
||||
this.attach("search-menu-results", {
|
||||
term: searchData.term,
|
||||
noResults: searchData.noResults,
|
||||
results: searchData.results,
|
||||
invalidTerm: searchData.invalidTerm,
|
||||
searchContextEnabled: searchData.contextEnabled,
|
||||
suggestionKeyword: searchData.suggestionKeyword,
|
||||
suggestionResults: searchData.suggestionResults,
|
||||
searchTopics: SearchHelper.includesTopics(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -260,6 +257,14 @@ export default createWidget("search-menu", {
|
|||
return results;
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
searchData.term = "";
|
||||
const searchInput = document.getElementById("search-term");
|
||||
searchInput.value = "";
|
||||
searchInput.focus();
|
||||
this.triggerSearch();
|
||||
},
|
||||
|
||||
searchService() {
|
||||
if (!this._searchService) {
|
||||
this._searchService = this.register.lookup("search-service:main");
|
||||
|
@ -267,29 +272,7 @@ export default createWidget("search-menu", {
|
|||
return this._searchService;
|
||||
},
|
||||
|
||||
searchContext() {
|
||||
if (!this._searchContext) {
|
||||
this._searchContext = this.searchService().get("searchContext");
|
||||
}
|
||||
return this._searchContext;
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const searchContext = this.searchContext();
|
||||
|
||||
const shouldTriggerSearch =
|
||||
searchData.contextEnabled !== attrs.contextEnabled ||
|
||||
(searchContext &&
|
||||
searchContext.type === "topic" &&
|
||||
searchData.topicId !== null &&
|
||||
searchData.topicId !== searchContext.id);
|
||||
|
||||
if (shouldTriggerSearch && searchData.term) {
|
||||
this.triggerSearch();
|
||||
}
|
||||
|
||||
searchData.contextEnabled = attrs.contextEnabled;
|
||||
|
||||
html() {
|
||||
return this.attach("menu-panel", {
|
||||
maxWidth: 500,
|
||||
contents: () => this.panelContents(),
|
||||
|
@ -312,18 +295,21 @@ export default createWidget("search-menu", {
|
|||
}
|
||||
|
||||
if (e.which === 65 /* a */) {
|
||||
let focused = $("header .results .search-link:focus");
|
||||
if (focused.length === 1) {
|
||||
if ($("#reply-control.open").length === 1) {
|
||||
if (document.activeElement?.classList.contains("search-link")) {
|
||||
if (document.querySelector("#reply-control.open")) {
|
||||
// add a link and focus composer
|
||||
|
||||
this.appEvents.trigger("composer:insert-text", focused[0].href, {
|
||||
ensureSpace: true,
|
||||
});
|
||||
this.appEvents.trigger(
|
||||
"composer:insert-text",
|
||||
document.activeElement.getAttribute("href"),
|
||||
{
|
||||
ensureSpace: true,
|
||||
}
|
||||
);
|
||||
this.appEvents.trigger("header:keyboard-trigger", { type: "search" });
|
||||
|
||||
e.preventDefault();
|
||||
$("#reply-control.open textarea").focus();
|
||||
document.querySelector("#reply-control.open textarea").focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -332,20 +318,28 @@ export default createWidget("search-menu", {
|
|||
const up = e.which === 38;
|
||||
const down = e.which === 40;
|
||||
if (up || down) {
|
||||
let focused = $(".search-menu *:focus")[0];
|
||||
let focused = document.activeElement.closest(".search-menu")
|
||||
? document.activeElement
|
||||
: null;
|
||||
|
||||
if (!focused) {
|
||||
return;
|
||||
}
|
||||
|
||||
let links = $(".search-menu .results a");
|
||||
let results = $(".search-menu .results .search-link");
|
||||
let links = document.querySelectorAll(".search-menu .results a");
|
||||
let results = document.querySelectorAll(
|
||||
".search-menu .results .search-link"
|
||||
);
|
||||
|
||||
if (!results.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prevResult;
|
||||
let result;
|
||||
|
||||
links.each((idx, item) => {
|
||||
if ($(item).hasClass("search-link")) {
|
||||
links.forEach((item) => {
|
||||
if (item.classList.contains("search-link")) {
|
||||
prevResult = item;
|
||||
}
|
||||
|
||||
|
@ -357,30 +351,46 @@ export default createWidget("search-menu", {
|
|||
let index = -1;
|
||||
|
||||
if (result) {
|
||||
index = results.index(result);
|
||||
index = Array.prototype.indexOf.call(results, result);
|
||||
}
|
||||
|
||||
if (index === -1 && down) {
|
||||
$(".search-menu .search-link:first").focus();
|
||||
document.querySelector(".search-menu .results .search-link").focus();
|
||||
} else if (index === 0 && up) {
|
||||
$(".search-menu input:first").focus();
|
||||
document.querySelector(".search-menu input#search-term").focus();
|
||||
} else if (index > -1) {
|
||||
index += down ? 1 : -1;
|
||||
if (index >= 0 && index < results.length) {
|
||||
$(results[index]).focus();
|
||||
results[index].focus();
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchInput = document.querySelector("#search-term");
|
||||
if (e.which === 13 && e.target === searchInput) {
|
||||
// same combination as key-enter-escape mixin
|
||||
if (e.ctrlKey || e.metaKey || (isiPad() && e.altKey)) {
|
||||
this.fullSearch();
|
||||
} else {
|
||||
searchData.typeFilter = null;
|
||||
this.triggerSearch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
triggerSearch() {
|
||||
searchData.noResults = false;
|
||||
this.searchService().set("highlightTerm", searchData.term);
|
||||
searchData.loading = true;
|
||||
discourseDebounce(SearchHelper, SearchHelper.perform, this, 400);
|
||||
if (searchData.term.includes("topic:")) {
|
||||
const highlightTerm = searchData.term.replace(TOPIC_REPLACE_REGEXP, "");
|
||||
this.searchService().set("highlightTerm", highlightTerm);
|
||||
}
|
||||
searchData.loading = SearchHelper.includesTopics() ? true : false;
|
||||
|
||||
const delay = SearchHelper.includesTopics() ? 400 : 200;
|
||||
discourseDebounce(SearchHelper, SearchHelper.perform, this, delay);
|
||||
},
|
||||
|
||||
moreOfType(type) {
|
||||
|
@ -388,30 +398,17 @@ export default createWidget("search-menu", {
|
|||
this.triggerSearch();
|
||||
},
|
||||
|
||||
searchContextChanged(enabled) {
|
||||
// This indicates the checkbox has been clicked, NOT that the context has changed.
|
||||
searchData.typeFilter = null;
|
||||
this.sendWidgetAction("searchMenuContextChanged", enabled);
|
||||
searchData.contextEnabled = enabled;
|
||||
this.triggerSearch();
|
||||
},
|
||||
|
||||
searchTermChanged(term) {
|
||||
searchData.typeFilter = null;
|
||||
searchTermChanged(term, opts = {}) {
|
||||
searchData.typeFilter = opts.searchTopics ? null : DEFAULT_TYPE_FILTER;
|
||||
searchData.term = term;
|
||||
this.triggerSearch();
|
||||
},
|
||||
|
||||
triggerAutocomplete(term) {
|
||||
searchData.afterAutocomplete = true;
|
||||
this.searchTermChanged(term);
|
||||
this.searchTermChanged(term, { searchTopics: true });
|
||||
},
|
||||
|
||||
fullSearch() {
|
||||
if (!isValidSearchTerm(searchData.term, this.siteSettings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchData.results = [];
|
||||
searchData.loading = false;
|
||||
SearchHelper.cancel();
|
||||
|
|
|
@ -245,9 +245,14 @@ acceptance("Group - Authenticated", function (needs) {
|
|||
);
|
||||
|
||||
await click("#search-button");
|
||||
assert.ok(
|
||||
exists(".search-context input:checked"),
|
||||
"scope to message checkbox is checked"
|
||||
await fillIn("#search-term", "smth");
|
||||
|
||||
assert.equal(
|
||||
query(
|
||||
".search-menu .results .search-menu-assistant-item:first-child"
|
||||
).innerText.trim(),
|
||||
"smth in:personal",
|
||||
"contextual search is available as first option"
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -3,22 +3,28 @@ import {
|
|||
count,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
import I18n from "I18n";
|
||||
import searchFixtures from "discourse/tests/fixtures/search-fixtures";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { test } from "qunit";
|
||||
import { DEFAULT_TYPE_FILTER } from "discourse/widgets/search-menu";
|
||||
|
||||
acceptance("Search - Anonymous", function (needs) {
|
||||
let calledEmpty = false;
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/search/query", (request) => {
|
||||
if (!request.queryParams["search_context[type]"]) {
|
||||
calledEmpty = true;
|
||||
if (request.queryParams.type_filter === DEFAULT_TYPE_FILTER) {
|
||||
// posts/topics are not present in the payload by default
|
||||
return helper.response({
|
||||
users: searchFixtures["search/query"]["users"],
|
||||
categories: searchFixtures["search/query"]["categories"],
|
||||
tags: searchFixtures["search/query"]["tags"],
|
||||
groups: searchFixtures["search/query"]["groups"],
|
||||
grouped_search_result:
|
||||
searchFixtures["search/query"]["grouped_search_result"],
|
||||
});
|
||||
}
|
||||
|
||||
return helper.response(searchFixtures["search/query"]);
|
||||
});
|
||||
});
|
||||
|
@ -28,24 +34,61 @@ acceptance("Search - Anonymous", function (needs) {
|
|||
|
||||
await click("#search-button");
|
||||
|
||||
assert.ok(exists("#search-term"), "it shows the search bar");
|
||||
assert.ok(!exists(".search-menu .results ul li"), "no results by default");
|
||||
assert.ok(exists("#search-term"), "it shows the search input");
|
||||
assert.ok(
|
||||
exists(".show-advanced-search"),
|
||||
"it shows full page search button"
|
||||
);
|
||||
assert.ok(
|
||||
exists(".search-menu .results ul li.search-random-quick-tip"),
|
||||
"shows random quick tip by default"
|
||||
);
|
||||
|
||||
await fillIn("#search-term", "dev");
|
||||
await triggerKeyEvent("#search-term", "keyup", 16);
|
||||
assert.ok(exists(".search-menu .results ul li"), "it shows results");
|
||||
|
||||
assert.ok(
|
||||
!exists(".search-menu .results ul li.search-random-quick-tip"),
|
||||
"quick tip no longer shown"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
query(
|
||||
".search-menu .results ul.search-menu-initial-options li:first-child"
|
||||
).innerText.trim(),
|
||||
`dev ${I18n.t("search.in_topics_posts")}`,
|
||||
"shows topic search as first dropdown item"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".search-menu .search-result-category ul li"),
|
||||
"shows matching category results"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".search-menu .search-result-user ul li"),
|
||||
"shows matching user results"
|
||||
);
|
||||
|
||||
await triggerKeyEvent(".search-menu", "keydown", 40);
|
||||
await click(document.activeElement);
|
||||
|
||||
assert.ok(
|
||||
exists(".search-menu .search-result-topic ul li"),
|
||||
"shows topic results"
|
||||
);
|
||||
assert.ok(
|
||||
exists(".search-menu .results ul li .topic-title[data-topic-id]"),
|
||||
"topic has data-topic-id"
|
||||
);
|
||||
|
||||
await click(".show-help");
|
||||
await click(".show-advanced-search");
|
||||
|
||||
assert.equal(
|
||||
queryAll(".full-page-search").val(),
|
||||
query(".full-page-search").value,
|
||||
"dev",
|
||||
"it shows the search term"
|
||||
"it goes to full search page and preserves the search term"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
exists(".search-advanced-options"),
|
||||
"advanced search is expanded"
|
||||
|
@ -68,85 +111,75 @@ acceptance("Search - Anonymous", function (needs) {
|
|||
assert.ok(!exists(".search-menu"));
|
||||
});
|
||||
|
||||
test("search for a tag", async function (assert) {
|
||||
await visit("/");
|
||||
test("search scope", async function (assert) {
|
||||
const firstResult =
|
||||
".search-menu .results .search-menu-assistant-item:first-child";
|
||||
|
||||
await click("#search-button");
|
||||
|
||||
await fillIn("#search-term", "evil");
|
||||
await triggerKeyEvent("#search-term", "keyup", 16);
|
||||
assert.ok(exists(".search-menu .results ul li"), "it shows results");
|
||||
});
|
||||
|
||||
test("search scope checkbox", async function (assert) {
|
||||
await visit("/tag/important");
|
||||
await click("#search-button");
|
||||
assert.ok(
|
||||
exists(".search-context input:checked"),
|
||||
"scope to tag checkbox is checked"
|
||||
await fillIn("#search-term", "smth");
|
||||
|
||||
assert.equal(
|
||||
query(firstResult).textContent.trim(),
|
||||
`smth ${I18n.t("search.in")} test`,
|
||||
"tag-scoped search is first available option"
|
||||
);
|
||||
await click("#search-button");
|
||||
|
||||
await visit("/c/bug");
|
||||
await click("#search-button");
|
||||
assert.ok(
|
||||
exists(".search-context input:checked"),
|
||||
"scope to category checkbox is checked"
|
||||
|
||||
assert.equal(
|
||||
query(firstResult).textContent.trim(),
|
||||
`smth ${I18n.t("search.in")} bug`,
|
||||
"category-scoped search is first available option"
|
||||
);
|
||||
await click("#search-button");
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("#search-button");
|
||||
assert.not(
|
||||
exists(".search-context input:checked"),
|
||||
"scope to topic checkbox is not checked"
|
||||
|
||||
assert.equal(
|
||||
query(firstResult).textContent.trim(),
|
||||
`smth ${I18n.t("search.in_this_topic")}`,
|
||||
"topic-scoped search is first available option"
|
||||
);
|
||||
await click("#search-button");
|
||||
|
||||
await visit("/u/eviltrout");
|
||||
await click("#search-button");
|
||||
assert.ok(
|
||||
exists(".search-context input:checked"),
|
||||
"scope to user checkbox is checked"
|
||||
|
||||
assert.equal(
|
||||
query(firstResult).textContent.trim(),
|
||||
`smth ${I18n.t("search.in_posts_by", {
|
||||
username: "eviltrout",
|
||||
})}`,
|
||||
"user-scoped search is first available option"
|
||||
);
|
||||
});
|
||||
|
||||
test("Search with context", async function (assert) {
|
||||
test("search scope for topics", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280/1");
|
||||
|
||||
await click("#search-button");
|
||||
await fillIn("#search-term", "a proper");
|
||||
await click(".search-context input[type='checkbox']");
|
||||
await triggerKeyEvent("#search-term", "keyup", 16);
|
||||
await focus("input#search-term");
|
||||
await triggerKeyEvent(".search-menu", "keydown", 40);
|
||||
|
||||
assert.ok(exists(".search-menu .results ul li"), "it shows results");
|
||||
|
||||
const highlighted = [];
|
||||
|
||||
queryAll("#post_7 span.highlighted").map((_, span) => {
|
||||
highlighted.push(span.innerText);
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
highlighted,
|
||||
["a proper"],
|
||||
"it should highlight the post with the search terms correctly"
|
||||
await click(document.activeElement);
|
||||
assert.ok(
|
||||
exists(".search-menu .search-result-post ul li"),
|
||||
"clicking first option formats results as posts"
|
||||
);
|
||||
|
||||
calledEmpty = false;
|
||||
await visit("/");
|
||||
await click("#search-button");
|
||||
assert.equal(
|
||||
query("#post_7 span.highlighted").textContent.trim(),
|
||||
"a proper",
|
||||
"highlights the post correctly"
|
||||
);
|
||||
|
||||
assert.ok(!exists(".search-context input[type='checkbox']"));
|
||||
assert.ok(calledEmpty, "it triggers a new search");
|
||||
|
||||
await visit("/t/internationalization-localization/280/1");
|
||||
await click("#search-button");
|
||||
|
||||
assert.ok(!$(".search-context input[type=checkbox]").is(":checked"));
|
||||
await click(".clear-search");
|
||||
assert.equal(query("#search-term").value, "", "clear button works");
|
||||
});
|
||||
|
||||
test("Right filters are shown to anonymous users", async function (assert) {
|
||||
test("Right filters are shown in full page search", async function (assert) {
|
||||
const inSelector = selectKit(".select-kit#in");
|
||||
|
||||
await visit("/search?expanded=true");
|
||||
|
@ -205,7 +238,7 @@ acceptance("Search - Authenticated", function (needs) {
|
|||
});
|
||||
});
|
||||
|
||||
test("Right filters are shown to logged-in users", async function (assert) {
|
||||
test("Right filters are shown in full page search", async function (assert) {
|
||||
const inSelector = selectKit(".select-kit#in");
|
||||
|
||||
await visit("/search?expanded=true");
|
||||
|
@ -230,16 +263,96 @@ acceptance("Search - Authenticated", function (needs) {
|
|||
|
||||
test("Works with empty result sets", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(".search-dropdown");
|
||||
await click(".search-context input[type=checkbox]");
|
||||
await click("#search-button");
|
||||
await fillIn("#search-term", "plans");
|
||||
await triggerKeyEvent("#search-term", "keyup", 32);
|
||||
assert.notEqual(count(".item"), 0);
|
||||
await focus("input#search-term");
|
||||
await triggerKeyEvent(".search-menu", "keydown", 40);
|
||||
await click(document.activeElement);
|
||||
|
||||
assert.notEqual(count(".search-menu .results .item"), 0);
|
||||
|
||||
await fillIn("#search-term", "plans empty");
|
||||
await triggerKeyEvent("#search-term", "keyup", 32);
|
||||
assert.equal(count(".item"), 0);
|
||||
assert.equal(count(".no-results"), 1);
|
||||
await triggerKeyEvent("#search-term", "keydown", 13);
|
||||
|
||||
assert.equal(count(".search-menu .results .item"), 0);
|
||||
assert.equal(count(".search-menu .results .no-results"), 1);
|
||||
});
|
||||
|
||||
test("search dropdown keyboard navigation", async function (assert) {
|
||||
const keyEnter = 13;
|
||||
const keyArrowDown = 40;
|
||||
const keyArrowUp = 38;
|
||||
const keyEsc = 27;
|
||||
const keyA = 65;
|
||||
const container = ".search-menu .results";
|
||||
|
||||
await visit("/");
|
||||
await click("#search-button");
|
||||
await fillIn("#search-term", "dev");
|
||||
|
||||
assert.ok(exists(query(`${container} ul li`)), "has a list of items");
|
||||
|
||||
await triggerKeyEvent("#search-term", "keydown", keyEnter);
|
||||
assert.ok(
|
||||
exists(query(`${container} .search-result-topic`)),
|
||||
"has topic results"
|
||||
);
|
||||
|
||||
await triggerKeyEvent("#search-term", "keydown", keyArrowDown);
|
||||
|
||||
assert.equal(
|
||||
document.activeElement.getAttribute("href"),
|
||||
query(`${container} li:first-child a`).getAttribute("href"),
|
||||
"arrow down selects first element"
|
||||
);
|
||||
|
||||
await triggerKeyEvent("#search-term", "keydown", keyArrowDown);
|
||||
|
||||
assert.equal(
|
||||
document.activeElement.getAttribute("href"),
|
||||
query(`${container} li:nth-child(2) a`).getAttribute("href"),
|
||||
"arrow down selects next element"
|
||||
);
|
||||
|
||||
await triggerKeyEvent("#search-term", "keydown", keyArrowDown);
|
||||
await triggerKeyEvent("#search-term", "keydown", keyArrowDown);
|
||||
await triggerKeyEvent("#search-term", "keydown", keyArrowDown);
|
||||
await triggerKeyEvent("#search-term", "keydown", keyArrowDown);
|
||||
|
||||
assert.equal(
|
||||
document.activeElement.getAttribute("href"),
|
||||
"/search?q=dev",
|
||||
"arrow down sets focus to more results link"
|
||||
);
|
||||
|
||||
await triggerKeyEvent(".search-menu", "keydown", keyEsc);
|
||||
assert.ok(!exists(".search-menu:visible"), "Esc removes search dropdown");
|
||||
|
||||
await click("#search-button");
|
||||
await triggerKeyEvent(".search-menu", "keydown", keyArrowDown);
|
||||
await triggerKeyEvent(".search-menu", "keydown", keyArrowUp);
|
||||
|
||||
assert.equal(
|
||||
document.activeElement.tagName.toLowerCase(),
|
||||
"input",
|
||||
"arrow up sets focus to search term input"
|
||||
);
|
||||
|
||||
await triggerKeyEvent(".search-menu", "keydown", keyEsc);
|
||||
await click("#create-topic");
|
||||
await click("#search-button");
|
||||
await triggerKeyEvent(".search-menu", "keydown", keyArrowDown);
|
||||
|
||||
const firstLink = query(`${container} li:nth-child(1) a`).getAttribute(
|
||||
"href"
|
||||
);
|
||||
await triggerKeyEvent(".search-menu", "keydown", keyA);
|
||||
|
||||
assert.equal(
|
||||
query("#reply-control textarea").value,
|
||||
firstLink,
|
||||
"hitting A when focused on a search result copies link to composer"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -249,19 +362,17 @@ acceptance("Search - with tagging enabled", function (needs) {
|
|||
|
||||
test("displays tags", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
await click("#search-button");
|
||||
|
||||
await fillIn("#search-term", "dev");
|
||||
await triggerKeyEvent("#search-term", "keyup", 16);
|
||||
await triggerKeyEvent("#search-term", "keydown", 13);
|
||||
|
||||
const tags = queryAll(
|
||||
".search-menu .results ul li:nth-of-type(1) .discourse-tags"
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
assert.equal(tags, "dev slow");
|
||||
assert.equal(
|
||||
query(
|
||||
".search-menu .results ul li:nth-of-type(1) .discourse-tags"
|
||||
).textContent.trim(),
|
||||
"dev slow",
|
||||
"tags displayed in search results"
|
||||
);
|
||||
});
|
||||
|
||||
test("displays tag shortcuts", async function (assert) {
|
||||
|
@ -276,7 +387,7 @@ acceptance("Search - with tagging enabled", function (needs) {
|
|||
".search-menu .results ul.search-menu-assistant .search-link";
|
||||
assert.ok(exists(query(firstItem)));
|
||||
|
||||
const firstTag = query(`${firstItem} .search-item-tag`).innerText.trim();
|
||||
const firstTag = query(`${firstItem} .search-item-tag`).textContent.trim();
|
||||
assert.equal(firstTag, "monkey");
|
||||
});
|
||||
});
|
||||
|
@ -325,10 +436,10 @@ acceptance("Search - assistant", function (needs) {
|
|||
|
||||
const firstResultSlug = query(
|
||||
`${firstCategory} .category-name`
|
||||
).innerText.trim();
|
||||
).textContent.trim();
|
||||
|
||||
await click(firstCategory);
|
||||
assert.equal(query("#search-term").value, `#${firstResultSlug} `);
|
||||
assert.equal(query("#search-term").value, `#${firstResultSlug}`);
|
||||
|
||||
await fillIn("#search-term", "sam #");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
|
@ -338,11 +449,11 @@ acceptance("Search - assistant", function (needs) {
|
|||
query(
|
||||
".search-menu .results ul.search-menu-assistant .search-item-prefix"
|
||||
).innerText,
|
||||
"sam"
|
||||
"sam "
|
||||
);
|
||||
|
||||
await click(firstCategory);
|
||||
assert.equal(query("#search-term").value, `sam #${firstResultSlug} `);
|
||||
assert.equal(query("#search-term").value, `sam #${firstResultSlug}`);
|
||||
});
|
||||
|
||||
test("shows in: shortcuts", async function (assert) {
|
||||
|
@ -379,6 +490,6 @@ acceptance("Search - assistant", function (needs) {
|
|||
assert.equal(firstUsername, "TeaMoe");
|
||||
|
||||
await click(query(firstUser));
|
||||
assert.equal(query("#search-term").value, `@${firstUsername} `);
|
||||
assert.equal(query("#search-term").value, `@${firstUsername}`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
@mixin user-item-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: $line-height-medium;
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
}
|
||||
|
||||
@mixin separator {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
margin-top: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
$search-pad-vertical: 0.25em;
|
||||
$search-pad-horizontal: 0.5em;
|
||||
|
||||
.search-menu {
|
||||
--search-padding: 0.5em;
|
||||
.menu-panel .panel-body-contents {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
position: relative;
|
||||
padding: var(--search-padding);
|
||||
}
|
||||
|
||||
.search-context {
|
||||
label {
|
||||
padding: 0.25em var(--search-padding);
|
||||
}
|
||||
.show-help {
|
||||
margin-left: auto;
|
||||
line-height: var(--line-height-medium);
|
||||
}
|
||||
padding: $search-pad-vertical 0.1em;
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
@ -27,28 +32,15 @@
|
|||
}
|
||||
|
||||
input[type="text"] {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.search-context {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-context + .results {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
padding-top: $search-pad-vertical;
|
||||
padding-bottom: $search-pad-vertical;
|
||||
|
||||
.list {
|
||||
min-width: 100px;
|
||||
|
@ -79,11 +71,16 @@
|
|||
.second-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
|
||||
.discourse-tags {
|
||||
.discourse-tag {
|
||||
margin-right: 0.25em;
|
||||
.badge-wrapper {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.discourse-tags .discourse-tag {
|
||||
margin-right: 0.25em;
|
||||
|
||||
.d-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,215 +92,186 @@
|
|||
}
|
||||
}
|
||||
|
||||
.main-results {
|
||||
.search-result-category {
|
||||
.widget-link {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-group .group-result,
|
||||
.search-result-user .user-result {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
.topic-statuses {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
align-items: center;
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
|
||||
.main-results + .secondary-results {
|
||||
border-left: 1px solid var(--primary-low);
|
||||
margin-left: 1em;
|
||||
padding-left: 1em;
|
||||
max-width: 33%;
|
||||
}
|
||||
|
||||
.secondary-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.separator {
|
||||
margin: 1em 0.25em;
|
||||
height: 1px;
|
||||
background: var(--primary-low);
|
||||
.search-result-group .group-result {
|
||||
.d-icon,
|
||||
.avatar-flair {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.search-result-tag {
|
||||
.discourse-tag {
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-category {
|
||||
.widget-link {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-group {
|
||||
.search-link {
|
||||
color: var(--primary-high);
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.group-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.d-icon,
|
||||
.avatar-flair {
|
||||
min-width: 25px;
|
||||
margin-right: 0.5em;
|
||||
|
||||
.d-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-flair-image {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.group-names {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
line-height: $line-height-medium;
|
||||
|
||||
&:hover {
|
||||
.name,
|
||||
.slug {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
}
|
||||
|
||||
.name,
|
||||
.slug {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.slug {
|
||||
font-size: $font-down-1;
|
||||
color: var(--primary-high);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-category,
|
||||
.search-result-user,
|
||||
.search-result-group,
|
||||
.search-result-tag {
|
||||
.list {
|
||||
display: block;
|
||||
|
||||
.item {
|
||||
.widget-link.search-link {
|
||||
flex: 1;
|
||||
font-size: $font-0;
|
||||
padding: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-user {
|
||||
.user-result {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
margin-right: 0.5em;
|
||||
display: block;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.user-titles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
line-height: $line-height-medium;
|
||||
|
||||
.username,
|
||||
.name {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
font-size: $font-down-1;
|
||||
}
|
||||
|
||||
.custom-field {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
font-size: $font-down-2;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
font-size: $font-0;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-menu-assistant {
|
||||
min-width: 100%;
|
||||
margin-top: -1em;
|
||||
|
||||
.search-menu-assistant-item {
|
||||
> span {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.search-item-user .avatar,
|
||||
.search-item-prefix {
|
||||
.avatar-flair {
|
||||
margin-right: 0.5em;
|
||||
border-radius: 50%;
|
||||
&.avatar-flair-image {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.d-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-item-tag {
|
||||
color: var(--primary-high);
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
.group-names {
|
||||
@include user-item-flex;
|
||||
.name,
|
||||
.slug {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.d-icon-tag {
|
||||
// match category badge styling
|
||||
// tag/category suggestions can be displayed simultaneously
|
||||
font-size: var(--font-down-2);
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-user .user-result {
|
||||
.user-titles {
|
||||
@include user-item-flex;
|
||||
|
||||
.username,
|
||||
.name {
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.username,
|
||||
.name,
|
||||
.custom-field {
|
||||
color: var(--primary-high-or-secondary-low);
|
||||
}
|
||||
|
||||
.custom-field {
|
||||
font-size: var(--font-down-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-category,
|
||||
.search-result-tag {
|
||||
+ .search-result-user,
|
||||
+ .search-result-group {
|
||||
@include separator;
|
||||
}
|
||||
}
|
||||
|
||||
.search-result-user .user-result img.avatar,
|
||||
.search-item-user img.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.label-suffix {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
|
||||
.badge-wrapper {
|
||||
font-size: var(--font-0);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.search-menu-initial-options {
|
||||
+ .search-result-tag,
|
||||
+ .search-result-category,
|
||||
+ .search-result-user,
|
||||
+ .search-result-group {
|
||||
@include separator;
|
||||
}
|
||||
}
|
||||
|
||||
.search-menu-initial-options,
|
||||
.search-result-tag,
|
||||
.search-menu-assistant {
|
||||
.search-link {
|
||||
.d-icon {
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.d-icon-tag {
|
||||
font-size: var(--font-down-2);
|
||||
}
|
||||
|
||||
.d-icon-search {
|
||||
font-size: var(--font-down-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-random-quick-tip {
|
||||
padding: $search-pad-vertical $search-pad-horizontal;
|
||||
padding-bottom: 0;
|
||||
font-size: var(--font-down-2);
|
||||
color: var(--primary-medium);
|
||||
.tip-label {
|
||||
background-color: rgba(var(--tertiary-rgb), 0.1);
|
||||
margin-right: 4px;
|
||||
padding: 2px 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searching {
|
||||
position: absolute;
|
||||
top: 1.1em;
|
||||
right: 1em;
|
||||
top: $search-pad-vertical + 0.4em;
|
||||
right: $search-pad-horizontal;
|
||||
min-height: 20px;
|
||||
|
||||
.spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-width: 2px;
|
||||
margin: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
a.show-advanced-search,
|
||||
a.clear-search {
|
||||
padding: 0px 3px;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
.d-icon {
|
||||
color: var(--primary-low-mid);
|
||||
}
|
||||
&:hover .d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
}
|
||||
|
||||
a.clear-search {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: var(--search-padding);
|
||||
padding: $search-pad-vertical $search-pad-horizontal;
|
||||
}
|
||||
|
||||
.search-link {
|
||||
padding: var(--search-padding);
|
||||
display: block;
|
||||
padding: $search-pad-vertical $search-pad-horizontal;
|
||||
|
||||
.badge-category-parent {
|
||||
line-height: $line-height-small;
|
||||
// This is purposefully redundant
|
||||
// the search widget can be used outside of the header
|
||||
// and the focus/hover styles from the header in those cases wouldn't follow
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: var(--highlight-medium);
|
||||
}
|
||||
|
||||
.topic {
|
||||
|
@ -319,4 +287,10 @@
|
|||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
||||
.search-result-topic,
|
||||
.search-result-post {
|
||||
.search-link {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2341,11 +2341,13 @@ en:
|
|||
select_all: "Select All"
|
||||
clear_all: "Clear All"
|
||||
too_short: "Your search term is too short."
|
||||
open_advanced: "Open advanced search"
|
||||
clear_search: "Clear search"
|
||||
sort_or_bulk_actions: "Sort or bulk select results"
|
||||
result_count:
|
||||
one: "<span>%{count} result for</span><span class='term'>%{term}</span>"
|
||||
other: "<span>%{count}%{plus} results for</span><span class='term'>%{term}</span>"
|
||||
title: "search topics, posts, users, or categories"
|
||||
title: "Search"
|
||||
full_page_title: "Search"
|
||||
no_results: "No results found."
|
||||
no_more_results: "No more results found."
|
||||
|
@ -2361,6 +2363,10 @@ en:
|
|||
search_term_label: "enter search keyword"
|
||||
categories: "Categories"
|
||||
tags: "Tags"
|
||||
in: "in"
|
||||
in_this_topic: "in this topic"
|
||||
in_topics_posts: "in all topics and posts"
|
||||
in_posts_by: "in posts by %{username}"
|
||||
|
||||
type:
|
||||
default: "Topics/posts"
|
||||
|
@ -2375,6 +2381,14 @@ en:
|
|||
topic: "Search this topic"
|
||||
private_messages: "Search messages"
|
||||
|
||||
tips:
|
||||
category_tag: "filters by category or tag"
|
||||
author: "filters by post author"
|
||||
in: "filters by metadata (e.g. in:title, in:personal, in:pinned)"
|
||||
status: "filters by topic status"
|
||||
full_search: "launches full page search"
|
||||
full_search_key: "%{modifier} + Enter"
|
||||
|
||||
advanced:
|
||||
title: Advanced filters
|
||||
posted_by:
|
||||
|
|
|
@ -33,7 +33,7 @@ class Search
|
|||
end
|
||||
|
||||
def self.facets
|
||||
%w(topic category user private_messages tags all_topics)
|
||||
%w(topic category user private_messages tags all_topics exclude_topics)
|
||||
end
|
||||
|
||||
def self.ts_config(locale = SiteSetting.default_locale)
|
||||
|
@ -230,7 +230,7 @@ class Search
|
|||
end
|
||||
|
||||
def limit
|
||||
if @opts[:type_filter].present?
|
||||
if @opts[:type_filter].present? && @opts[:type_filter] != "exclude_topics"
|
||||
Search.per_filter + 1
|
||||
else
|
||||
Search.per_facet + 1
|
||||
|
@ -862,13 +862,13 @@ class Search
|
|||
groups = Group
|
||||
.visible_groups(@guardian.user, "name ASC", include_everyone: false)
|
||||
.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{@term}%")
|
||||
.limit(limit)
|
||||
|
||||
groups.each { |group| @results.add(group) }
|
||||
end
|
||||
|
||||
def tags_search
|
||||
return unless SiteSetting.tagging_enabled
|
||||
|
||||
tags = Tag.includes(:tag_search_data)
|
||||
.where("tag_search_data.search_data @@ #{ts_query}")
|
||||
.references(:tag_search_data)
|
||||
|
@ -882,6 +882,15 @@ class Search
|
|||
end
|
||||
end
|
||||
|
||||
def exclude_topics_search
|
||||
if @term.present?
|
||||
user_search
|
||||
category_search
|
||||
tags_search
|
||||
groups_search
|
||||
end
|
||||
end
|
||||
|
||||
PHRASE_MATCH_REGEXP_PATTERN = '"([^"]+)"'
|
||||
|
||||
def posts_query(limit, type_filter: nil, aggregate_search: false)
|
||||
|
|
|
@ -180,6 +180,7 @@ module SvgSprite
|
|||
"sign-in-alt",
|
||||
"sign-out-alt",
|
||||
"signal",
|
||||
"sliders-h",
|
||||
"star",
|
||||
"step-backward",
|
||||
"step-forward",
|
||||
|
|
|
@ -1921,4 +1921,33 @@ describe Search do
|
|||
expect(Search.new("advanced order:chars").execute.posts).to eq([post0, post1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'exclude_topics filter' do
|
||||
before { SiteSetting.tagging_enabled = true }
|
||||
let!(:user) { Fabricate(:user) }
|
||||
fab!(:group) { Fabricate(:group, name: 'bruce-world-fans') }
|
||||
fab!(:topic) { Fabricate(:topic, title: 'Bruce topic not a result') }
|
||||
|
||||
it 'works' do
|
||||
category = Fabricate(:category_with_definition, name: 'bruceland', user: user)
|
||||
tag = Fabricate(:tag, name: 'brucealicious')
|
||||
|
||||
result = Search.execute('bruce', type_filter: 'exclude_topics')
|
||||
|
||||
expect(result.users.map(&:id)).to contain_exactly(user.id)
|
||||
|
||||
expect(result.categories.map(&:id)).to contain_exactly(category.id)
|
||||
|
||||
expect(result.groups.map(&:id)).to contain_exactly(group.id)
|
||||
|
||||
expect(result.tags.map(&:id)).to contain_exactly(tag.id)
|
||||
|
||||
expect(result.posts.length).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not fail when parsed term is empty' do
|
||||
result = Search.execute('#cat ', type_filter: 'exclude_topics')
|
||||
expect(result.categories.length).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue
Block a user