diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index f4aa7897fb3..783b14e1271 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -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. * diff --git a/app/assets/javascripts/discourse/app/lib/user-search.js b/app/assets/javascripts/discourse/app/lib/user-search.js index c16e74bda4c..6b6900db794 100644 --- a/app/assets/javascripts/discourse/app/lib/user-search.js +++ b/app/assets/javascripts/discourse/app/lib/user-search.js @@ -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, diff --git a/app/assets/javascripts/select-kit/addon/components/user-chooser.js b/app/assets/javascripts/select-kit/addon/components/user-chooser.js index 9d3f90c00ca..a364c046e2c 100644 --- a/app/assets/javascripts/select-kit/addon/components/user-chooser.js +++ b/app/assets/javascripts/select-kit/addon/components/user-chooser.js @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0835db62eef..0b1a266ada9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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| diff --git a/app/models/group.rb b/app/models/group.rb index d863b43d86b..4742e4b32e0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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}%" ) diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 625e7fe798b..28a5c717270 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -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 diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 28fac3753ff..fcd25b1025c 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9e3dc1b869d..c72f6f869d9 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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 diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 812a2e6539a..b9aa3a23d62 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -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',