diff --git a/app/assets/javascripts/discourse/app/routes/tag-show.js b/app/assets/javascripts/discourse/app/routes/tag-show.js index 9c73f3b1d8d..2482f9159de 100644 --- a/app/assets/javascripts/discourse/app/routes/tag-show.js +++ b/app/assets/javascripts/discourse/app/routes/tag-show.js @@ -136,7 +136,21 @@ export default DiscourseRoute.extend(FilterModeMixin, { noSubcategories, loading: false, }); - this.searchService.set("searchContext", model.tag.searchContext); + + if (model.category || model.additionalTags) { + const tagIntersectionSearchContext = { + type: "tagIntersection", + tagId: model.tag.id, + tag: model.tag, + additionalTags: model.additionalTags || null, + categoryId: model.category?.id || null, + category: model.category || null, + }; + + this.searchService.set("searchContext", tagIntersectionSearchContext); + } else { + this.searchService.set("searchContext", model.tag.searchContext); + } }, titleToken() { 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 bc88e254e5c..6b4464313d6 100644 --- a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js +++ b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js @@ -415,13 +415,42 @@ createWidget("search-menu-assistant", { const content = []; const { suggestionKeyword, term } = attrs; - let prefix = term?.split(suggestionKeyword)[0].trim() || ""; - if (prefix.length) { - prefix = `${prefix} `; + let prefix; + if (suggestionKeyword !== "+") { + prefix = term?.split(suggestionKeyword)[0].trim() || ""; + + if (prefix.length) { + prefix = `${prefix} `; + } } switch (suggestionKeyword) { + case "+": + attrs.results.forEach((item) => { + if (item.additionalTags) { + prefix = term?.split(" ").slice(0, -1).join(" ").trim() || ""; + } else { + prefix = term?.split("#")[0].trim() || ""; + } + + if (prefix.length) { + prefix = `${prefix} `; + } + + content.push( + this.attach("search-menu-assistant-item", { + prefix, + tag: item.tagName, + additionalTags: item.additionalTags, + category: item.category, + slug: term, + withInLabel: attrs.withInLabel, + isIntersection: true, + }) + ); + }); + break; case "#": attrs.results.forEach((item) => { if (item.model) { @@ -572,6 +601,36 @@ createWidget("search-menu-initial-options", { }) ); break; + case "tagIntersection": + let tagTerm; + if (ctx.additionalTags) { + const tags = [ctx.tagId, ...ctx.additionalTags]; + tagTerm = `${term} tags:${tags.join("+")}`; + } else { + tagTerm = `${term} #${ctx.tagId}`; + } + let suggestionOptions = { + tagName: ctx.tagId, + additionalTags: ctx.additionalTags, + }; + if (ctx.category) { + const categorySlug = ctx.category.parentCategory + ? `#${ctx.category.parentCategory.slug}:${ctx.category.slug}` + : `#${ctx.category.slug}`; + suggestionOptions.categoryName = categorySlug; + suggestionOptions.category = ctx.category; + tagTerm = tagTerm + ` ${categorySlug}`; + } + + content.push( + this.attach("search-menu-assistant", { + term: tagTerm, + suggestionKeyword: "+", + results: [suggestionOptions], + withInLabel: true, + }) + ); + break; case "user": content.push( this.attach("search-menu-assistant-item", { @@ -677,11 +736,22 @@ createWidget("search-menu-assistant-item", { link: false, }) ); - } else if (attrs.tag) { - attributes.href = getURL(`/tag/${attrs.tag}`); - content.push(iconNode("tag")); - content.push(h("span.search-item-tag", attrs.tag)); + // category and tag combination + if (attrs.tag && attrs.isIntersection) { + attributes.href = getURL(`/tag/${attrs.tag}`); + content.push(iconNode("tag")); + content.push(h("span.search-item-tag", attrs.tag)); + } + } else if (attrs.tag) { + if (attrs.isIntersection && attrs.additionalTags?.length) { + const tags = [attrs.tag, ...attrs.additionalTags]; + content.push(h("span.search-item-tag", `tags:${tags.join("+")}`)); + } else { + 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", { diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-test.js index f0b21db01e7..49a7cff76f2 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/search-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/search-test.js @@ -757,6 +757,47 @@ acceptance("Search - assistant", function (needs) { return helper.response(searchFixtures["search/query"]); }); + server.get("/tag/dev/notifications", () => { + return helper.response({ + tag_notification: { id: "dev", notification_level: 2 }, + }); + }); + + server.get("/tags/c/bug/1/dev/l/latest.json", () => { + return helper.response({ + users: [], + primary_groups: [], + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 1, + per_page: 30, + tags: [ + { + id: 1, + name: "dev", + topic_count: 1, + }, + ], + topics: [], + }, + }); + }); + + server.get("/tags/intersection/dev/foo.json", () => { + return helper.response({ + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 1, + per_page: 30, + topics: [], + }, + }); + }); + server.get("/u/search/users", () => { return helper.response({ users: [ @@ -810,13 +851,92 @@ acceptance("Search - assistant", function (needs) { query( ".search-menu .results ul.search-menu-assistant .search-item-prefix" ).innerText, - "sam " + "sam" ); await click(firstCategory); assert.strictEqual(query("#search-term").value, `sam #${firstResultSlug}`); }); + test("Shows category / tag combination shortcut when both are present", async function (assert) { + await visit("/tags/c/bug/dev"); + await click("#search-button"); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .category-name") + .innerText, + "bug", + "Category is displayed" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "dev", + "Tag is displayed" + ); + }); + + test("Updates tag / category combination search suggestion when typing", async function (assert) { + await visit("/tags/c/bug/dev"); + await click("#search-button"); + await fillIn("#search-term", "foo bar"); + + assert.strictEqual( + query( + ".search-menu .results ul.search-menu-assistant .search-item-prefix" + ).innerText, + "foo bar", + "Input is applied to search query" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .category-name") + .innerText, + "bug" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "dev", + "Tag is displayed" + ); + }); + + test("Shows tag combination shortcut when visiting tag intersection", async function (assert) { + await visit("/tags/intersection/dev/foo"); + await click("#search-button"); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "tags:dev+foo", + "Tags are displayed" + ); + }); + + test("Updates tag intersection search suggestion when typing", async function (assert) { + await visit("/tags/intersection/dev/foo"); + await click("#search-button"); + await fillIn("#search-term", "foo bar"); + + assert.strictEqual( + query( + ".search-menu .results ul.search-menu-assistant .search-item-prefix" + ).innerText, + "foo bar", + "Input is applied to search query" + ); + + assert.strictEqual( + query(".search-menu .results ul.search-menu-assistant .search-item-tag") + .innerText, + "tags:dev+foo", + "Tags are displayed" + ); + }); + test("shows in: shortcuts", async function (assert) { await visit("/"); await click("#search-button"); diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss index 189cee2a63a..95a2353e031 100644 --- a/app/assets/stylesheets/common/base/search-menu.scss +++ b/app/assets/stylesheets/common/base/search-menu.scss @@ -208,9 +208,13 @@ $search-pad-horizontal: 0.5em; margin-top: 2px; } - .search-item-slug .badge-wrapper { - font-size: var(--font-0); - margin-left: 2px; + .search-item-slug { + margin-right: 5px; + + .badge-wrapper { + font-size: var(--font-0); + margin-left: 2px; + } } .search-menu-initial-options { @@ -225,7 +229,13 @@ $search-pad-horizontal: 0.5em; .search-menu-initial-options, .search-result-tag, .search-menu-assistant { + .search-item-prefix { + padding-right: 5px; + } .search-link { + display: flex; + flex-wrap: wrap; + align-items: center; @include ellipsis; .d-icon { margin-right: 5px;