FIX: better filter for groups search (#14262)

Follow up of https://github.com/discourse/discourse/pull/14216

Allow plugins to register custom filter with block
This commit is contained in:
Krzysztof Kotlarek 2021-09-08 09:38:45 +10:00 committed by GitHub
parent cddba50570
commit e3793e6d7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 47 deletions

View File

@ -83,9 +83,10 @@ import { replaceTagRenderer } from "discourse/lib/render-tag";
import { setNewCategoryDefaultColors } from "discourse/routes/new-category"; import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
import { addSearchResultsCallback } from "discourse/lib/search"; import { addSearchResultsCallback } from "discourse/lib/search";
import { addSearchSuggestion } from "discourse/widgets/search-menu-results"; import { addSearchSuggestion } from "discourse/widgets/search-menu-results";
import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
// If you add any methods to the API ensure you bump up this number // If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = "0.12.2"; const PLUGIN_API_VERSION = "0.12.3";
// This helper prevents us from applying the same `modifyClass` over and over in test mode. // This helper prevents us from applying the same `modifyClass` over and over in test mode.
function canModify(klass, type, resolverName, changes) { function canModify(klass, type, resolverName, changes) {
@ -1407,6 +1408,29 @@ class PluginApi {
addSearchSuggestion(value); addSearchSuggestion(value);
} }
/**
* Add custom user search options.
* It is heavily correlated with `register_groups_callback_for_users_search_controller_action` which allows defining custom filter.
* Example usage:
* ```
* api.addUserSearchOption("adminsOnly");
* register_groups_callback_for_users_search_controller_action(:admins_only) do |groups, user|
* groups.where(name: "admins")
* end
*
* {{email-group-user-chooser
* options=(hash
* includeGroups=true
* adminsOnly=true
* )
* }}
* ```
*/
addUserSearchOption(value) {
CUSTOM_USER_SEARCH_OPTIONS.push(value);
}
/** /**
* Calls a method on a mounted widget whenever an app event happens. * Calls a method on a mounted widget whenever an app event happens.
* *

View File

@ -20,14 +20,18 @@ export function resetUserSearchCache() {
oldSearch = null; oldSearch = null;
} }
export function camelCaseToSnakeCase(text) {
return text.replace(/([a-zA-Z])(?=[A-Z])/g, "$1_").toLowerCase();
}
function performSearch( function performSearch(
term, term,
topicId, topicId,
categoryId, categoryId,
includeGroups, includeGroups,
customGroupsScope,
includeMentionableGroups, includeMentionableGroups,
includeMessageableGroups, includeMessageableGroups,
customUserSearchOptions,
allowedUsers, allowedUsers,
groupMembersOf, groupMembersOf,
includeStagedUsers, includeStagedUsers,
@ -50,22 +54,29 @@ function performSearch(
return; return;
} }
let data = {
term: term,
topic_id: topicId,
category_id: categoryId,
include_groups: includeGroups,
include_mentionable_groups: includeMentionableGroups,
include_messageable_groups: includeMessageableGroups,
groups: groupMembersOf,
topic_allowed_users: allowedUsers,
include_staged_users: includeStagedUsers,
last_seen_users: lastSeenUsers,
limit: limit,
};
if (customUserSearchOptions) {
Object.keys(customUserSearchOptions).forEach((key) => {
data[camelCaseToSnakeCase(key)] = customUserSearchOptions[key];
});
}
// need to be able to cancel this // need to be able to cancel this
oldSearch = $.ajax(userPath("search/users"), { oldSearch = $.ajax(userPath("search/users"), {
data: { data,
term: term,
topic_id: topicId,
category_id: categoryId,
include_groups: includeGroups,
custom_groups_scope: customGroupsScope,
include_mentionable_groups: includeMentionableGroups,
include_messageable_groups: includeMessageableGroups,
groups: groupMembersOf,
topic_allowed_users: allowedUsers,
include_staged_users: includeStagedUsers,
last_seen_users: lastSeenUsers,
limit: limit,
},
}); });
let returnVal = CANCELLED_STATUS; let returnVal = CANCELLED_STATUS;
@ -102,9 +113,9 @@ let debouncedSearch = function (
topicId, topicId,
categoryId, categoryId,
includeGroups, includeGroups,
customGroupsScope,
includeMentionableGroups, includeMentionableGroups,
includeMessageableGroups, includeMessageableGroups,
customUserSearchOptions,
allowedUsers, allowedUsers,
groupMembersOf, groupMembersOf,
includeStagedUsers, includeStagedUsers,
@ -119,9 +130,9 @@ let debouncedSearch = function (
topicId, topicId,
categoryId, categoryId,
includeGroups, includeGroups,
customGroupsScope,
includeMentionableGroups, includeMentionableGroups,
includeMessageableGroups, includeMessageableGroups,
customUserSearchOptions,
allowedUsers, allowedUsers,
groupMembersOf, groupMembersOf,
includeStagedUsers, includeStagedUsers,
@ -211,9 +222,9 @@ export default function userSearch(options) {
let term = options.term || "", let term = options.term || "",
includeGroups = options.includeGroups, includeGroups = options.includeGroups,
customGroupsScope = options.customGroupsScope,
includeMentionableGroups = options.includeMentionableGroups, includeMentionableGroups = options.includeMentionableGroups,
includeMessageableGroups = options.includeMessageableGroups, includeMessageableGroups = options.includeMessageableGroups,
customUserSearchOptions = options.customUserSearchOptions,
allowedUsers = options.allowedUsers, allowedUsers = options.allowedUsers,
topicId = options.topicId, topicId = options.topicId,
categoryId = options.categoryId, categoryId = options.categoryId,
@ -253,9 +264,9 @@ export default function userSearch(options) {
topicId, topicId,
categoryId, categoryId,
includeGroups, includeGroups,
customGroupsScope,
includeMentionableGroups, includeMentionableGroups,
includeMessageableGroups, includeMessageableGroups,
customUserSearchOptions,
allowedUsers, allowedUsers,
groupMembersOf, groupMembersOf,
includeStagedUsers, includeStagedUsers,

View File

@ -6,6 +6,8 @@ import MultiSelectComponent from "select-kit/components/multi-select";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers"; import { makeArray } from "discourse-common/lib/helpers";
export const CUSTOM_USER_SEARCH_OPTIONS = [];
export default MultiSelectComponent.extend({ export default MultiSelectComponent.extend({
pluginApiIdentifiers: ["user-chooser"], pluginApiIdentifiers: ["user-chooser"],
classNames: ["user-chooser"], classNames: ["user-chooser"],
@ -64,19 +66,29 @@ export default MultiSelectComponent.extend({
return; return;
} }
let customUserSearchOptions = CUSTOM_USER_SEARCH_OPTIONS.reduce(
(obj, option) => {
return {
...obj,
[option]: options[option],
};
},
{}
);
return userSearch({ return userSearch({
term: filter, term: filter,
topicId: options.topicId, topicId: options.topicId,
categoryId: options.categoryId, categoryId: options.categoryId,
exclude: this.excludedUsers, exclude: this.excludedUsers,
includeGroups: options.includeGroups, includeGroups: options.includeGroups,
customGroupsScope: options.customGroupsScope,
allowedUsers: options.allowedUsers, allowedUsers: options.allowedUsers,
includeMentionableGroups: options.includeMentionableGroups, includeMentionableGroups: options.includeMentionableGroups,
includeMessageableGroups: options.includeMessageableGroups, includeMessageableGroups: options.includeMessageableGroups,
groupMembersOf: options.groupMembersOf, groupMembersOf: options.groupMembersOf,
allowEmails: options.allowEmails, allowEmails: options.allowEmails,
includeStagedUsers: this.includeStagedUsers, includeStagedUsers: this.includeStagedUsers,
customUserSearchOptions,
}).then((result) => { }).then((result) => {
if (typeof result === "string") { if (typeof result === "string") {
// do nothing promise probably got cancelled // do nothing promise probably got cancelled

View File

@ -1123,12 +1123,13 @@ class UsersController < ApplicationController
end end
if groups if groups
groups = Group.search_groups(term, DiscoursePluginRegistry.groups_callback_for_users_search_controller_action.each do |param_name, block|
groups: groups, if params[param_name.to_s]
custom_scope: { groups = block.call(groups, current_user)
name: params["custom_groups_scope"]&.to_sym, end
arguments: [current_user] end
})
groups = Group.search_groups(term, groups: groups)
groups = groups.order('groups.name asc') groups = groups.order('groups.name asc')
to_render[:groups] = groups.map do |m| to_render[:groups] = groups.map do |m|

View File

@ -560,10 +560,6 @@ class Group < ActiveRecord::Base
def self.search_groups(name, groups: nil, custom_scope: {}) def self.search_groups(name, groups: nil, custom_scope: {})
groups ||= Group groups ||= Group
if custom_scope.present? && DiscoursePluginRegistry.group_scope_for_search.include?(custom_scope[:name])
groups = groups.send(custom_scope[:name], *custom_scope[:arguments])
end
groups.where( groups.where(
"name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%" "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%"
) )

View File

@ -68,6 +68,7 @@ class DiscoursePluginRegistry
define_register :vendored_core_pretty_text, Set define_register :vendored_core_pretty_text, Set
define_register :seedfu_filter, Set define_register :seedfu_filter, Set
define_register :demon_processes, Set define_register :demon_processes, Set
define_register :groups_callback_for_users_search_controller_action, Hash
define_filtered_register :staff_user_custom_fields define_filtered_register :staff_user_custom_fields
define_filtered_register :public_user_custom_fields define_filtered_register :public_user_custom_fields
@ -77,7 +78,6 @@ class DiscoursePluginRegistry
define_filtered_register :editable_group_custom_fields define_filtered_register :editable_group_custom_fields
define_filtered_register :group_params define_filtered_register :group_params
define_filtered_register :group_scope_for_search
define_filtered_register :topic_thumbnail_sizes define_filtered_register :topic_thumbnail_sizes

View File

@ -375,9 +375,19 @@ class Plugin::Instance
DiscoursePluginRegistry.register_group_param(param, self) DiscoursePluginRegistry.register_group_param(param, self)
end end
# Add a custom scopes for search to Group, respecting if the plugin is enabled # Add a custom callback for search to Group
def register_group_scope_for_search(scope_name) # Callback is called in UsersController#search_users
DiscoursePluginRegistry.register_group_scope_for_search(scope_name, self) # Block takes groups and optional current_user
# For example:
# plugin.register_groups_callback_for_users_search_controller_action(:admins_filter) do |groups, user|
# groups.where(name: "admins")
# end
def register_groups_callback_for_users_search_controller_action(callback, &block)
if DiscoursePluginRegistry.groups_callback_for_users_search_controller_action.key?(callback)
raise "groups_callback_for_users_search_controller_action callback already registered"
end
DiscoursePluginRegistry.groups_callback_for_users_search_controller_action[callback] = block
end end
# Add validation method but check that the plugin is enabled # Add validation method but check that the plugin is enabled

View File

@ -935,18 +935,6 @@ describe Group do
expect(Group.search_groups('sOmEthi')).to eq([group]) expect(Group.search_groups('sOmEthi')).to eq([group])
expect(Group.search_groups('test2')).to eq([]) expect(Group.search_groups('test2')).to eq([])
end end
it 'allows to filter with additional scope' do
messageable_group
expect(Group.search_groups('es', custom_scope: { name: :messageable, arguments: [user] }).sort).to eq([messageable_group, group].sort)
plugin = Plugin::Instance.new
plugin.register_group_scope_for_search(:messageable)
expect(Group.search_groups('es', custom_scope: { name: :messageable, arguments: [user] }).sort).to eq([messageable_group].sort)
DiscoursePluginRegistry.reset!
end
end end
describe '#bulk_add' do describe '#bulk_add' do

View File

@ -4083,6 +4083,25 @@ describe UsersController do
.to_not include(private_group.name) .to_not include(private_group.name)
end end
it 'allows plugins to register custom groups filter' do
get "/u/search/users.json", params: { include_groups: "true", term: "a" }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups.count).to eq(6)
plugin = Plugin::Instance.new
plugin.register_groups_callback_for_users_search_controller_action(:admins_filter) do |original_groups, user|
original_groups.where(name: "admins")
end
get "/u/search/users.json", params: { include_groups: "true", admins_filter: "true", term: "a" }
expect(response.status).to eq(200)
groups = response.parsed_body["groups"]
expect(groups).to eq([{ "name" => "admins", "full_name" => nil }])
DiscoursePluginRegistry.reset!
end
it "doesn't search for groups" do it "doesn't search for groups" do
get "/u/search/users.json", params: { get "/u/search/users.json", params: {
include_mentionable_groups: 'false', include_mentionable_groups: 'false',