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 { addSearchResultsCallback } from "discourse/lib/search";
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
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.
function canModify(klass, type, resolverName, changes) {
@ -1407,6 +1408,29 @@ class PluginApi {
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.
*

View File

@ -20,14 +20,18 @@ export function resetUserSearchCache() {
oldSearch = null;
}
export function camelCaseToSnakeCase(text) {
return text.replace(/([a-zA-Z])(?=[A-Z])/g, "$1_").toLowerCase();
}
function performSearch(
term,
topicId,
categoryId,
includeGroups,
customGroupsScope,
includeMentionableGroups,
includeMessageableGroups,
customUserSearchOptions,
allowedUsers,
groupMembersOf,
includeStagedUsers,
@ -50,22 +54,29 @@ function performSearch(
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
oldSearch = $.ajax(userPath("search/users"), {
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,
},
data,
});
let returnVal = CANCELLED_STATUS;
@ -102,9 +113,9 @@ let debouncedSearch = function (
topicId,
categoryId,
includeGroups,
customGroupsScope,
includeMentionableGroups,
includeMessageableGroups,
customUserSearchOptions,
allowedUsers,
groupMembersOf,
includeStagedUsers,
@ -119,9 +130,9 @@ let debouncedSearch = function (
topicId,
categoryId,
includeGroups,
customGroupsScope,
includeMentionableGroups,
includeMessageableGroups,
customUserSearchOptions,
allowedUsers,
groupMembersOf,
includeStagedUsers,
@ -211,9 +222,9 @@ export default function userSearch(options) {
let term = options.term || "",
includeGroups = options.includeGroups,
customGroupsScope = options.customGroupsScope,
includeMentionableGroups = options.includeMentionableGroups,
includeMessageableGroups = options.includeMessageableGroups,
customUserSearchOptions = options.customUserSearchOptions,
allowedUsers = options.allowedUsers,
topicId = options.topicId,
categoryId = options.categoryId,
@ -253,9 +264,9 @@ export default function userSearch(options) {
topicId,
categoryId,
includeGroups,
customGroupsScope,
includeMentionableGroups,
includeMessageableGroups,
customUserSearchOptions,
allowedUsers,
groupMembersOf,
includeStagedUsers,

View File

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

View File

@ -1123,12 +1123,13 @@ class UsersController < ApplicationController
end
if groups
groups = Group.search_groups(term,
groups: groups,
custom_scope: {
name: params["custom_groups_scope"]&.to_sym,
arguments: [current_user]
})
DiscoursePluginRegistry.groups_callback_for_users_search_controller_action.each do |param_name, block|
if params[param_name.to_s]
groups = block.call(groups, current_user)
end
end
groups = Group.search_groups(term, groups: groups)
groups = groups.order('groups.name asc')
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: {})
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(
"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 :seedfu_filter, 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 :public_user_custom_fields
@ -77,7 +78,6 @@ class DiscoursePluginRegistry
define_filtered_register :editable_group_custom_fields
define_filtered_register :group_params
define_filtered_register :group_scope_for_search
define_filtered_register :topic_thumbnail_sizes

View File

@ -375,9 +375,19 @@ class Plugin::Instance
DiscoursePluginRegistry.register_group_param(param, self)
end
# Add a custom scopes for search to Group, respecting if the plugin is enabled
def register_group_scope_for_search(scope_name)
DiscoursePluginRegistry.register_group_scope_for_search(scope_name, self)
# Add a custom callback for search to Group
# Callback is called in UsersController#search_users
# 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
# 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('test2')).to eq([])
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
describe '#bulk_add' do

View File

@ -4083,6 +4083,25 @@ describe UsersController do
.to_not include(private_group.name)
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
get "/u/search/users.json", params: {
include_mentionable_groups: 'false',