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:
parent
3e3dc3815b
commit
303274eae3
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default {
|
||||||
|
shouldRender(args, component) {
|
||||||
|
return component.currentUser && component.currentUser.can_assign;
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,12 +1,14 @@
|
||||||
import { renderAvatar } from "discourse/helpers/user-avatar";
|
import { renderAvatar } from "discourse/helpers/user-avatar";
|
||||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import computed from "discourse-common/utils/decorators";
|
import computed from "discourse-common/utils/decorators";
|
||||||
|
import { observes } from "discourse-common/utils/decorators";
|
||||||
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
|
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
|
||||||
import { h } from "virtual-dom";
|
import { h } from "virtual-dom";
|
||||||
import { queryRegistry } from "discourse/widgets/widget";
|
import { queryRegistry } from "discourse/widgets/widget";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
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 { addBulkButton } from "discourse/controllers/topic-bulk-actions";
|
||||||
import TopicButtonAction from "discourse/controllers/topic-bulk-actions";
|
import TopicButtonAction from "discourse/controllers/topic-bulk-actions";
|
||||||
import { inject } from "@ember/controller";
|
import { inject } from "@ember/controller";
|
||||||
|
@ -122,6 +124,23 @@ function initialize(api) {
|
||||||
before: "top",
|
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
|
// You can't act on flags claimed by another user
|
||||||
api.modifyClass(
|
api.modifyClass(
|
||||||
"component:flagged-post",
|
"component:flagged-post",
|
||||||
|
@ -315,6 +334,8 @@ function initialize(api) {
|
||||||
api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" });
|
api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REGEXP_USERNAME_PREFIX = /^(assigned:)/gi;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "extend-for-assign",
|
name: "extend-for-assign",
|
||||||
initialize(container) {
|
initialize(container) {
|
||||||
|
@ -322,9 +343,47 @@ export default {
|
||||||
if (!siteSettings.assign_enabled) {
|
if (!siteSettings.assign_enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUser = container.lookup("current-user:main");
|
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({
|
TopicButtonAction.reopen({
|
||||||
assignUser: inject("assign-user"),
|
assignUser: inject("assign-user"),
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -347,7 +406,8 @@ export default {
|
||||||
class: "btn-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) =>
|
withPluginApi("0.8.28", (api) =>
|
||||||
registerTopicFooterButtons(api, container)
|
registerTopicFooterButtons(api, container)
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,6 +53,13 @@ en:
|
||||||
assign_event:
|
assign_event:
|
||||||
name: "Assign Event"
|
name: "Assign Event"
|
||||||
details: "When a user assigns or unassigns a topic."
|
details: "When a user assigns or unassigns a topic."
|
||||||
|
search:
|
||||||
|
advanced:
|
||||||
|
in:
|
||||||
|
assigned: "are assigned"
|
||||||
|
unassigned: "are unassigned"
|
||||||
|
assigned:
|
||||||
|
label: "Assigned to"
|
||||||
topics:
|
topics:
|
||||||
bulk:
|
bulk:
|
||||||
unassign: "Unassign Topics"
|
unassign: "Unassign Topics"
|
||||||
|
|
36
plugin.rb
36
plugin.rb
|
@ -544,4 +544,40 @@ after_initialize do
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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"'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue