FEATURE: the ability to search users by custom fields (#12762)

FEATURE: the ability to search users by custom fields (#12762)

When the admin creates a new custom field they can specify if that field should be searchable or not.

That setting is taken into consideration for quick search results.

diff --git a/app/assets/javascripts/admin/addon/components/admin-user-field-item.js b/app/assets/javascripts/admin/addon/components/admin-user-field-item.js
index c9642e3..be2674e 100644
--- a/app/assets/javascripts/admin/addon/components/admin-user-field-item.js
+++ b/app/assets/javascripts/admin/addon/components/admin-user-field-item.js
@@ -44,25 +44,25 @@ export default Component.extend(bufferedProperty("userField"), {
   },
 
   @discourseComputed(
-    "userField.editable",
-    "userField.required",
-    "userField.show_on_profile",
-    "userField.show_on_user_card"
+    "userField.{editable,required,show_on_profile,show_on_user_card,searchable}"
   )
-  flags(editable, required, showOnProfile, showOnUserCard) {
+  flags(userField) {
     const ret = [];
-    if (editable) {
+    if (userField.editable) {
       ret.push(I18n.t("admin.user_fields.editable.enabled"));
     }
-    if (required) {
+    if (userField.required) {
       ret.push(I18n.t("admin.user_fields.required.enabled"));
     }
-    if (showOnProfile) {
+    if (userField.showOnProfile) {
       ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
     }
-    if (showOnUserCard) {
+    if (userField.showOnUserCard) {
       ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
     }
+    if (userField.searchable) {
+      ret.push(I18n.t("admin.user_fields.searchable.enabled"));
+    }
 
     return ret.join(", ");
   },
@@ -78,6 +78,7 @@ export default Component.extend(bufferedProperty("userField"), {
         "required",
         "show_on_profile",
         "show_on_user_card",
+        "searchable",
         "options"
       );
 
diff --git a/app/assets/javascripts/admin/addon/templates/components/admin-user-field-item.hbs b/app/assets/javascripts/admin/addon/templates/components/admin-user-field-item.hbs
index 211cc00..06ab7a2 100644
--- a/app/assets/javascripts/admin/addon/templates/components/admin-user-field-item.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/admin-user-field-item.hbs
@@ -22,19 +22,23 @@
   {{/if}}
 
   {{#admin-form-row wrapLabel="true"}}
-    {{input type="checkbox" checked=buffered.editable}} {{i18n "admin.user_fields.editable.title"}}
+    {{input type="checkbox" checked=buffered.editable}} <span>{{i18n "admin.user_fields.editable.title"}}</span>
   {{/admin-form-row}}
 
   {{#admin-form-row wrapLabel="true"}}
-    {{input type="checkbox" checked=buffered.required}} {{i18n "admin.user_fields.required.title"}}
+    {{input type="checkbox" checked=buffered.required}} <span>{{i18n "admin.user_fields.required.title"}}</span>
   {{/admin-form-row}}
 
   {{#admin-form-row wrapLabel="true"}}
-    {{input type="checkbox" checked=buffered.show_on_profile}} {{i18n "admin.user_fields.show_on_profile.title"}}
+    {{input type="checkbox" checked=buffered.show_on_profile}} <span>{{i18n "admin.user_fields.show_on_profile.title"}}</span>
   {{/admin-form-row}}
 
   {{#admin-form-row wrapLabel="true"}}
-    {{input type="checkbox" checked=buffered.show_on_user_card}} {{i18n "admin.user_fields.show_on_user_card.title"}}
+    {{input type="checkbox" checked=buffered.show_on_user_card}} <span>{{i18n "admin.user_fields.show_on_user_card.title"}}</span>
+  {{/admin-form-row}}
+
+  {{#admin-form-row wrapLabel="true"}}
+    {{input type="checkbox" checked=buffered.searchable}} <span>{{i18n "admin.user_fields.searchable.title"}}</span>
   {{/admin-form-row}}
 
   {{#admin-form-row}}
diff --git a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js
index aa22d04..57b89ef 100644
--- a/app/assets/javascripts/discourse/app/widgets/search-menu-results.js
+++ b/app/assets/javascripts/discourse/app/widgets/search-menu-results.js
@@ -129,6 +129,12 @@ createSearchResult({
 
     userTitles.push(h("span.username", formatUsername(u.username)));
 
+    if (u.custom_data) {
+      u.custom_data.forEach((row) =>
+        userTitles.push(h("span.custom-field", `${row.name}: ${row.value}`))
+      );
+    }
+
     const userResultContents = [
       avatarImg("small", {
         template: u.avatar_template,
diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss
index 0929477..4076e01 100644
--- a/app/assets/stylesheets/common/base/search-menu.scss
+++ b/app/assets/stylesheets/common/base/search-menu.scss
@@ -222,6 +222,11 @@
               font-size: $font-down-1;
             }
 
+            .custom-field {
+              color: var(--primary-high-or-secondary-low);
+              font-size: $font-down-2;
+            }
+
             .name {
               color: var(--primary-high-or-secondary-low);
               font-size: $font-0;
diff --git a/app/controllers/admin/user_fields_controller.rb b/app/controllers/admin/user_fields_controller.rb
index 26fa6a5..939ba7c 100644
--- a/app/controllers/admin/user_fields_controller.rb
+++ b/app/controllers/admin/user_fields_controller.rb
@@ -3,7 +3,7 @@
 class Admin::UserFieldsController < Admin::AdminController
 
   def self.columns
-    [:name, :field_type, :editable, :description, :required, :show_on_profile, :show_on_user_card, :position]
+    %i(name field_type editable description required show_on_profile show_on_user_card position searchable)
   end
 
   def create
diff --git a/app/models/user.rb b/app/models/user.rb
index 349cedf..e9c7af5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -37,6 +37,7 @@ class User < ActiveRecord::Base
   has_many :targeted_group_histories, dependent: :destroy, foreign_key: :target_user_id, class_name: 'GroupHistory'
   has_many :reviewable_scores, dependent: :destroy
   has_many :invites, foreign_key: :invited_by_id, dependent: :destroy
+  has_many :user_custom_fields, dependent: :destroy
 
   has_one :user_option, dependent: :destroy
   has_one :user_avatar, dependent: :destroy
@@ -183,6 +184,9 @@ class User < ActiveRecord::Base
   # set to true to optimize creation and save for imports
   attr_accessor :import_mode
 
+  # Cache for user custom fields. Currently it is used to display quick search results
+  attr_accessor :custom_data
+
   scope :with_email, ->(email) do
     joins(:user_emails).where("lower(user_emails.email) IN (?)", email)
   end
@@ -1421,7 +1425,8 @@ class User < ActiveRecord::Base
   end
 
   def index_search
-    SearchIndexer.index(self)
+    # force is needed as user custom fields are updated using SQL and after_save callback is not triggered
+    SearchIndexer.index(self, force: true)
   end
 
   def clear_global_notice_if_needed
diff --git a/app/models/user_custom_field.rb b/app/models/user_custom_field.rb
index c38e654..22f4d6d 100644
--- a/app/models/user_custom_field.rb
+++ b/app/models/user_custom_field.rb
@@ -2,6 +2,8 @@
 
 class UserCustomField < ActiveRecord::Base
   belongs_to :user
+
+  scope :searchable, -> { joins("INNER JOIN user_fields ON user_fields.id = REPLACE(user_custom_fields.name, 'user_field_', '')::INTEGER AND user_fields.searchable IS TRUE AND user_custom_fields.name like 'user_field_%'") }
 end
 
 # == Schema Information
diff --git a/app/models/user_field.rb b/app/models/user_field.rb
index 1fcbd0e..9adc5b3 100644
--- a/app/models/user_field.rb
+++ b/app/models/user_field.rb
@@ -9,9 +9,15 @@ class UserField < ActiveRecord::Base
   has_many :user_field_options, dependent: :destroy
   accepts_nested_attributes_for :user_field_options
 
+  after_save :queue_index_search
+
   def self.max_length
     2048
   end
+
+  def queue_index_search
+    SearchIndexer.queue_users_reindex(UserCustomField.where(name: "user_field_#{self.id}").pluck(:user_id))
+  end
 end
 
 # == Schema Information
@@ -31,4 +37,5 @@ end
 #  show_on_user_card :boolean          default(FALSE), not null
 #  external_name     :string
 #  external_type     :string
+#  searchable        :boolean          default(FALSE), not null
 #

[... diff too long, it was truncated ...]

GitHub sha: e29605b7

This commit appears in #12762 which was approved by jjaffeux. It was merged by lis2.