From dfeca42bf82b07c054bca940da986318fd91c02a Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Mon, 20 Sep 2021 10:01:11 -0400 Subject: [PATCH] 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. --- .../app/components/search-advanced-options.js | 16 +- .../app/components/search-result-entry.js | 4 +- .../app/controllers/full-page-search.js | 178 +++++++--- .../javascripts/discourse/app/lib/search.js | 2 +- .../templates/components/google-search.hbs | 12 +- .../components/search-advanced-options.hbs | 317 +++++++++-------- .../components/search-result-entries.hbs | 2 +- .../components/search-result-entry.hbs | 108 +++--- .../app/templates/full-page-search.hbs | 301 +++++++++------- .../tests/acceptance/search-full-test.js | 118 ++++++- .../tests/acceptance/search-mobile-test.js | 10 +- .../stylesheets/common/base/search.scss | 323 ++++++++++-------- .../stylesheets/common/foundation/base.scss | 3 +- app/assets/stylesheets/mobile/search.scss | 48 +-- config/locales/client.en.yml | 14 +- 15 files changed, 856 insertions(+), 600 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/search-advanced-options.js b/app/assets/javascripts/discourse/app/components/search-advanced-options.js index 0bcb5007f3..60658c9014 100644 --- a/app/assets/javascripts/discourse/app/components/search-advanced-options.js +++ b/app/assets/javascripts/discourse/app/components/search-advanced-options.js @@ -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], ""); diff --git a/app/assets/javascripts/discourse/app/components/search-result-entry.js b/app/assets/javascripts/discourse/app/components/search-result-entry.js index 44494409ab..59cd22206a 100644 --- a/app/assets/javascripts/discourse/app/components/search-result-entry.js +++ b/app/assets/javascripts/discourse/app/components/search-result-entry.js @@ -1,5 +1,7 @@ import Component from "@ember/component"; export default Component.extend({ - tagName: "", + tagName: "div", + classNames: ["fps-result"], + classNameBindings: ["bulkSelectEnabled"], }); diff --git a/app/assets/javascripts/discourse/app/controllers/full-page-search.js b/app/assets/javascripts/discourse/app/controllers/full-page-search.js index 4fdd54a6fc..a5bbf30948 100644 --- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js +++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js @@ -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() { diff --git a/app/assets/javascripts/discourse/app/lib/search.js b/app/assets/javascripts/discourse/app/lib/search.js index a53769b512..eb2e829c95 100644 --- a/app/assets/javascripts/discourse/app/lib/search.js +++ b/app/assets/javascripts/discourse/app/lib/search.js @@ -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(); diff --git a/app/assets/javascripts/discourse/app/templates/components/google-search.hbs b/app/assets/javascripts/discourse/app/templates/components/google-search.hbs index 16b84de567..5cff891f22 100644 --- a/app/assets/javascripts/discourse/app/templates/components/google-search.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/google-search.hbs @@ -1,7 +1,5 @@ -
- -
+ diff --git a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs index 03b0a9a4d8..d6f7604b3b 100644 --- a/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/search-advanced-options.hbs @@ -1,170 +1,180 @@ -{{plugin-outlet name="advanced-search-options-above" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}} + + {{i18n "search.advanced.title"}} + +
+
+ {{plugin-outlet name="advanced-search-options-above" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}} -
-
- -
- {{user-chooser - id="search-posted-by" - value=searchedTerms.username - onChange=(action "onChangeSearchTermForUsername") - options=(hash - maximum=1 - excludeCurrentUser=false - ) - }} -
-
-
- -
- {{search-advanced-category-chooser - id="search-in-category" - value=searchedTerms.category.id - onChange=(action "onChangeSearchTermForCategory") - }} -
-
-
- -{{#if siteSettings.tagging_enabled}} -
-
- +
+
- {{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") }} -
- -
-
-{{/if}} -
-
-
-
- {{i18n "search.advanced.filters.label"}} + {{#if siteSettings.tagging_enabled}} +
+ +
+ {{tag-chooser + id="search-with-tags" + tags=searchedTerms.tags + allowCreate=false + everyTag=true + unlimitedTagCount=true + onChange=(action "onChangeSearchTermForTags") + }} + {{#if showAllTagsCheckbox}} +
+ +
+ {{/if}} +
+
+ {{/if}} - {{#if currentUser}} -
- {{input - id="matching-title-only" - type="checkbox" - class="in-title" - checked=searchedTerms.special.in.title - click=(action "onChangeSearchTermForSpecialInTitle" value="target.checked") - }} - -
+
+
+
+ {{i18n "search.advanced.filters.label"}} -
- {{input - id="matching-liked" - type="checkbox" - class="in-likes" - checked=searchedTerms.special.in.likes - click=(action "onChangeSearchTermForSpecialInLikes" value="target.checked") - }} - -
+ {{#if currentUser}} +
+ {{input + id="matching-title-only" + type="checkbox" + class="in-title" + checked=searchedTerms.special.in.title + click=(action "onChangeSearchTermForSpecialInTitle" value="target.checked") + }} + +
-
- {{input - id="matching-in-messages" - type="checkbox" - class="in-private" - checked=searchedTerms.special.in.personal - click=(action "onChangeSearchTermForSpecialInPersonal" value="target.checked") - }} - -
+
+ {{input + id="matching-liked" + type="checkbox" + class="in-likes" + checked=searchedTerms.special.in.likes + click=(action "onChangeSearchTermForSpecialInLikes" value="target.checked") + }} + +
-
- {{input - id="matching-seen" - type="checkbox" - class="in-seen" - checked=searchedTerms.special.in.seen - click=(action "onChangeSearchTermForSpecialInSeen" value="target.checked") - }} - -
- {{/if}} +
+ {{input + id="matching-in-messages" + type="checkbox" + class="in-private" + checked=searchedTerms.special.in.personal + click=(action "onChangeSearchTermForSpecialInPersonal" value="target.checked") + }} + +
+ +
+ {{input + id="matching-seen" + type="checkbox" + class="in-seen" + checked=searchedTerms.special.in.seen + click=(action "onChangeSearchTermForSpecialInSeen" value="target.checked") + }} + +
+ {{/if}} + + {{combo-box + id="in" + valueProperty="value" + content=inOptions + value=searchedTerms.in + onChange=(action "onChangeSearchTermForIn") + options=(hash + none="user.locale.any" + clearable=true + ) + }} +
+
+
+ +
+ +
{{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 ) }} -
+
-
-
- -
- {{combo-box - id="search-status-options" - valueProperty="value" - content=statusOptions - value=searchedTerms.status - onChange=(action "onChangeSearchTermForStatus") + +
+ +
+ {{user-chooser + id="search-posted-by" + value=searchedTerms.username + onChange=(action "onChangeSearchTermForUsername") options=(hash - none="user.locale.any" - clearable=true + maximum=1 + excludeCurrentUser=false ) - }} + }} +
-
-
-
-
- -
- {{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" - }} +
+ +
+ {{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" + }} +
+ + {{plugin-outlet name="advanced-search-options-below" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}}
-
- -
+
+ + {{i18n "search.advanced.additional_options.label"}} + +
+ {{!-- TODO: Using a label here fails no-nested-interactive lint rule --}} + {{i18n "search.advanced.post.count.label"}}
{{input type="number" @@ -174,11 +184,7 @@ input=(action "onChangeSearchTermMinPostCount" value="target.value") placeholder=(i18n "search.advanced.post.min.placeholder") }} -
-
- -
-
+ {{d-icon "arrows-alt-h"}} {{input type="number" value=(readonly searchedTerms.max_posts) @@ -189,11 +195,10 @@ }}
-
-
- -
+
+ {{!-- TODO: Using a label here fails no-nested-interactive lint rule --}} + {{i18n "search.advanced.views.label"}}
{{input type="number" @@ -203,11 +208,7 @@ input=(action "onChangeSearchTermMinViews" value="target.value") placeholder=(i18n "search.advanced.min_views.placeholder") }} -
-
- -
-
+ {{d-icon "arrows-alt-h"}} {{input type="number" value=(readonly searchedTerms.max_views) @@ -218,7 +219,5 @@ }}
-
+
- -{{plugin-outlet name="advanced-search-options-below" args=(hash searchedTerms=searchedTerms onChangeSearchedTermField=onChangeSearchedTermField) tagName=""}} diff --git a/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs b/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs index ebc30486f3..55f3e6d2c4 100644 --- a/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/search-result-entries.hbs @@ -1,5 +1,5 @@
{{#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}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/search-result-entry.hbs b/app/assets/javascripts/discourse/app/templates/components/search-result-entry.hbs index 652820e589..0aa042f3df 100644 --- a/app/assets/javascripts/discourse/app/templates/components/search-result-entry.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/search-result-entry.hbs @@ -1,66 +1,64 @@ -
- + -
-
- {{#if bulkSelectEnabled}} - {{track-selected selectedList=selected selectedId=post.topic class="bulk-select"}} - {{/if}} +
+
+ {{#if bulkSelectEnabled}} + {{track-selected selectedList=selected selectedId=post.topic class="bulk-select"}} + {{/if}} - - {{raw "topic-status" topic=post.topic showPrivateMessageIcon=true}} - - {{#if post.useTopicTitleHeadline}} - {{html-safe post.topicTitleHeadline}} - {{else}} - {{#highlight-search highlight=q}} - {{html-safe post.topic.fancyTitle}} - {{/highlight-search}} - {{/if}} - - - -
- {{#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)}} -
-
- - + + - {{#if showLikeCount}} - {{#if post.like_count}} - +
+ {{#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)}} +
+
+ +
+ + {{format-date post.created_at format="tiny"}} + {{#if post.blurb}} + - + {{/if}} + + + {{#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}}
+ + {{#if showLikeCount}} + {{#if post.like_count}} + + {{/if}} + {{/if}}
diff --git a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs index d1858fad5d..8ebfa92588 100644 --- a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs @@ -1,20 +1,59 @@ {{#d-section pageClass="search" class="search-container"}} {{scroll-tracker name="full-page-search" tag=searchTerm class="hidden"}} -
- {{#unless site.mobileView}} -