From 303274eae3c6012ee6178c347d834aed40fe3906 Mon Sep 17 00:00:00 2001 From: Ahmed Gagan Date: Fri, 11 Sep 2020 16:45:50 +0530 Subject: [PATCH] FEATURE: Advanced search filters for assigned topics (#102) Adds three new search modifiers: - in:assigned for assigned topics - in:unassigned for unassigned topics - assigned:{username} to list topics assigned to a specific user These modifiers are all made available in the advanced search sidebar --- .../assigned-advanced-search.hbs | 12 +++ .../assigned-advanced-search.js.es6 | 5 + .../initializers/extend-for-assigns.js.es6 | 66 +++++++++++++- config/locales/client.en.yml | 7 ++ plugin.rb | 36 ++++++++ spec/components/search_spec.rb | 42 +++++++++ .../acceptance/search-full-test.js.es6 | 91 +++++++++++++++++++ 7 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.hbs create mode 100644 assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.js.es6 create mode 100644 spec/components/search_spec.rb create mode 100644 test/javascripts/acceptance/search-full-test.js.es6 diff --git a/assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.hbs b/assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.hbs new file mode 100644 index 0000000..b3fe0a6 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.hbs @@ -0,0 +1,12 @@ +
+ +
+ {{user-selector + excludeCurrentUser=false + usernames=searchedTerms.assigned + single=true + canReceiveUpdates=true + class="user-selector-assigned" + }} +
+
diff --git a/assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.js.es6 b/assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.js.es6 new file mode 100644 index 0000000..3cdd800 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/advanced-search-options-below/assigned-advanced-search.js.es6 @@ -0,0 +1,5 @@ +export default { + shouldRender(args, component) { + return component.currentUser && component.currentUser.can_assign; + }, +}; diff --git a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 index 5ce0e70..3956e82 100644 --- a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 +++ b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 @@ -1,12 +1,14 @@ import { renderAvatar } from "discourse/helpers/user-avatar"; import { withPluginApi } from "discourse/lib/plugin-api"; import computed from "discourse-common/utils/decorators"; +import { observes } from "discourse-common/utils/decorators"; import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; import { h } from "virtual-dom"; import { queryRegistry } from "discourse/widgets/widget"; import { getOwner } from "discourse-common/lib/get-owner"; import { htmlSafe } from "@ember/template"; import getURL from "discourse-common/lib/get-url"; +import SearchAdvancedOptions from "discourse/components/search-advanced-options"; import { addBulkButton } from "discourse/controllers/topic-bulk-actions"; import TopicButtonAction from "discourse/controllers/topic-bulk-actions"; import { inject } from "@ember/controller"; @@ -122,6 +124,23 @@ function initialize(api) { before: "top", }); + api.addAdvancedSearchOptions( + api.getCurrentUser() && api.getCurrentUser().can_assign + ? { + inOptionsForUsers: [ + { + name: I18n.t("search.advanced.in.assigned"), + value: "assigned", + }, + { + name: I18n.t("search.advanced.in.unassigned"), + value: "unassigned", + }, + ], + } + : {} + ); + // You can't act on flags claimed by another user api.modifyClass( "component:flagged-post", @@ -315,6 +334,8 @@ function initialize(api) { api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" }); } +const REGEXP_USERNAME_PREFIX = /^(assigned:)/gi; + export default { name: "extend-for-assign", initialize(container) { @@ -322,9 +343,47 @@ export default { if (!siteSettings.assign_enabled) { return; } - const currentUser = container.lookup("current-user:main"); - if (currentUser.can_assign) { + if (currentUser && currentUser.can_assign) { + SearchAdvancedOptions.reopen({ + _init() { + this._super(); + + this.set("searchedTerms.assigned", ""); + }, + + @observes("searchedTerms.assigned") + updateSearchTermForAssignedUsername() { + const match = this.filterBlocks(REGEXP_USERNAME_PREFIX); + const userFilter = this.get("searchedTerms.assigned"); + let searchTerm = this.searchTerm || ""; + let keyword = "assigned"; + if (userFilter && userFilter.length !== 0) { + if (match.length !== 0) { + searchTerm = searchTerm.replace( + match[0], + `${keyword}:${userFilter}` + ); + } else { + searchTerm += ` ${keyword}:${userFilter}`; + } + + this.set("searchTerm", searchTerm.trim()); + } else if (match.length !== 0) { + searchTerm = searchTerm.replace(match[0], ""); + this.set("searchTerm", searchTerm.trim()); + } + }, + + _update() { + this._super(...arguments); + this.setSearchedTermValue( + "searchedTerms.assigned", + REGEXP_USERNAME_PREFIX + ); + }, + }); + TopicButtonAction.reopen({ assignUser: inject("assign-user"), actions: { @@ -347,7 +406,8 @@ export default { class: "btn-default", }); } - withPluginApi("0.8.11", (api) => initialize(api, container)); + + withPluginApi("0.11.0", (api) => initialize(api, container)); withPluginApi("0.8.28", (api) => registerTopicFooterButtons(api, container) ); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index db73b26..1d0e329 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -53,6 +53,13 @@ en: assign_event: name: "Assign Event" details: "When a user assigns or unassigns a topic." + search: + advanced: + in: + assigned: "are assigned" + unassigned: "are unassigned" + assigned: + label: "Assigned to" topics: bulk: unassign: "Unassign Topics" diff --git a/plugin.rb b/plugin.rb index 8c7acdf..f13fda2 100644 --- a/plugin.rb +++ b/plugin.rb @@ -544,4 +544,40 @@ after_initialize do end end + register_search_advanced_filter(/in:assigned/) do |posts| + if @guardian.can_assign? + posts.where("topics.id IN ( + SELECT tc.topic_id + FROM topic_custom_fields tc + WHERE tc.name = 'assigned_to_id' AND + tc.value IS NOT NULL + )") + end + end + + register_search_advanced_filter(/in:unassigned/) do |posts| + if @guardian.can_assign? + posts.where("topics.id NOT IN ( + SELECT tc.topic_id + FROM topic_custom_fields tc + WHERE tc.name = 'assigned_to_id' AND + tc.value IS NOT NULL + )") + end + end + + register_search_advanced_filter(/assigned:(.+)$/) do |posts, match| + if @guardian.can_assign? + if user_id = User.find_by_username(match)&.id + posts.where("topics.id IN ( + SELECT tc.topic_id + FROM topic_custom_fields tc + WHERE tc.name = 'assigned_to_id' AND + tc.value IS NOT NULL AND + tc.value::int = #{user_id} + )") + end + end + end + end diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb new file mode 100644 index 0000000..06eeb50 --- /dev/null +++ b/spec/components/search_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' +require_relative '../support/assign_allowed_group' + +describe Search do + + fab!(:user) { Fabricate(:active_user) } + fab!(:user2) { Fabricate(:user) } + + before do + SearchIndexer.enable + SiteSetting.assign_enabled = true + end + + context 'Advanced search' do + include_context 'A group that is allowed to assign' + + let(:post1) { Fabricate(:post) } + let(:post2) { Fabricate(:post) } + let(:post3) { Fabricate(:post) } + + before do + add_to_assign_allowed_group(user) + add_to_assign_allowed_group(user2) + + TopicAssigner.new(post1.topic, user).assign(user) + TopicAssigner.new(post2.topic, user).assign(user2) + TopicAssigner.new(post3.topic, user).assign(user) + end + + it 'can find by status' do + expect(Search.execute('in:assigned', guardian: Guardian.new(user)).posts.length).to eq(3) + + TopicAssigner.new(post3.topic, user).unassign + + expect(Search.execute('in:unassigned', guardian: Guardian.new(user)).posts.length).to eq(1) + expect(Search.execute("assigned:#{user.username}", guardian: Guardian.new(user)).posts.length).to eq(1) + end + + end +end diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6 new file mode 100644 index 0000000..265e950 --- /dev/null +++ b/test/javascripts/acceptance/search-full-test.js.es6 @@ -0,0 +1,91 @@ +import selectKit from "helpers/select-kit-helper"; +import { acceptance, waitFor, updateCurrentUser } from "helpers/qunit-helpers"; + +acceptance("Search - Full Page", { + settings: { assign_enabled: true }, + loggedIn: true, +}); +QUnit.test( + "update in:assigned filter through advanced search ui", + async (assert) => { + updateCurrentUser({ can_assign: true }); + const inSelector = selectKit(".search-advanced-options .select-kit#in"); + + await visit("/search"); + + await fillIn(".search-query", "none"); + await inSelector.expand(); + await inSelector.selectRowByValue("assigned"); + assert.equal( + inSelector.header().label(), + "are assigned", + 'has "are assigned" populated' + ); + assert.equal( + find(".search-query").val(), + "none in:assigned", + 'has updated search term to "none in:assinged"' + ); + } +); + +QUnit.test( + "update in:unassigned filter through advanced search ui", + async (assert) => { + updateCurrentUser({ can_assign: true }); + const inSelector = selectKit(".search-advanced-options .select-kit#in"); + + await visit("/search"); + + await fillIn(".search-query", "none"); + await inSelector.expand(); + await inSelector.selectRowByValue("unassigned"); + assert.equal( + inSelector.header().label(), + "are unassigned", + 'has "are unassigned" populated' + ); + assert.equal( + find(".search-query").val(), + "none in:unassigned", + 'has updated search term to "none in:unassinged"' + ); + } +); + +QUnit.skip("update assigned to through advanced search ui", async (assert) => { + updateCurrentUser({ can_assign: true }); + await visit("/search"); + await fillIn(".search-query", "none"); + await fillIn(".search-advanced-options .user-selector-assigned", "admin"); + await click(".search-advanced-options .user-selector-assigned"); + await keyEvent( + ".search-advanced-options .user-selector-assigned", + "keydown", + 8 + ); + waitFor(assert, async () => { + assert.ok( + visible(".search-advanced-options .autocomplete"), + '"autocomplete" popup is visible' + ); + assert.ok( + exists( + '.search-advanced-options .autocomplete ul li a span.username:contains("admin")' + ), + '"autocomplete" popup has an entry for "admin"' + ); + + await click(".search-advanced-options .autocomplete ul li a:first"); + + assert.ok( + exists('.search-advanced-options span:contains("admin")'), + 'has "admin" pre-populated' + ); + assert.equal( + find(".search-query").val(), + "none assigned:admin", + 'has updated search term to "none assigned:admin"' + ); + }); +});