UX: Better topic search experience (#14625)

This commit is contained in:
Penar Musaraj 2021-10-18 13:17:27 -04:00 committed by GitHub
parent 7a4a1744a2
commit 073e5ccd83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 163 additions and 84 deletions

View File

@ -117,7 +117,7 @@ function translateGroupedSearchResults(results, opts) {
const name = pair[1];
if (results[name].length > 0) {
const componentName =
opts.showPosts && type === "topic" ? "post" : type;
opts.searchContext && type === "topic" ? "post" : type;
const result = {
results: results[name],
@ -154,8 +154,12 @@ export function searchForTerm(term, opts) {
data.restrict_to_archetype = opts.restrictToArchetype;
}
if (term.includes("topic:")) {
opts.showPosts = true;
if (opts.searchContext) {
data.search_context = {
type: opts.searchContext.type,
id: opts.searchContext.id,
name: opts.searchContext.name,
};
}
let ajaxPromise = ajax("/search/query", { data });

View File

@ -4,10 +4,10 @@ import { addExtraUserClasses } from "discourse/helpers/user-avatar";
import { ajax } from "discourse/lib/ajax";
import { avatarImg } from "discourse/widgets/post";
import { createWidget } from "discourse/widgets/widget";
import { get } from "@ember/object";
import getURL from "discourse-common/lib/get-url";
import { h } from "virtual-dom";
import { iconNode } from "discourse-common/lib/icon-library";
import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
import { schedule } from "@ember/runloop";
import { scrollTop } from "discourse/mixins/scroll-top";
import { wantsNewWindow } from "discourse/lib/intercept-click";
@ -321,8 +321,6 @@ createWidget("header-cloak", {
scheduleRerender() {},
});
const forceContextEnabled = ["category", "user", "private_messages", "tag"];
let additionalPanels = [];
export function attachAdditionalPanel(name, toggle, transformAttrs) {
additionalPanels.push({ name, toggle, transformAttrs });
@ -339,6 +337,7 @@ export default createWidget("header", {
hamburgerVisible: false,
userVisible: false,
ringBackdrop: true,
inTopicContext: false,
};
if (this.site.mobileView) {
@ -366,21 +365,10 @@ export default createWidget("header", {
const panels = [this.attach("header-buttons", attrs), headerIcons];
if (state.searchVisible) {
const contextType = this.searchContextType();
if (state.searchContextType !== contextType) {
state.contextEnabled = undefined;
state.searchContextType = contextType;
}
if (state.contextEnabled === undefined) {
if (forceContextEnabled.includes(contextType)) {
state.contextEnabled = true;
}
}
panels.push(
this.attach("search-menu", { contextEnabled: state.contextEnabled })
this.attach("search-menu", {
inTopicContext: state.inTopicContext,
})
);
} else if (state.hamburgerVisible) {
panels.push(this.attach("hamburger-menu"));
@ -477,11 +465,9 @@ export default createWidget("header", {
this.updateHighlight();
if (this.state.searchVisible) {
schedule("afterRender", () => {
const searchInput = document.querySelector("#search-term");
searchInput.focus();
searchInput.select();
});
this.focusSearchInput();
} else {
this.state.inTopicContext = false;
}
},
@ -537,8 +523,7 @@ export default createWidget("header", {
togglePageSearch() {
const { state } = this;
state.contextEnabled = false;
state.inTopicContext = false;
const currentPath = this.router.get("_router.currentPath");
const blocklist = [/^discovery\.categories/];
@ -565,7 +550,7 @@ export default createWidget("header", {
}
if (showSearch) {
state.contextEnabled = true;
state.inTopicContext = true;
this.toggleSearchMenu();
return false;
}
@ -573,13 +558,6 @@ export default createWidget("header", {
return true;
},
searchMenuContextChanged(value) {
this.state.contextType = this.register
.lookup("search-service:main")
.get("contextType");
this.state.contextEnabled = value;
},
domClean() {
const { state } = this;
@ -637,10 +615,6 @@ export default createWidget("header", {
this.toggleHamburger();
break;
case "page-search":
let contextType = this.searchContextType();
if (contextType === "topic") {
this.state.searchContextType = contextType;
}
if (!this.togglePageSearch()) {
msg.event.preventDefault();
msg.event.stopPropagation();
@ -649,13 +623,21 @@ export default createWidget("header", {
}
},
searchContextType() {
const service = this.register.lookup("search-service:main");
if (service) {
const ctx = service.get("searchContext");
if (ctx) {
return get(ctx, "type");
}
focusSearchInput() {
if (this.state.searchVisible) {
schedule("afterRender", () => {
putCursorAtEnd(document.querySelector("#search-term"));
});
}
},
setTopicContext() {
this.state.inTopicContext = true;
this.focusSearchInput();
},
clearContext() {
this.state.inTopicContext = false;
this.focusSearchInput();
},
});

View File

@ -11,10 +11,7 @@ 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";
import { MODIFIER_REGEXP } from "discourse/widgets/search-menu";
const suggestionShortcuts = [
"in:title",
@ -79,9 +76,7 @@ resetQuickSearchRandomTips();
class Highlighted extends RawHtml {
constructor(html, term) {
super({ html: `<span>${html}</span>` });
if (term) {
this.term = term.replace(TOPIC_REPLACE_REGEXP, "");
}
this.term = term;
}
decorate($html) {
@ -296,9 +291,7 @@ createWidget("search-menu-results", {
}
if (!term) {
return this.attach("search-menu-initial-options", {
term,
});
return this.attach("search-menu-initial-options", { term });
}
const resultTypes = results.resultTypes || [];
@ -483,15 +476,15 @@ createWidget("search-menu-initial-options", {
if (attrs.term || ctx) {
if (ctx) {
const term = attrs.term ? `${attrs.term} ` : "";
const term = attrs.term || "";
switch (ctx.type) {
case "topic":
content.push(
this.attach("search-menu-assistant-item", {
slug: `${term}topic:${ctx.id}`,
slug: term,
setTopicContext: true,
label: [
h("span", term),
h("span", `${term} `),
h("span.label-suffix", I18n.t("search.in_this_topic")),
],
})
@ -501,7 +494,7 @@ createWidget("search-menu-initial-options", {
case "private_messages":
content.push(
this.attach("search-menu-assistant-item", {
slug: `${term}in:personal`,
slug: `${term} in:personal`,
})
);
break;
@ -513,7 +506,7 @@ createWidget("search-menu-initial-options", {
content.push(
this.attach("search-menu-assistant", {
term: `${term}${fullSlug}`,
term: `${term} ${fullSlug}`,
suggestionKeyword: "#",
results: [{ model: ctx.category }],
withInLabel: true,
@ -524,7 +517,7 @@ createWidget("search-menu-initial-options", {
case "tag":
content.push(
this.attach("search-menu-assistant", {
term: `${term}#${ctx.name}`,
term: `${term} #${ctx.name}`,
suggestionKeyword: "#",
results: [{ name: ctx.name }],
withInLabel: true,
@ -534,9 +527,9 @@ createWidget("search-menu-initial-options", {
case "user":
content.push(
this.attach("search-menu-assistant-item", {
slug: `${term}@${ctx.user.username}`,
slug: `${term} @${ctx.user.username}`,
label: [
h("span", term),
h("span", `${term} `),
h(
"span.label-suffix",
I18n.t("search.in_posts_by", {
@ -636,6 +629,7 @@ createWidget("search-menu-assistant-item", {
this.sendWidgetAction("triggerAutocomplete", {
value: this.attrs.slug,
searchTopics: true,
setTopicContext: this.attrs.setTopicContext,
});
e.preventDefault();
return false;

View File

@ -15,7 +15,6 @@ 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 = /\btopic:\d+\s?/i;
export const MODIFIER_REGEXP = /.*(\#|\@|:).*$/gi;
export const DEFAULT_TYPE_FILTER = "exclude_topics";
@ -50,6 +49,8 @@ const SearchHelper = {
this.cancel();
const { term, typeFilter } = searchData;
const searchContext = widget.searchContext();
const fullSearchUrl = widget.fullSearchUrl();
const matchSuggestions = this.matchesSuggestions();
@ -127,13 +128,14 @@ const SearchHelper = {
this._activeSearch = searchForTerm(term, {
typeFilter,
fullSearchUrl,
searchContext,
});
this._activeSearch
.then((results) => {
// we ensure the current search term is the one used
// when starting the query
if (results && term === searchData.term) {
if (term.includes("topic:")) {
if (searchContext) {
widget.appEvents.trigger("post-stream:refresh", { force: true });
}
@ -183,6 +185,14 @@ export default createWidget("search-menu", {
tagName: "div.search-menu",
searchData,
buildKey: () => "search-menu",
defaultState(attrs) {
return {
inTopicContext: attrs.inTopicContext,
};
},
fullSearchUrl(opts) {
let url = "/search";
const params = [];
@ -209,7 +219,23 @@ export default createWidget("search-menu", {
},
panelContents() {
let searchInput = [this.attach("search-term", { value: searchData.term })];
let searchInput = [];
if (this.state.inTopicContext) {
searchInput.push(
this.attach("button", {
icon: "times",
label: "search.in_this_topic",
title: "search.in_this_topic_tooltip",
className: "btn btn-small search-context",
action: "clearTopicContext",
iconRight: true,
})
);
}
searchInput.push(this.attach("search-term", { value: searchData.term }));
if (searchData.loading) {
searchInput.push(h("div.searching", h("div.spinner")));
} else {
@ -238,6 +264,13 @@ export default createWidget("search-menu", {
const results = [h("div.search-input", searchInput)];
if (
this.state.inTopicContext &&
(!SearchHelper.includesTopics() || !searchData.term)
) {
return results;
}
if (!searchData.loading) {
results.push(
this.attach("search-menu-results", {
@ -270,7 +303,11 @@ export default createWidget("search-menu", {
return this._searchService;
},
html() {
html(attrs, state) {
if (attrs.inTopicContext === false) {
state.inTopicContext = false;
}
return this.attach("menu-panel", {
maxWidth: 500,
contents: () => this.panelContents(),
@ -281,6 +318,10 @@ export default createWidget("search-menu", {
this.sendWidgetAction("toggleSearchMenu");
},
clearTopicContext() {
this.sendWidgetAction("clearContext");
},
keyDown(e) {
if (e.which === 27 /* escape */) {
this.sendWidgetAction("toggleSearchMenu");
@ -377,16 +418,22 @@ export default createWidget("search-menu", {
this.triggerSearch();
}
}
if (e.target === searchInput && e.which === 8 /* backspace */) {
if (!searchInput.value) {
this.clearTopicContext();
}
}
},
triggerSearch() {
searchData.noResults = false;
if (searchData.term.includes("topic:")) {
const highlightTerm = searchData.term.replace(TOPIC_REPLACE_REGEXP, "");
this.searchService().set("highlightTerm", highlightTerm);
}
if (SearchHelper.includesTopics()) {
if (this.state.inTopicContext) {
this.searchService().set("highlightTerm", searchData.term);
}
searchData.loading = true;
SearchHelper.perform(this);
} else {
@ -407,6 +454,10 @@ export default createWidget("search-menu", {
},
triggerAutocomplete(opts = {}) {
if (opts.setTopicContext) {
this.sendWidgetAction("setTopicContext");
this.state.inTopicContext = true;
}
this.searchTermChanged(opts.value, { searchTopics: opts.searchTopics });
},
@ -420,4 +471,12 @@ export default createWidget("search-menu", {
DiscourseURL.routeTo(url);
}
},
searchContext() {
if (this.state.inTopicContext) {
return this.searchService().get("searchContext");
}
return false;
},
});

View File

@ -197,19 +197,38 @@ acceptance("Search - Anonymous", function (needs) {
"highlights the post correctly"
);
await fillIn("#search-term", "topic:280 interface");
assert.ok(
exists(".search-menu .search-context"),
"search context indicator is visible"
);
await click(".clear-search");
assert.equal(query("#search-term").value, "", "clear button works");
await click(".search-context");
assert.ok(
!exists(".search-menu .search-context"),
"search context indicator is no longer visible"
);
await fillIn("#search-term", "dev");
await focus("input#search-term");
await triggerKeyEvent(".search-menu", "keydown", 40);
await click(document.activeElement);
assert.equal(
query("#post_7 span.highlighted").textContent.trim(),
"interface",
"highlights the post when term is after modifier"
assert.ok(
exists(".search-menu .search-context"),
"search context indicator is visible"
);
await click(".clear-search");
assert.equal(query("#search-term").value, "", "clear button works");
await fillIn("#search-term", "");
await focus("input#search-term");
await triggerKeyEvent("input#search-term", "keydown", 8); // backspace
assert.ok(
!exists(".search-menu .search-context"),
"backspace resets search context"
);
});
test("Right filters are shown in full page search", async function (assert) {

View File

@ -21,7 +21,24 @@ $search-pad-horizontal: 0.5em;
.search-input {
position: relative;
padding: $search-pad-vertical 0.1em;
margin: 1px;
display: flex;
align-items: center;
border: 1px solid var(--primary-medium);
input#search-term {
border-width: 0;
margin-bottom: 0;
&:focus {
outline: none;
}
}
.btn {
margin-left: 1px;
}
&:focus-within {
@include default-focus;
}
}
.heading {
@ -32,8 +49,7 @@ $search-pad-horizontal: 0.5em;
}
input[type="text"] {
width: 100%;
margin-bottom: 0;
margin-right: 0px;
}
.results {
@ -242,7 +258,7 @@ $search-pad-horizontal: 0.5em;
.searching {
position: absolute;
top: $search-pad-vertical + 0.4em;
top: $search-pad-vertical + 0.2em;
right: $search-pad-horizontal;
min-height: 20px;

View File

@ -28,6 +28,10 @@
margin-right: 0.45em;
transition: color 0.25s;
}
.d-button-label + .d-icon {
margin-left: 0.45em;
margin-right: 0;
}
&.no-text {
.d-icon {
margin-right: 0;

View File

@ -2365,6 +2365,7 @@ en:
tags: "Tags"
in: "in"
in_this_topic: "in this topic"
in_this_topic_tooltip: "switch to searching all topics"
in_topics_posts: "in all topics and posts"
enter_hint: "or press Enter"
in_posts_by: "in posts by %{username}"