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
This commit is contained in:
Ahmed Gagan 2020-09-11 16:45:50 +05:30 committed by GitHub
parent 3e3dc3815b
commit 303274eae3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 3 deletions

View File

@ -0,0 +1,12 @@
<div class="control-group pull-left">
<label class="control-label" for="search-assigned-to">{{i18n "search.advanced.assigned.label"}}</label>
<div class="controls">
{{user-selector
excludeCurrentUser=false
usernames=searchedTerms.assigned
single=true
canReceiveUpdates=true
class="user-selector-assigned"
}}
</div>
</div>

View File

@ -0,0 +1,5 @@
export default {
shouldRender(args, component) {
return component.currentUser && component.currentUser.can_assign;
},
};

View File

@ -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)
);

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"'
);
});
});