FEATURE: user/category/tag results in full page search (#14346)

See PR for details, this commit also changes the layout of the full page search.
This commit is contained in:
Penar Musaraj 2021-09-20 10:01:11 -04:00 committed by GitHub
parent a736ff5f69
commit dfeca42bf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 856 additions and 600 deletions

View File

@ -80,7 +80,9 @@ export function addAdvancedSearchOptions(options) {
}
export default Component.extend({
classNames: ["search-advanced-options"],
tagName: "details",
attributeBindings: ["expandFilters:open"],
classNames: ["advanced-filters"],
category: null,
init() {
@ -116,6 +118,7 @@ export default Component.extend({
: inOptionsForAll(),
statusOptions: statusOptions(),
postTimeOptions: postTimeOptions(),
showAllTagsCheckbox: false,
});
},
@ -313,10 +316,10 @@ export default Component.extend({
const userInput = match[0].replace(REGEXP_TAGS_REPLACE, "");
if (existingInput !== userInput) {
this.set(
"searchedTerms.tags",
userInput.length !== 0 ? userInput.split(joinChar) : null
);
const updatedTags = userInput?.split(joinChar);
this.set("searchedTerms.tags", updatedTags);
this.set("showAllTagsCheckbox", !!(updatedTags.length > 1));
}
} else if (!tags) {
this.set("searchedTerms.tags", null);
@ -496,6 +499,9 @@ export default Component.extend({
searchTerm += ` tags:${tags}`;
}
if (tagFilter.length > 1) {
this.set("showAllTagsCheckbox", true);
}
this._updateSearchTerm(searchTerm);
} else if (match.length !== 0) {
searchTerm = searchTerm.replace(match[0], "");

View File

@ -1,5 +1,7 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
tagName: "div",
classNames: ["fps-result"],
classNameBindings: ["bulkSelectEnabled"],
});

View File

@ -15,6 +15,9 @@ import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { scrollTop } from "discourse/mixins/scroll-top";
import { setTransient } from "discourse/lib/page-tracker";
import { Promise } from "rsvp";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import userSearch from "discourse/lib/user-search";
const SortOrders = [
{ name: I18n.t("search.relevance"), id: 0 },
@ -23,6 +26,19 @@ const SortOrders = [
{ name: I18n.t("search.most_viewed"), id: 3, term: "order:views" },
{ name: I18n.t("search.latest_topic"), id: 4, term: "order:latest_topic" },
];
export const SEARCH_TYPE_DEFAULT = "topics_posts";
export const SEARCH_TYPE_CATS_TAGS = "categories_tags";
export const SEARCH_TYPE_USERS = "users";
const SearchTypes = [
{ name: I18n.t("search.type.default"), id: SEARCH_TYPE_DEFAULT },
{
name: I18n.t("search.type.categories_and_tags"),
id: SEARCH_TYPE_CATS_TAGS,
},
{ name: I18n.t("search.type.users"), id: SEARCH_TYPE_USERS },
];
const PAGE_LIMIT = 10;
export default Controller.extend({
@ -31,11 +47,17 @@ export default Controller.extend({
bulkSelectEnabled: null,
loading: false,
queryParams: ["q", "expanded", "context_id", "context", "skip_context"],
q: null,
selected: [],
expanded: false,
queryParams: [
"q",
"expanded",
"context_id",
"context",
"skip_context",
"search_type",
],
q: undefined,
context_id: null,
search_type: SEARCH_TYPE_DEFAULT,
context: null,
searching: false,
sortOrder: 0,
@ -43,12 +65,24 @@ export default Controller.extend({
invalidSearch: false,
page: 1,
resultCount: null,
searchTypes: SearchTypes,
init() {
this._super(...arguments);
this.selected = [];
},
@discourseComputed("resultCount")
hasResults(resultCount) {
return (resultCount || 0) > 0;
},
@discourseComputed("expanded")
expandFilters(expanded) {
return expanded === "true";
},
@discourseComputed("q")
hasAutofocus(q) {
return isEmpty(q);
@ -138,6 +172,14 @@ export default Controller.extend({
}
},
@observes("search_type")
triggerSearchOnTypeChange() {
if (this.searchActive) {
this.set("page", 1);
this._search();
}
},
@observes("model")
modelChanged() {
if (this.searchTerm !== this.q) {
@ -182,9 +224,19 @@ export default Controller.extend({
return I18n.t("search.result_count", { count, plus, term });
},
@observes("model.posts.length")
@observes("model.[posts,categories,tags,users].length")
resultCountChanged() {
this.set("resultCount", this.get("model.posts.length"));
if (!this.model.posts) {
return 0;
}
this.set(
"resultCount",
this.model.posts.length +
this.model.categories.length +
this.model.tags.length +
this.model.users.length
);
},
@discourseComputed("hasResults")
@ -202,6 +254,18 @@ export default Controller.extend({
return page === PAGE_LIMIT;
},
@discourseComputed("search_type")
usingDefaultSearchType(searchType) {
return searchType === SEARCH_TYPE_DEFAULT;
},
@discourseComputed("bulkSelectEnabled")
searchInfoClassNames(bulkSelectEnabled) {
return bulkSelectEnabled
? "search-info bulk-select-visible"
: "search-info";
},
searchButtonDisabled: or("searching", "loading"),
_search() {
@ -244,33 +308,71 @@ export default Controller.extend({
const searchKey = getSearchKey(args);
ajax("/search", { data: args })
.then(async (results) => {
const model = (await translateResults(results)) || {};
switch (this.search_type) {
case SEARCH_TYPE_CATS_TAGS:
const categoryTagSearch = searchCategoryTag(
searchTerm,
this.siteSettings
);
Promise.resolve(categoryTagSearch)
.then(async (results) => {
const categories = results.filter((c) => Boolean(c.model));
const tags = results.filter((c) => !Boolean(c.model));
const model = (await translateResults({ categories, tags })) || {};
this.set("model", model);
})
.finally(() => {
this.setProperties({
searching: false,
loading: false,
});
});
break;
case SEARCH_TYPE_USERS:
userSearch({ term: searchTerm, limit: 20 })
.then(async (results) => {
const model = (await translateResults({ users: results })) || {};
this.set("model", model);
})
.finally(() => {
this.setProperties({
searching: false,
loading: false,
});
});
break;
default:
ajax("/search", { data: args })
.then(async (results) => {
const model = (await translateResults(results)) || {};
if (results.grouped_search_result) {
this.set("q", results.grouped_search_result.term);
}
if (results.grouped_search_result) {
this.set("q", results.grouped_search_result.term);
}
if (args.page > 1) {
if (model) {
this.model.posts.pushObjects(model.posts);
this.model.topics.pushObjects(model.topics);
this.model.set(
"grouped_search_result",
results.grouped_search_result
);
}
} else {
setTransient("lastSearch", { searchKey, model }, 5);
model.grouped_search_result = results.grouped_search_result;
this.set("model", model);
}
})
.finally(() => {
this.set("searching", false);
this.set("loading", false);
});
if (args.page > 1) {
if (model) {
this.model.posts.pushObjects(model.posts);
this.model.topics.pushObjects(model.topics);
this.model.set(
"grouped_search_result",
results.grouped_search_result
);
}
} else {
setTransient("lastSearch", { searchKey, model }, 5);
model.grouped_search_result = results.grouped_search_result;
this.set("model", model);
}
})
.finally(() => {
this.setProperties({
searching: false,
loading: false,
});
});
break;
}
},
actions: {
@ -309,16 +411,14 @@ export default Controller.extend({
this.selected.clear();
},
search() {
search(collapseFilters = false) {
if (collapseFilters) {
document
.querySelector("details.advanced-filters")
?.removeAttribute("open");
}
this.set("page", 1);
this._search();
if (this.site.mobileView) {
this.set("expanded", false);
}
},
toggleAdvancedSearch() {
this.toggleProperty("expanded");
},
loadMore() {

View File

@ -55,7 +55,7 @@ export function translateResults(results, opts) {
results.categories = results.categories
.map(function (category) {
return Category.list().findBy("id", category.id);
return Category.list().findBy("id", category.id || category.model.id);
})
.compact();

View File

@ -1,7 +1,5 @@
<div>
<form action="//google.com/search" id="google-search">
<input type="text" name="q" aria-label={{i18n "search.search_google"}} value={{searchTerm}}>
<input name="as_sitesearch" value={{siteUrl}} type="hidden">
<button class="btn btn-primary" type="submit">{{i18n "search.search_google_button"}}</button>
</form>
</div>
<form action="//google.com/search" id="google-search" class="inline-form">
<input type="text" name="q" aria-label={{i18n "search.search_google"}} value={{searchTerm}}>
<input name="as_sitesearch" value={{siteUrl}} type="hidden">
<button class="btn btn-primary" type="submit">{{i18n "search.search_google_button"}}</button>
</form>

View File

@ -1,170 +1,180 @@
{{plugin-outlet name="advanced-search-options-above" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}}
<summary>
{{i18n "search.advanced.title"}}
</summary>
<div class="search-advanced-filters">
<div class="search-advanced-options">
{{plugin-outlet name="advanced-search-options-above" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}}
<div class="container advanced-search-posted-by-group">
<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-chooser
id="search-posted-by"
value=searchedTerms.username
onChange=(action "onChangeSearchTermForUsername")
options=(hash
maximum=1
excludeCurrentUser=false
)
}}
</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">
{{search-advanced-category-chooser
id="search-in-category"
value=searchedTerms.category.id
onChange=(action "onChangeSearchTermForCategory")
}}
</div>
</div>
</div>
{{#if siteSettings.tagging_enabled}}
<div class="container advanced-search-tag-group">
<div class="control-group">
<label class="control-label" for="search-with-tags">{{i18n "search.advanced.with_tags.label"}}</label>
<div class="control-group advanced-search-category">
<label class="control-label" for="search-in-category">{{i18n "search.advanced.in_category.label"}}</label>
<div class="controls">
{{tag-chooser
id="search-with-tags"
tags=searchedTerms.tags
allowCreate=false
everyTag=true
unlimitedTagCount=true
onChange=(action "onChangeSearchTermForTags")
{{search-advanced-category-chooser
id="search-in-category"
value=searchedTerms.category.id
onChange=(action "onChangeSearchTermForCategory")
}}
<section class="field">
<label>
{{input
type="checkbox"
class="all-tags"
checked=searchedTerms.special.all_tags
click=(action "onChangeSearchTermForAllTags" value="target.checked")
}}
{{i18n "search.advanced.filters.all_tags"}}
</label>
</section>
</div>
</div>
</div>
{{/if}}
<div class="container advanced-search-topics-posts-group">
<div class="control-group pull-left">
<div class="controls">
<fieldset class="grouped-control">
<legend class="grouped-control-label" for="search-in-options">{{i18n "search.advanced.filters.label"}}</legend>
{{#if siteSettings.tagging_enabled}}
<div class="control-group advanced-search-tags">
<label class="control-label" for="search-with-tags">{{i18n "search.advanced.with_tags.label"}}</label>
<div class="controls">
{{tag-chooser
id="search-with-tags"
tags=searchedTerms.tags
allowCreate=false
everyTag=true
unlimitedTagCount=true
onChange=(action "onChangeSearchTermForTags")
}}
{{#if showAllTagsCheckbox}}
<section class="field">
<label>
{{input
type="checkbox"
class="all-tags"
checked=searchedTerms.special.all_tags
click=(action "onChangeSearchTermForAllTags" value="target.checked")
}}
{{i18n "search.advanced.filters.all_tags"}}
</label>
</section>
{{/if}}
</div>
</div>
{{/if}}
{{#if currentUser}}
<div class="grouped-control-field">
{{input
id="matching-title-only"
type="checkbox"
class="in-title"
checked=searchedTerms.special.in.title
click=(action "onChangeSearchTermForSpecialInTitle" value="target.checked")
}}
<label for="matching-title-only">
{{i18n "search.advanced.filters.title"}}
</label>
</div>
<div class="control-group advanced-search-topics-posts">
<div class="controls">
<fieldset class="grouped-control">
<legend class="grouped-control-label" for="search-in-options">{{i18n "search.advanced.filters.label"}}</legend>
<div class="grouped-control-field">
{{input
id="matching-liked"
type="checkbox"
class="in-likes"
checked=searchedTerms.special.in.likes
click=(action "onChangeSearchTermForSpecialInLikes" value="target.checked")
}}
<label for="matching-liked">{{i18n "search.advanced.filters.likes"}}</label>
</div>
{{#if currentUser}}
<div class="grouped-control-field">
{{input
id="matching-title-only"
type="checkbox"
class="in-title"
checked=searchedTerms.special.in.title
click=(action "onChangeSearchTermForSpecialInTitle" value="target.checked")
}}
<label for="matching-title-only">
{{i18n "search.advanced.filters.title"}}
</label>
</div>
<div class="grouped-control-field">
{{input
id="matching-in-messages"
type="checkbox"
class="in-private"
checked=searchedTerms.special.in.personal
click=(action "onChangeSearchTermForSpecialInPersonal" value="target.checked")
}}
<label for="matching-in-messages">{{i18n "search.advanced.filters.private"}}</label>
</div>
<div class="grouped-control-field">
{{input
id="matching-liked"
type="checkbox"
class="in-likes"
checked=searchedTerms.special.in.likes
click=(action "onChangeSearchTermForSpecialInLikes" value="target.checked")
}}
<label for="matching-liked">{{i18n "search.advanced.filters.likes"}}</label>
</div>
<div class="grouped-control-field">
{{input
id="matching-seen"
type="checkbox"
class="in-seen"
checked=searchedTerms.special.in.seen
click=(action "onChangeSearchTermForSpecialInSeen" value="target.checked")
}}
<label for="matching-seen">{{i18n "search.advanced.filters.seen"}}</label>
</div>
{{/if}}
<div class="grouped-control-field">
{{input
id="matching-in-messages"
type="checkbox"
class="in-private"
checked=searchedTerms.special.in.personal
click=(action "onChangeSearchTermForSpecialInPersonal" value="target.checked")
}}
<label for="matching-in-messages">{{i18n "search.advanced.filters.private"}}</label>
</div>
<div class="grouped-control-field">
{{input
id="matching-seen"
type="checkbox"
class="in-seen"
checked=searchedTerms.special.in.seen
click=(action "onChangeSearchTermForSpecialInSeen" value="target.checked")
}}
<label for="matching-seen">{{i18n "search.advanced.filters.seen"}}</label>
</div>
{{/if}}
{{combo-box
id="in"
valueProperty="value"
content=inOptions
value=searchedTerms.in
onChange=(action "onChangeSearchTermForIn")
options=(hash
none="user.locale.any"
clearable=true
)
}}
</fieldset>
</div>
</div>
<div class="control-group advanced-search-topic-status">
<label class="control-label" for="search-status-options">{{i18n "search.advanced.statuses.label"}}</label>
<div class="controls">
{{combo-box
id="in"
id="search-status-options"
valueProperty="value"
content=inOptions
value=searchedTerms.in
onChange=(action "onChangeSearchTermForIn")
content=statusOptions
value=searchedTerms.status
onChange=(action "onChangeSearchTermForStatus")
options=(hash
none="user.locale.any"
clearable=true
)
}}
</fieldset>
</div>
</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="search-status-options"
valueProperty="value"
content=statusOptions
value=searchedTerms.status
onChange=(action "onChangeSearchTermForStatus")
<div class="control-group advanced-search-posted-by">
<label class="control-label" for="search-posted-by">
{{i18n "search.advanced.posted_by.label"}}
</label>
<div class="controls">
{{user-chooser
id="search-posted-by"
value=searchedTerms.username
onChange=(action "onChangeSearchTermForUsername")
options=(hash
none="user.locale.any"
clearable=true
maximum=1
excludeCurrentUser=false
)
}}
}}
</div>
</div>
</div>
</div>
<div class="container advanced-search-date-count-group">
<div class="control-group pull-left">
<label class="control-label" for="search-post-date">{{i18n "search.advanced.post.time.label"}}</label>
<div class="controls full-search-dates">
{{combo-box
id="postTime"
valueProperty="value"
content=postTimeOptions
value=searchedTerms.time.when
onChange=(action "onChangeWhenTime")
}}
{{date-input
date=searchedTerms.time.days
onChange=(action "onChangeWhenDate")
id="search-post-date"
}}
<div class="control-group advanced-search-posted-date">
<label class="control-label" for="search-post-date">{{i18n "search.advanced.post.time.label"}}</label>
<div class="controls inline-form full-width">
{{combo-box
id="postTime"
valueProperty="value"
content=postTimeOptions
value=searchedTerms.time.when
onChange=(action "onChangeWhenTime")
}}
{{date-input
date=searchedTerms.time.days
onChange=(action "onChangeWhenDate")
id="search-post-date"
}}
</div>
</div>
{{plugin-outlet name="advanced-search-options-below" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}}
</div>
<div class="count-group control-group pull-left">
<label class="control-label" for="search-min-post-count">{{i18n "search.advanced.post.count.label"}}</label>
<div class="count pull-left">
<details class="search-advanced-additional-options">
<summary>
{{i18n "search.advanced.additional_options.label"}}
</summary>
<div class="count-group control-group">
{{!-- TODO: Using a label here fails no-nested-interactive lint rule --}}
<span class="control-label" for="search-min-post-count">{{i18n "search.advanced.post.count.label"}}</span>
<div class="controls">
{{input
type="number"
@ -174,11 +184,7 @@
input=(action "onChangeSearchTermMinPostCount" value="target.value")
placeholder=(i18n "search.advanced.post.min.placeholder")
}}
</div>
</div>
<span class="count-dash">&mdash;</span>
<div class="count pull-right">
<div class="controls">
{{d-icon "arrows-alt-h"}}
{{input
type="number"
value=(readonly searchedTerms.max_posts)
@ -189,11 +195,10 @@
}}
</div>
</div>
</div>
<div class="count-group control-group pull-left">
<label class="control-label" for="search-min-views">{{i18n "search.advanced.views.label"}}</label>
<div class="count pull-left">
<div class="count-group control-group">
{{!-- TODO: Using a label here fails no-nested-interactive lint rule --}}
<span class="control-label" for="search-min-views">{{i18n "search.advanced.views.label"}}</span>
<div class="controls">
{{input
type="number"
@ -203,11 +208,7 @@
input=(action "onChangeSearchTermMinViews" value="target.value")
placeholder=(i18n "search.advanced.min_views.placeholder")
}}
</div>
</div>
<span class="count-dash">&mdash;</span>
<div class="count pull-right">
<div class="controls">
{{d-icon "arrows-alt-h"}}
{{input
type="number"
value=(readonly searchedTerms.max_views)
@ -218,7 +219,5 @@
}}
</div>
</div>
</div>
</details>
</div>
{{plugin-outlet name="advanced-search-options-below" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}}

View File

@ -1,5 +1,5 @@
<div class="fps-result-entries">
{{#each posts as |post|}}
{{search-result-entry post=post bulkSelectEnabled=bulkSelectEnabled selected=selected}}
{{search-result-entry post=post bulkSelectEnabled=bulkSelectEnabled selected=selected highlightQuery=highlightQuery}}
{{/each}}
</div>

View File

@ -1,66 +1,64 @@
<div class="fps-result">
<div class="author">
<a href={{post.userPath}} data-user-card={{post.username}}>
{{avatar post imageSize="large"}}
</a>
</div>
<div class="author">
<a href={{post.userPath}} data-user-card={{post.username}}>
{{avatar post imageSize="large"}}
</a>
</div>
<div class="fps-topic">
<div class="topic">
{{#if bulkSelectEnabled}}
{{track-selected selectedList=selected selectedId=post.topic class="bulk-select"}}
{{/if}}
<div class="fps-topic">
<div class="topic">
{{#if bulkSelectEnabled}}
{{track-selected selectedList=selected selectedId=post.topic class="bulk-select"}}
{{/if}}
<a href={{post.url}} {{action "logClick" post.topic_id}} class="search-link">
{{raw "topic-status" topic=post.topic showPrivateMessageIcon=true}}
<span class="topic-title">
{{#if post.useTopicTitleHeadline}}
{{html-safe post.topicTitleHeadline}}
{{else}}
{{#highlight-search highlight=q}}
{{html-safe post.topic.fancyTitle}}
{{/highlight-search}}
{{/if}}
</span>
</a>
<div class="search-category">
{{#if post.topic.category.parentCategory}}
{{category-link post.topic.category.parentCategory}}
{{/if}}
{{category-link post.topic.category hideParent=true}}
{{#if post.topic}}
{{discourse-tags post.topic}}
{{/if}}
{{plugin-outlet name="full-page-search-category" args=(hash post=post)}}
</div>
</div>
<div class="blurb container">
<span class="date">
{{format-date post.created_at format="tiny"}}
{{#if post.blurb}}
<span class="separator">-</span>
{{/if}}
</span>
{{#if post.blurb}}
{{#if siteSettings.use_pg_headlines_for_excerpt}}
{{html-safe post.blurb}}
<a href={{post.url}} {{action "logClick" post.topic_id}} class="search-link">
{{raw "topic-status" topic=post.topic showPrivateMessageIcon=true}}
<span class="topic-title">
{{#if post.useTopicTitleHeadline}}
{{html-safe post.topicTitleHeadline}}
{{else}}
{{#highlight-search highlight=highlightQuery}}
{{html-safe post.blurb}}
{{html-safe post.topic.fancyTitle}}
{{/highlight-search}}
{{/if}}
{{/if}}
</div>
</span>
</a>
{{#if showLikeCount}}
{{#if post.like_count}}
<span class="like-count">
<span class="value">{{post.like_count}}</span> {{d-icon "heart"}}
</span>
<div class="search-category">
{{#if post.topic.category.parentCategory}}
{{category-link post.topic.category.parentCategory}}
{{/if}}
{{category-link post.topic.category hideParent=true}}
{{#if post.topic}}
{{discourse-tags post.topic}}
{{/if}}
{{plugin-outlet name="full-page-search-category" args=(hash post=post)}}
</div>
</div>
<div class="blurb container">
<span class="date">
{{format-date post.created_at format="tiny"}}
{{#if post.blurb}}
<span class="separator">-</span>
{{/if}}
</span>
{{#if post.blurb}}
{{#if siteSettings.use_pg_headlines_for_excerpt}}
{{html-safe post.blurb}}
{{else}}
{{#highlight-search highlight=highlightQuery}}
{{html-safe post.blurb}}
{{/highlight-search}}
{{/if}}
{{/if}}
</div>
{{#if showLikeCount}}
{{#if post.like_count}}
<span class="like-count">
<span class="value">{{post.like_count}}</span> {{d-icon "heart"}}
</span>
{{/if}}
{{/if}}
</div>

View File

@ -1,20 +1,59 @@
{{#d-section pageClass="search" class="search-container"}}
{{scroll-tracker name="full-page-search" tag=searchTerm class="hidden"}}
<div class="search-advanced">
{{#unless site.mobileView}}
<div class="search-bar">
{{search-text-field
value=searchTerm
class="full-page-search search no-blur search-query"
aria-label=(i18n "search.full_page_title")
enter=(action "search")
hasAutofocus=hasAutofocus
aria-controls="search-result-count"
<div class="search-header">
<h1 class="search-page-heading">
{{#if hasResults}}
<div class="result-count" id="search-result-count" aria-live="polite">
{{html-safe resultCountLabel}}
</div>
{{else}}
{{i18n "search.full_page_title"}}
{{/if}}
</h1>
<div class="search-bar">
{{search-text-field
value=searchTerm
class="full-page-search search no-blur search-query"
aria-label=(i18n "search.search_term_label")
enter=(action "search" true)
hasAutofocus=hasAutofocus
aria-controls="search-result-count"
}}
{{combo-box
id="search-type"
value=search_type
content=searchTypes
castInteger=true
onChange=(action (mut search_type))
}}
{{d-button
action=(action "search" true)
icon="search"
label="search.search_button"
class="btn-primary search-cta"
ariaLabel="search.search_button"
disabled=searchButtonDisabled
}}
</div>
{{#if usingDefaultSearchType}}
{{!-- context is only provided when searching from mobile view --}}
{{#if context}}
<div class="search-context">
<label>
{{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}}
</label>
</div>
{{/if}}
<div class="search-filters">
{{search-advanced-options
searchTerm=(readonly searchTerm)
onChangeSearchTerm=(action (mut searchTerm))
expandFilters=expandFilters
}}
{{d-button action=(action "search") icon="search" class="btn-primary search-cta" ariaLabel="search.search_button" disabled=searchButtonDisabled}}
</div>
{{/unless}}
{{/if}}
<div class="search-notice">
{{#if invalidSearch}}
@ -24,52 +63,36 @@
{{/if}}
</div>
{{!-- context is only provided when searching from mobile view --}}
<div class="search-context">
{{#if context}}
<div class="fps-search-context">
<label>
{{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}}
</label>
</div>
{{/if}}
</div>
</div>
<div class="search-advanced">
{{#if hasResults}}
<div class="search-title">
{{#if hasResults}}
{{create-topic-button canCreateTopic=canCreateTopic action=(action "createTopic" searchTerm)}}
{{/if}}
{{#if usingDefaultSearchType}}
<div class={{searchInfoClassNames}}>
{{#if canBulkSelect}}
{{d-button icon="list" class="btn-default bulk-select" title="topics.bulk.toggle" action=(action "toggleBulkSelect")}}
{{bulk-select-button selected=selected category=category action=(action "search")}}
{{/if}}
{{#if canBulkSelect}}
{{d-button icon="list" class="btn-default bulk-select" title="topics.bulk.toggle" action=(action "toggleBulkSelect")}}
{{bulk-select-button selected=selected category=category action=(action "search")}}
{{/if}}
{{#if bulkSelectEnabled}}
<div class="fps-select">
{{#if bulkSelectEnabled}}
{{d-button icon="check-square" class="btn-default" action=(action "selectAll") label="search.select_all"~}}
{{d-button icon="far-square" class="btn-default" action=(action "clearAll") label="search.clear_all"}}
</div>
{{/if}}
</div>
{{/if}}
<div class="search-info">
<div class="result-count" id="search-result-count" aria-live="polite">
{{html-safe resultCountLabel}}
<div class="sort-by inline-form">
<label for="search-sort-by">
{{i18n "search.sort_by"}}
</label>
{{combo-box
value=sortOrder
content=sortOrders
castInteger=true
onChange=(action (mut sortOrder))
id="search-sort-by"
}}
</div>
</div>
<div class="sort-by">
<span class="desc">
{{i18n "search.sort_by"}}
</span>
{{combo-box
value=sortOrder
content=sortOrders
castInteger=true
onChange=(action (mut sortOrder))
}}
</div>
</div>
{{/if}}
{{/if}}
{{plugin-outlet name="full-page-search-below-search-info" args=(hash search=searchTerm)}}
@ -79,99 +102,113 @@
{{else}}
<div class="search-results">
{{#load-more selector=".fps-result" action=(action "loadMore")}}
{{search-result-entries posts=model.posts bulkSelectEnabled=bulkSelectEnabled selected=selected}}
{{#if usingDefaultSearchType}}
{{search-result-entries
posts=model.posts
bulkSelectEnabled=bulkSelectEnabled
selected=selected
highlightQuery=highlightQuery
}}
{{#conditional-loading-spinner condition=loading }}
{{#unless hasResults}}
{{#if searchActive}}
<h3>{{i18n "search.no_results"}}</h3>
{{#conditional-loading-spinner condition=loading }}
{{#unless hasResults}}
{{#if searchActive}}
<h3>{{i18n "search.no_results"}}</h3>
{{#if model.grouped_search_result.error}}
<div class="warning">
{{model.grouped_search_result.error}}
{{#if model.grouped_search_result.error}}
<div class="warning">
{{model.grouped_search_result.error}}
</div>
{{/if}}
{{#if showSuggestion}}
<div class="no-results-suggestion">
{{i18n "search.cant_find"}}
{{#if canCreateTopic}}
<a href {{action "createTopic" searchTerm}}>{{i18n "search.start_new_topic"}}</a>
{{#unless siteSettings.login_required}}
{{i18n "search.or_search_google"}}
{{/unless}}
{{else}}
{{i18n "search.search_google"}}
{{/if}}
</div>
{{google-search searchTerm=searchTerm}}
{{/if}}
{{/if}}
{{/unless}}
{{#if hasResults}}
{{#unless loading}}
<h3 class="search-footer">
{{#if model.grouped_search_result.more_full_page_results}}
{{#if isLastPage }}
{{i18n "search.more_results"}}
{{/if}}
{{else}}
{{i18n "search.no_more_results"}}
{{/if}}
</h3>
{{/unless}}
{{/if}}
{{/conditional-loading-spinner}}
{{else}}
{{#conditional-loading-spinner condition=loading }}
{{#if hasResults}}
{{#if model.categories.length}}
<h4 class="category-heading">
{{i18n "search.categories"}}
</h4>
<div class="category-items">
{{#each model.categories as |category|}}
{{category-link category extraClasses="fps-category-item"}}
{{/each}}
</div>
{{/if}}
{{#if showSuggestion}}
<div class="no-results-suggestion">
{{i18n "search.cant_find"}}
{{#if canCreateTopic}}
<a href {{action "createTopic" searchTerm}}>{{i18n "search.start_new_topic"}}</a>
{{#unless siteSettings.login_required}}
{{i18n "search.or_search_google"}}
{{/unless}}
{{else}}
{{i18n "search.search_google"}}
{{/if}}
{{#if model.tags.length}}
<h4 class="tag-heading">
{{i18n "search.tags"}}
</h4>
<div class="tag-items">
{{#each model.tags as |tag|}}
<div class="fps-tag-item">
<a href={{tag.url}}>
{{tag.id}}
</a>
</div>
{{/each}}
</div>
{{/if}}
{{google-search searchTerm=searchTerm}}
{{#if model.users}}
{{#each model.users as |user|}}
{{#user-link user=user class="fps-user-item"}}
{{avatar user imageSize="large"}}
<div class="user-titles">
{{#if user.name}}
<span class="name">
{{user.name}}
</span>
{{/if}}
<span class="username">
{{user.username}}
</span>
</div>
{{/user-link}}
{{/each}}
{{/if}}
{{else}}
{{#if searchActive}}
<h3>{{i18n "search.no_results"}}</h3>
{{/if}}
{{/if}}
{{/unless}}
{{#if hasResults}}
{{#unless loading}}
<h3 class="search-footer">
{{#if model.grouped_search_result.more_full_page_results}}
{{#if isLastPage }}
{{i18n "search.more_results"}}
{{/if}}
{{else}}
{{i18n "search.no_more_results"}}
{{/if}}
</h3>
{{/unless}}
{{/if}}
{{/conditional-loading-spinner}}
{{/conditional-loading-spinner}}
{{/if}}
{{/load-more}}
</div>
{{/if}}
</div>
<div class="search-advanced-sidebar">
{{#if site.mobileView}}
<div class="search-bar">
{{search-text-field value=searchTerm class="full-page-search search no-blur search-query" enter=(action "search") hasAutofocus=hasAutofocus}}
{{d-button action=(action "search") icon="search" class="btn-primary search-cta" ariaLabel="search.search_button" disabled=searchButtonDisabled}}
</div>
{{/if}}
{{#if site.mobileView}}
<div role="button" class="search-advanced-title" {{on "click" (action "toggleAdvancedSearch")}}>
{{d-icon (if expanded "caret-down" "caret-right")}}
<span>{{i18n "search.advanced.title"}}</span>
</div>
{{else}}
<h1 class="search-advanced-title">
{{i18n "search.advanced.title"}}
</h1>
{{/if}}
{{#if site.mobileView}}
{{#if expanded}}
<div class="search-advanced-filters">
{{search-advanced-options
searchTerm=(readonly searchTerm)
onChangeSearchTerm=(action (mut searchTerm))
}}
</div>
{{/if}}
{{else}}
<div class="search-advanced-filters">
{{search-advanced-options
searchTerm=(readonly searchTerm)
onChangeSearchTerm=(action (mut searchTerm))
onChangeCategory=(action (mut category))
}}
{{d-button
label="submit"
action=(action "search")
icon="search"
class="btn-primary search-cta"
disabled=searchButtonDisabled}}
</div>
{{/if}}
</div>
{{/d-section}}

View File

@ -9,6 +9,11 @@ import {
} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
import { skip, test } from "qunit";
import {
SEARCH_TYPE_CATS_TAGS,
SEARCH_TYPE_DEFAULT,
SEARCH_TYPE_USERS,
} from "discourse/controllers/full-page-search";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance("Search - Full Page", function (needs) {
@ -16,7 +21,12 @@ acceptance("Search - Full Page", function (needs) {
needs.settings({ tagging_enabled: true });
needs.pretender((server, helper) => {
server.get("/tags/filter/search", () => {
return helper.response({ results: [{ text: "monkey", count: 1 }] });
return helper.response({
results: [
{ text: "monkey", count: 1 },
{ text: "gazelle", count: 2 },
],
});
});
server.get("/u/search/users", () => {
@ -126,6 +136,8 @@ acceptance("Search - Full Page", function (needs) {
1,
"shows the right icon"
);
assert.equal(count(".search-highlight"), 1, "search highlights work");
});
test("escape search term", async function (assert) {
@ -419,7 +431,9 @@ acceptance("Search - Full Page", function (needs) {
await fillIn("#search-min-post-count", "5");
assert.equal(
queryAll(".search-advanced-options #search-min-post-count").val(),
queryAll(
".search-advanced-additional-options #search-min-post-count"
).val(),
"5",
'has "5" populated'
);
@ -436,7 +450,9 @@ acceptance("Search - Full Page", function (needs) {
await fillIn("#search-max-post-count", "5");
assert.equal(
queryAll(".search-advanced-options #search-max-post-count").val(),
queryAll(
".search-advanced-additional-options #search-max-post-count"
).val(),
"5",
'has "5" populated'
);
@ -469,4 +485,100 @@ acceptance("Search - Full Page", function (needs) {
"does not populate the likes checkbox"
);
});
test("all tags checkbox only visible for two or more tags", async function (assert) {
await visit("/search?expanded=true");
const tagSelector = selectKit("#search-with-tags");
await tagSelector.expand();
await tagSelector.selectRowByValue("monkey");
assert.ok(!visible("input.all-tags"), "all tags checkbox not visible");
await tagSelector.selectRowByValue("gazelle");
assert.ok(visible("input.all-tags"), "all tags checkbox is visible");
});
test("search for users", async function (assert) {
await visit("/search");
const typeSelector = selectKit(".search-bar .select-kit#search-type");
await fillIn(".search-query", "admin");
assert.ok(!exists(".fps-user-item"), "has no user results");
await typeSelector.expand();
await typeSelector.selectRowByValue(SEARCH_TYPE_USERS);
assert.ok(!exists(".search-filters"), "has no filters");
await click(".search-cta");
assert.equal(count(".fps-user-item"), 1, "has one user result");
await typeSelector.expand();
await typeSelector.selectRowByValue(SEARCH_TYPE_DEFAULT);
assert.ok(
exists(".search-filters"),
"returning to topic/posts shows filters"
);
assert.ok(!exists(".fps-user-item"), "has no user results");
});
test("search for categories/tags", async function (assert) {
await visit("/search");
await fillIn(".search-query", "monk");
const typeSelector = selectKit(".search-bar .select-kit#search-type");
assert.ok(!exists(".fps-tag-item"), "has no category/tag results");
await typeSelector.expand();
await typeSelector.selectRowByValue(SEARCH_TYPE_CATS_TAGS);
await click(".search-cta");
assert.ok(!exists(".search-filters"), "has no filters");
assert.equal(count(".fps-tag-item"), 2, "has two tag results");
await typeSelector.expand();
await typeSelector.selectRowByValue(SEARCH_TYPE_DEFAULT);
assert.ok(
exists(".search-filters"),
"returning to topic/posts shows filters"
);
assert.ok(!exists(".user-item"), "has no user results");
});
test("filters expand/collapse as expected", async function (assert) {
await visit("/search?expanded=true");
assert.ok(
visible(".search-advanced-options"),
"advanced filters are expanded when url query param is included"
);
await fillIn(".search-query", "none");
await click(".search-cta");
assert.ok(
!visible(".search-advanced-options"),
"launching a search collapses advanced filters"
);
await visit("/search");
assert.ok(
!visible(".search-advanced-options"),
"filters are collapsed when query param is not present"
);
await click(".advanced-filters > summary");
assert.ok(
visible(".search-advanced-options"),
"clicking on element expands filters"
);
});
});

View File

@ -3,6 +3,7 @@ import {
count,
exists,
queryAll,
visible,
} from "discourse/tests/helpers/qunit-helpers";
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
@ -22,11 +23,10 @@ acceptance("Search - Mobile", function (needs) {
assert.ok(!exists(".search-results .fps-topic"), "no results by default");
await click(".search-advanced-title");
await click(".advanced-filters summary");
assert.equal(
count(".search-advanced-filters"),
1,
assert.ok(
visible(".search-advanced-filters"),
"it should expand advanced search filters"
);
@ -36,7 +36,7 @@ acceptance("Search - Mobile", function (needs) {
assert.equal(count(".fps-topic"), 1, "has one post");
assert.ok(
!exists(".search-advanced-filters"),
!visible(".search-advanced-filters"),
"it should collapse advanced search filters"
);

View File

@ -1,182 +1,179 @@
@mixin search-page-spacing {
padding: 1rem 10%;
@include breakpoint(medium) {
padding: 1rem;
}
}
.search-highlight {
font-weight: bold;
}
.search-container {
display: flex;
justify-content: space-between;
.search-header {
@include search-page-spacing();
background: var(--primary-very-low);
}
.warning {
background-color: var(--danger-medium);
padding: 5px 8px;
color: var(--secondary);
}
.search-page-heading {
font-size: var(--font-up-3);
// spans can be in different orders depending of locale
span + span {
margin-left: 0.25em;
}
span.term {
background: var(--tertiary-low);
}
}
.search-bar {
display: flex;
justify-content: space-between;
align-items: stretch;
margin-bottom: 1em;
background: var(--primary-very-low);
.search-query {
flex: 1 0 0px;
margin: 0 0.5em 0 0;
input.search-query {
flex: 1 0 60%;
margin: 0 1em 0 0;
}
.search-cta {
padding-bottom: 6.5px;
padding-top: 6.5px;
.select-kit {
margin-right: 1em;
flex: 1 0 20%;
}
@include breakpoint(mobile-extra-large) {
flex-direction: column;
input.search-query,
.select-kit {
margin-right: 0;
margin-bottom: 0.5em;
}
}
}
.search-advanced {
width: 70%;
@include breakpoint(medium) {
width: 65%;
}
position: relative;
.search-actions,
.search-notice,
.search-results,
.search-title,
.search-bar {
margin-bottom: 1em;
}
.search-results {
@include search-page-spacing();
padding-bottom: 3em;
}
.search-info {
display: flex;
@include search-page-spacing();
flex-wrap: wrap;
border-bottom: 1px solid var(--primary-low);
padding-bottom: 1em;
margin-bottom: 1.5em;
padding-top: 2em;
margin-bottom: 2em;
flex-direction: row;
align-items: center;
align-items: flex-start;
justify-content: flex-start;
.result-count {
display: flex;
.term {
font-weight: bold;
}
// spans can be in different orders depending of locale
span + span {
margin-left: 0.25em;
}
&.bulk-select-visible {
@include sticky;
top: 60px;
background-color: var(--secondary);
z-index: 10;
}
.sort-by {
display: flex;
margin-left: auto;
align-items: center;
.desc {
margin-right: 0.5em;
}
.combo-box {
min-width: 150px;
}
}
}
.search-title {
display: flex;
justify-content: flex-start;
align-items: stretch;
flex-wrap: wrap;
padding-right: 2.6em; // placeholder for fixed position bulk search button
button {
margin: 0 0.5em 0.5em 0;
}
.bulk-select-container {
order: 2; // last button
margin-left: auto;
z-index: z("dropdown"); // below composer
}
#bulk-select {
position: fixed;
position: relative;
right: unset;
margin: 0;
padding: 0;
display: inline;
button {
margin: 0;
box-shadow: 0 0 0.4em 0.45em var(--secondary); // slight fade behind the button, because it can overlay content
margin-right: 0.5em;
}
}
}
.search-notice {
.fps-invalid {
padding: 0.5em;
background-color: var(--danger-low);
border: 1px solid var(--danger-medium);
color: var(--danger);
}
}
}
.search-advanced-sidebar {
width: 30%;
@include breakpoint(medium) {
width: 35%;
}
margin-left: 1em;
.search-notice .fps-invalid {
padding: 0.5em;
background-color: var(--danger-low);
border: 1px solid var(--danger-medium);
color: var(--danger);
}
.search-context {
margin-top: 1em;
}
.search-filters {
background: var(--primary-very-low);
display: flex;
flex-direction: column;
.input-small,
.combo-box,
.ac-wrap,
details.advanced-filters,
details.search-advanced-additional-options {
margin-top: 1em;
> summary {
color: var(--tertiary);
cursor: pointer;
}
&[open] > summary {
color: var(--primary);
margin-bottom: 1em;
}
}
details.search-advanced-additional-options {
> summary {
font-size: var(--font-down-1);
}
}
.combo-box:not(#postTime),
.control-group,
.multi-select,
.search-advanced-category-chooser {
box-sizing: border-box;
.multi-select {
width: 100%;
min-width: 100%;
margin: 0;
input,
.item {
padding-left: 4px; // temporarily normalizing input padding for this section
}
}
.d-date-input {
margin-top: 0.5em;
width: 100%;
}
.search-advanced-title {
font-size: $font-up-1;
background: var(--primary-low);
padding: 0.358em 1em;
margin-bottom: 0;
@include breakpoint(medium) {
padding: 0.358em 0.5em;
}
font-weight: 700;
text-align: left;
cursor: pointer;
.d-icon {
margin: 0;
}
}
.search-advanced-filters {
background: var(--primary-very-low);
padding: 1em;
.control-group {
margin-bottom: 15px;
@include breakpoint(mobile-extra-large, min-width) {
.search-advanced-options {
column-count: 2;
column-gap: 2em;
.control-group {
break-inside: avoid;
}
}
}
section.field {
margin-top: 5px;
@include breakpoint(medium, min-width) {
.search-advanced-options {
column-gap: 5em;
}
}
@include breakpoint(medium) {
padding: 0.75em 0.5em;
.ac-wrap,
.choices,
.select-kit.multi-select {
// overriding inline width from JS
@ -187,13 +184,18 @@
}
}
.control-group {
margin-bottom: 1em;
}
.count-group {
.count {
width: 45%;
input[type="number"] {
width: 8em;
}
.count-dash {
padding-left: 6px;
vertical-align: middle;
.d-icon {
margin-left: 0.25em;
margin-right: 0.25em;
}
}
}
@ -201,15 +203,21 @@
}
.fps-invalid {
margin-top: 1em;
margin-bottom: 1em;
}
.fps-result {
display: flex;
padding: 0 0.5em;
margin-bottom: 28px;
max-width: 780px;
margin-bottom: 2em;
max-width: 100%;
word-break: break-word;
position: relative;
&.bulk-select-enabled {
padding-left: 3em;
}
.author {
display: inline-block;
@ -229,7 +237,14 @@
grid-template-columns: auto 1fr;
align-items: baseline;
.bulk-select {
grid-area: bulk-select;
position: absolute;
left: 0px;
top: 0px;
padding: 0.5em;
background: var(--tertiary-very-low);
input[type="checkbox"] {
margin: 0;
}
}
.search-link {
grid-area: title;
@ -260,19 +275,9 @@
}
}
input[type="checkbox"] {
margin-top: 0;
margin-left: 0;
// cross-browser alignment below
position: relative;
vertical-align: bottom;
margin-bottom: 0.39em;
}
.blurb {
font-size: $font-0;
line-height: $line-height-large;
max-width: 640px;
color: var(--primary-medium);
.date {
color: var(--primary-high);
@ -319,18 +324,48 @@
}
}
.no-results-suggestion {
margin-top: 30px;
}
.search-footer {
margin-bottom: 30px;
}
.panel-body-contents .search-context label {
float: left;
}
.no-results-suggestion,
.google-search-form {
margin-top: 2em;
margin-top: 1em;
}
// temporary
.search-results {
.fps-user-item {
margin-bottom: 1.5em;
display: flex;
flex-direction: row;
align-items: center;
.avatar {
margin-right: 0.5em;
min-width: 25px;
}
.user-titles {
display: flex;
flex-direction: column;
max-width: 300px;
.name {
color: var(--primary-high-or-secondary-low);
font-size: var(--font-0);
font-weight: 700;
@include ellipsis;
}
.username {
color: var(--primary-high-or-secondary-low);
font-size: var(--font-down-1);
@include ellipsis;
}
}
}
.category-items,
.tag-items {
margin-bottom: 1.5em;
.fps-category-item,
.fps-tag-item {
margin-bottom: 1.5em;
display: block;
}
}
}

View File

@ -174,7 +174,8 @@ input[type="submit"] {
> .select-kit,
> input[type="text"],
> label,
> .btn {
> .btn,
> .d-date-input {
margin-bottom: 0.5em; // for when items wrap (mobile, narrow windows)
margin-right: 0.5em;
&:last-child {

View File

@ -1,48 +1,6 @@
.search-container {
flex-direction: column;
margin: 0;
.search-advanced {
order: 1;
width: 100%;
.search-info {
flex-direction: column;
align-items: left;
justify-content: center;
.sort-by {
display: flex;
align-items: center;
margin-top: 0.5em;
margin-left: 0;
width: 100%;
.select-kit {
flex: 1 1 auto;
}
}
}
}
.search-notice {
margin-top: 1em;
}
.search-advanced-sidebar {
order: 0;
width: 100%;
margin: 0;
.tag-chooser,
.user-chooser {
width: 100%;
}
}
}
.fps-result {
input[type="checkbox"] {
vertical-align: baseline;
.search-advanced .search-info {
padding-left: 0;
padding-right: 0;
}
}

View File

@ -2338,7 +2338,7 @@ en:
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"
full_page_title: "search topics or posts"
full_page_title: "Search"
no_results: "No results found."
no_more_results: "No more results found."
post_format: "#%{post_number} by %{username}"
@ -2350,6 +2350,14 @@ en:
search_google: "Try searching with Google instead:"
search_google_button: "Google"
search_button: "Search"
search_term_label: "enter search keyword"
categories: "Categories"
tags: "Tags"
type:
default: "Topics/posts"
users: "Users"
categories_and_tags: "Categories/tags"
context:
user: "Search posts by @%{username}"
@ -2359,7 +2367,7 @@ en:
private_messages: "Search messages"
advanced:
title: Advanced Search
title: Advanced filters
posted_by:
label: Posted by
in_category:
@ -2412,6 +2420,8 @@ en:
placeholder: minimum
max_views:
placeholder: maximum
additional_options:
label: "Filter by post count and topic views"
hamburger_menu: "go to another topic list or category"
new_item: "new"