FEATURE: show recent searches in quick search panel (#15024)

FEATURE: show recent searches in quick search panel (#15024)

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 d27a573..d75bc34 100644
--- a/app/assets/javascripts/discourse/app/controllers/full-page-search.js
+++ b/app/assets/javascripts/discourse/app/controllers/full-page-search.js
@@ -5,6 +5,7 @@ import {
   isValidSearchTerm,
   searchContextDescription,
   translateResults,
+  updateRecentSearches,
 } from "discourse/lib/search";
 import Category from "discourse/models/category";
 import Composer from "discourse/models/composer";
@@ -345,6 +346,9 @@ export default Controller.extend({
           });
         break;
       default:
+        if (this.currentUser) {
+          updateRecentSearches(this.currentUser, searchTerm);
+        }
         ajax("/search", { data: args })
           .then(async (results) => {
             const model = (await translateResults(results)) || {};
diff --git a/app/assets/javascripts/discourse/app/lib/search.js b/app/assets/javascripts/discourse/app/lib/search.js
index 09cdeae..6df0545 100644
--- a/app/assets/javascripts/discourse/app/lib/search.js
+++ b/app/assets/javascripts/discourse/app/lib/search.js
@@ -17,6 +17,7 @@ import { userPath } from "discourse/lib/url";
 import userSearch from "discourse/lib/user-search";
 
 const translateResultsCallbacks = [];
+const MAX_RECENT_SEARCHES = 5; // should match backend constant with the same name
 
 export function addSearchResultsCallback(callback) {
   translateResultsCallbacks.push(callback);
@@ -230,3 +231,16 @@ export function applySearchAutocomplete($input, siteSettings) {
     );
   }
 }
+
+export function updateRecentSearches(currentUser, term) {
+  let recentSearches = Object.assign(currentUser.recent_searches || []);
+
+  if (recentSearches.includes(term)) {
+    recentSearches = recentSearches.without(term);
+  } else if (recentSearches.length === MAX_RECENT_SEARCHES) {
+    recentSearches.popObject();
+  }
+
+  recentSearches.unshiftObject(term);
+  currentUser.set("recent_searches", recentSearches);
+}
diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js
index 2876dd6..2143457 100644
--- a/app/assets/javascripts/discourse/app/models/user.js
+++ b/app/assets/javascripts/discourse/app/models/user.js
@@ -1072,6 +1072,14 @@ User.reopenClass(Singleton, {
     return ajax(userPath("check_email"), { data: { email } });
   },
 
+  loadRecentSearches() {
+    return ajax(`/u/recent-searches`);
+  },
+
+  resetRecentSearches() {
+    return ajax(`/u/recent-searches`, { type: "DELETE" });
+  },
+
   groupStats(stats) {
     const responses = UserActionStat.create({
       count: 0,
diff --git a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js
index d607ad3..ae3d08b 100644
--- a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js
+++ b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js
@@ -8,10 +8,12 @@ import { dateNode } from "discourse/helpers/node";
 import { emojiUnescape } from "discourse/lib/text";
 import getURL from "discourse-common/lib/get-url";
 import { h } from "virtual-dom";
+import hbs from "discourse/widgets/hbs-compiler";
 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 } from "discourse/widgets/search-menu";
+import User from "discourse/models/user";
 
 const suggestionShortcuts = [
   "in:title",
@@ -585,6 +587,14 @@ createWidget("search-menu-initial-options", {
 
     if (content.length === 0) {
       content.push(this.attach("random-quick-tip"));
+
+      if (this.currentUser && this.siteSettings.log_search_queries) {
+        if (this.currentUser.recent_searches?.length) {
+          content.push(this.attach("search-menu-recent-searches"));
+        } else {
+          this.loadRecentSearches();
+        }
+      }
     }
 
     return content;
@@ -602,6 +612,22 @@ createWidget("search-menu-initial-options", {
       ],
     });
   },
+
+  refreshSearchMenuResults() {
+    this.scheduleRerender();
+  },
+
+  loadRecentSearches() {
+    User.loadRecentSearches().then((result) => {
+      if (result.success && result.recent_searches?.length) {
+        this.currentUser.set(
+          "recent_searches",
+          Object.assign(result.recent_searches)
+        );
+        this.scheduleRerender();
+      }
+    });
+  },
 });
 
 createWidget("search-menu-assistant-item", {
@@ -612,7 +638,7 @@ createWidget("search-menu-assistant-item", {
     const attributes = {};
     attributes.href = "#";
 
-    let content = [iconNode("search")];
+    let content = [iconNode(attrs.icon || "search")];
 
     if (prefix) {
       content.push(h("span.search-item-prefix", `${prefix} `));
@@ -702,3 +728,35 @@ createWidget("random-quick-tip", {
     }
   },
 });
+
+createWidget("search-menu-recent-searches", {
+  tagName: "div.search-menu-recent",
+
+  template: hbs`
+    <div class="heading">
+      <h4>{{i18n "search.recent"}}</h4>
+      {{flat-button
+        className="clear-recent-searches"
+        title="search.clear_recent"
+        icon="times"
+        action="clearRecent"
+      }}
+    </div>
+
+    {{#each this.currentUser.recent_searches as |slug|}}
+      {{attach
+        widget="search-menu-assistant-item"
+        attrs=(hash slug=slug icon="history")
+      }}
+    {{/each}}
+  `,
+
+  clearRecent() {
+    return User.resetRecentSearches().then((result) => {
+      if (result.success) {
+        this.currentUser.recent_searches.clear();
+        this.sendWidgetAction("refreshSearchMenuResults");
+      }
+    });
+  },
+});
diff --git a/app/assets/javascripts/discourse/app/widgets/search-menu.js b/app/assets/javascripts/discourse/app/widgets/search-menu.js
index 8a76a33..78f16ba 100644
--- a/app/assets/javascripts/discourse/app/widgets/search-menu.js
+++ b/app/assets/javascripts/discourse/app/widgets/search-menu.js
@@ -1,4 +1,8 @@
-import { isValidSearchTerm, searchForTerm } from "discourse/lib/search";
+import {
+  isValidSearchTerm,
+  searchForTerm,
+  updateRecentSearches,
+} from "discourse/lib/search";
 import DiscourseURL from "discourse/lib/url";
 import { createWidget } from "discourse/widgets/widget";
 import discourseDebounce from "discourse-common/lib/debounce";
@@ -456,6 +460,9 @@ export default createWidget("search-menu", {
       searchData.loading = true;
       cancel(this.state._debouncer);
       SearchHelper.perform(this);
+      if (this.currentUser) {
+        updateRecentSearches(this.currentUser, searchData.term);
+      }
     } else {
       searchData.loading = false;
       if (!this.state.inTopicContext) {
diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-test.js
index 6be3204..f500897 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/search-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/search-test.js
@@ -332,6 +332,7 @@ acceptance("Search - Anonymous", function (needs) {
 
 acceptance("Search - Authenticated", function (needs) {
   needs.user();
+  needs.settings({ log_search_queries: true });
 
   needs.pretender((server, helper) => {
     server.get("/search/query", (request) => {
@@ -506,6 +507,27 @@ acceptance("Search - Authenticated", function (needs) {
     await triggerKeyEvent("#search-term", "keydown", keyEnter);
     assert.ok(exists(query(`.search-menu`)), "search dropdown is visible");
   });
+
+  test("Shows recent search results", async function (assert) {
+    await visit("/");
+    await click("#search-button");
+
+    assert.strictEqual(
+      query(
+        ".search-menu .search-menu-recent li:nth-of-type(1) .search-link"
+      ).textContent.trim(),
+      "yellow",

[... diff too long, it was truncated ...]

GitHub sha: d99deaf1ab27e24e42a88140a67aabfc907acd8c

This commit appears in #15024 which was approved by eviltrout. It was merged by pmusaraj.