diff --git a/grade/classes/external/get_enrolled_users_for_search_widget.php b/grade/classes/external/get_enrolled_users_for_search_widget.php
index 09faf68770d..74a17e8069c 100644
--- a/grade/classes/external/get_enrolled_users_for_search_widget.php
+++ b/grade/classes/external/get_enrolled_users_for_search_widget.php
@@ -101,6 +101,9 @@ class get_enrolled_users_for_search_widget extends external_api {
$users = [];
+ $userfieldsapi = \core_user\fields::for_identity($coursecontext, false)->with_userpic();
+ $extrauserfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
+
while ($userdata = $gui->next_user()) {
$guiuser = $userdata->user;
$user = new \stdClass();
@@ -110,7 +113,9 @@ class get_enrolled_users_for_search_widget extends external_api {
$userpicture = new \user_picture($guiuser);
$userpicture->size = 1;
$user->profileimage = $userpicture->get_url($PAGE)->out(false);
- $user->email = $guiuser->email;
+ foreach ($extrauserfields as $field) {
+ $user->$field = $userdata->user->$field ?? null;
+ }
$user->active = false;
$users[] = $user;
diff --git a/grade/classes/external/get_enrolled_users_for_selector.php b/grade/classes/external/get_enrolled_users_for_selector.php
index 9639db4db6d..4b547d6999d 100644
--- a/grade/classes/external/get_enrolled_users_for_selector.php
+++ b/grade/classes/external/get_enrolled_users_for_selector.php
@@ -95,16 +95,23 @@ class get_enrolled_users_for_selector extends external_api {
$users = [];
- while ($userdata = $gui->next_user()) {
- $user = $userdata->user;
- $user->fullname = fullname($user);
- $userpicture = new user_picture($user);
- $userpicture->size = 1;
- $user->profileimageurl = $userpicture->get_url($PAGE)->out(false);
- $userpicture->size = 0; // Size f2.
- $user->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
+ $userfieldsapi = \core_user\fields::for_identity($coursecontext, false)->with_userpic();
+ $extrauserfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
- $users[] = $user;
+ while ($userdata = $gui->next_user()) {
+ $userforselector = new \stdClass();
+ $userforselector->id = $userdata->user->id;
+ $userforselector->fullname = fullname($userdata->user);
+ $userpicture = new user_picture($userdata->user);
+ $userpicture->size = 1;
+ $userforselector->profileimageurl = $userpicture->get_url($PAGE)->out(false);
+ $userpicture->size = 0; // Size f2.
+ $userforselector->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
+ foreach ($extrauserfields as $field) {
+ $userforselector->$field = $userdata->user->$field ?? null;
+ }
+
+ $users[] = $userforselector;
}
$gui->close();
diff --git a/grade/report/grader/classes/external/get_users_in_report.php b/grade/report/grader/classes/external/get_users_in_report.php
index d58e8b604dc..16a7e4c2b58 100644
--- a/grade/report/grader/classes/external/get_users_in_report.php
+++ b/grade/report/grader/classes/external/get_users_in_report.php
@@ -87,15 +87,23 @@ class get_users_in_report extends external_api {
);
$report = new grade_report_grader($courseid, $gpr, $context);
+ $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic();
+ $extrauserfields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
+
// For the returned users, Add a couple of extra fields that we need for the search module.
- $users = array_map(function ($user) use ($PAGE) {
- $user->fullname = fullname($user);
+ $users = array_map(function ($user) use ($PAGE, $extrauserfields) {
+ $userforselector = new \stdClass();
+ $userforselector->id = $user->id;
+ $userforselector->fullname = fullname($user);
$userpicture = new user_picture($user);
$userpicture->size = 1;
- $user->profileimageurl = $userpicture->get_url($PAGE)->out(false);
+ $userforselector->profileimageurl = $userpicture->get_url($PAGE)->out(false);
$userpicture->size = 0; // Size f2.
- $user->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
- return $user;
+ $userforselector->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
+ foreach ($extrauserfields as $field) {
+ $userforselector->$field = $user->$field ?? null;
+ }
+ return $userforselector;
}, $report->load_users(true));
sort($users);
diff --git a/grade/report/user/tests/behat/usersearch.feature b/grade/report/user/tests/behat/usersearch.feature
index 370acd7a3d2..cf8c950f7e4 100644
--- a/grade/report/user/tests/behat/usersearch.feature
+++ b/grade/report/user/tests/behat/usersearch.feature
@@ -196,6 +196,26 @@ Feature: Within the User report, a teacher can search for users.
And "Turtle Manatee" "heading" should exist
And "Teacher 1" "heading" should not exist
+ Scenario: A teacher can only search for fields that he allowed to see
+ Given the following "permission overrides" exist:
+ | capability | permission | role | contextlevel | reference |
+ | moodle/course:viewhiddenuserfields | Prohibit | editingteacher | System | |
+ And the following config values are set as admin:
+ | hiddenuserfields | email |
+ And I am on the "Course 1" "grades > User report > View" page logged in as "teacher1"
+ When I set the field "Search users" to "User"
+ And "View all results (5)" "option_role" should exist
+ And I confirm "Dummy User" in "user" search within the gradebook widget exists
+ And I confirm "User Example" in "user" search within the gradebook widget exists
+ And I confirm "User Test" in "user" search within the gradebook widget exists
+ # Email is not shown in results.
+ And I confirm "User" in "user" search within the gradebook widget exists
+ And I confirm "example.com" in "user" search within the gradebook widget does not exist
+ # Email is not searchable.
+ And I set the field "Search users" to "student5@example.com"
+ And "View all results (5)" "option_role" should not exist
+ And I confirm "No results for \"student5@example.com\"" in "user" search within the gradebook widget exists
+
@accessibility
Scenario: A teacher can set focus and search using the input are with a keyboard
Given I set the field "Search users" to "ABC"
diff --git a/user/amd/build/comboboxsearch/user.min.js b/user/amd/build/comboboxsearch/user.min.js
index 49a56ffb080..a456f38eace 100644
--- a/user/amd/build/comboboxsearch/user.min.js
+++ b/user/amd/build/comboboxsearch/user.min.js
@@ -1,3 +1,3 @@
-define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery"],(function(_exports,_search_combobox,_str,_templates,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"profilestringmap",null),["click","focus"].forEach((eventType=>{document.addEventListener(eventType,(e=>{this.searchDropdown.classList.contains("show")&&!this.combobox.contains(e.target)&&this.toggleDropdown()}),!0)})),this.component.addEventListener("keydown",this.keyHandler.bind(this)),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'},this.courseID=this.component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid,this.instance=this.component.querySelector(this.selectors.instance).dataset.instance,this.renderDefault()}static init(){return new UserSearch}componentSelector(){return".user-search"}dropdownSelector(){return".usersearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,instance:this.instance,matches:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){if(this.getPreppedSearchTerm()){const stringMap=await this.getStringMap();return filterableData.filter((user=>Object.keys(user).some((key=>!(""===user[key]||null===user[key]||!stringMap.get(key))&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}return[]}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm(),matchingFieldName=stringMap.get(key);if(matchingFieldName&&valueString.includes(preppedSearchTerm)){user.matchingFieldName=matchingFieldName;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")");break}}return user})))}changeHandler(e){this.toggleDropdown(),"0"===e.target.value?window.location=this.selectAllResultsLink():window.location=this.selectOneLink(e.target.value)}keyHandler(e){switch(e.key){case"ArrowUp":case"ArrowDown":""!==this.getSearchTerm()&&!this.searchDropdown.classList.contains("show")&&e.target.contains(this.combobox)&&this.renderAndShow();break;case"Enter":case" ":if(e.target.closest(this.selectors.resetPageButton)){e.stopPropagation(),window.location=e.target.closest(this.selectors.resetPageButton).href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0})}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","true"),this.searchInput.focus({preventScroll:!0})):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","false"),this.searchInput.removeAttribute("aria-activedescendant"),this.searchDropdown.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","fullname","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default}));
+define("core_user/comboboxsearch/user",["exports","core/comboboxsearch/search_combobox","core/str","core/templates","jquery"],(function(_exports,_search_combobox,_str,_templates,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_jquery=_interopRequireDefault(_jquery);class UserSearch extends _search_combobox.default{constructor(){var _document$querySelect,_document$querySelect2;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"groupID",void 0),_defineProperty(this,"profilestringmap",null),["click","focus"].forEach((eventType=>{document.addEventListener(eventType,(e=>{this.searchDropdown.classList.contains("show")&&!this.combobox.contains(e.target)&&this.toggleDropdown()}),!0)})),this.component.addEventListener("keydown",this.keyHandler.bind(this)),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',groupid:'[data-region="groupid"]',resetPageButton:'[data-action="resetpage"]'},this.courseID=this.component.querySelector(this.selectors.courseid).dataset.courseid,this.groupID=null===(_document$querySelect=document.querySelector(this.selectors.groupid))||void 0===_document$querySelect||null===(_document$querySelect2=_document$querySelect.dataset)||void 0===_document$querySelect2?void 0:_document$querySelect2.groupid,this.instance=this.component.querySelector(this.selectors.instance).dataset.instance,this.renderDefault()}static init(){return new UserSearch}componentSelector(){return".user-search"}dropdownSelector(){return".usersearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_user/comboboxsearch/resultset",{users:this.getMatchedResults().slice(0,5),hasresults:this.getMatchedResults().length>0,instance:this.instance,matches:this.getMatchedResults().length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown()}fetchDataset(){throw new Error("fetchDataset() must be implemented in ".concat(this.constructor.name))}async filterDataset(filterableData){if(this.getPreppedSearchTerm()){const stringMap=await this.getStringMap();return filterableData.filter((user=>Object.keys(user).some((key=>!(""===user[key]||null===user[key]||!stringMap.get(key))&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}return[]}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){if(null===value)continue;const valueString=value.toString().toLowerCase(),preppedSearchTerm=this.getPreppedSearchTerm(),searchTerm=this.getSearchTerm(),matchingFieldName=stringMap.get(key);if(matchingFieldName&&valueString.includes(preppedSearchTerm)){user.matchingFieldName=matchingFieldName;const escapedMatchingField=valueString.replace(/'.concat(searchTerm.replace(/"));user.email?user.matchingField="".concat(escapedMatchingField," (").concat(user.email,")"):user.matchingField=escapedMatchingField;break}}return user})))}changeHandler(e){this.toggleDropdown(),"0"===e.target.value?window.location=this.selectAllResultsLink():window.location=this.selectOneLink(e.target.value)}keyHandler(e){switch(e.key){case"ArrowUp":case"ArrowDown":""!==this.getSearchTerm()&&!this.searchDropdown.classList.contains("show")&&e.target.contains(this.combobox)&&this.renderAndShow();break;case"Enter":case" ":if(e.target.closest(this.selectors.resetPageButton)){e.stopPropagation(),window.location=e.target.closest(this.selectors.resetPageButton).href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0})}}toggleDropdown(){arguments.length>0&&void 0!==arguments[0]&&arguments[0]?(this.searchDropdown.classList.add("show"),(0,_jquery.default)(this.searchDropdown).show(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","true"),this.searchInput.focus({preventScroll:!0})):(this.searchDropdown.classList.remove("show"),(0,_jquery.default)(this.searchDropdown).hide(),this.getHTMLElements().searchInput.setAttribute("aria-expanded","false"),this.searchInput.removeAttribute("aria-activedescendant"),this.searchDropdown.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})))}selectAllResultsLink(){throw new Error("selectAllResultsLink() must be implemented in ".concat(this.constructor.name))}selectOneLink(userID){throw new Error("selectOneLink(".concat(userID,") must be implemented in ").concat(this.constructor.name))}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","fullname","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.getStrings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default}));
//# sourceMappingURL=user.min.js.map
\ No newline at end of file
diff --git a/user/amd/build/comboboxsearch/user.min.js.map b/user/amd/build/comboboxsearch/user.min.js.map
index c7c46bf45b4..4f524d2ef9a 100644
--- a/user/amd/build/comboboxsearch/user.min.js.map
+++ b/user/amd/build/comboboxsearch/user.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allow the user to search for learners.\n *\n * @module core_user/comboboxsearch/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a couple of events onto the document since we need to check if they are moving off the component.\n ['click', 'focus'].forEach(eventType => {\n // Since we are handling dropdowns manually, ensure we can close it when moving off.\n document.addEventListener(eventType, e => {\n if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) {\n this.toggleDropdown();\n }\n }, true);\n });\n\n // Register keyboard events.\n this.component.addEventListener('keydown', this.keyHandler.bind(this));\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;\n\n // We need to render some content by default for ARIA purposes.\n this.renderDefault();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n matches: this.getMatchedResults().length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n if (this.getPreppedSearchTerm()) {\n const stringMap = await this.getStringMap();\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n } else {\n return [];\n }\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n // Ensure we match only on expected keys.\n const matchingFieldName = stringMap.get(key);\n if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n user.matchingFieldName = matchingFieldName;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n break;\n }\n }\n return user;\n })\n );\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n if (e.target.value === '0') {\n window.location = this.selectAllResultsLink();\n } else {\n window.location = this.selectOneLink(e.target.value);\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n case 'ArrowDown':\n if (\n this.getSearchTerm() !== ''\n && !this.searchDropdown.classList.contains('show')\n && e.target.contains(this.combobox)\n ) {\n this.renderAndShow();\n }\n break;\n case 'Enter':\n case ' ':\n if (e.target.closest(this.selectors.resetPageButton)) {\n e.stopPropagation();\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n }\n }\n\n /**\n * When called, hide or show the users dropdown.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');\n this.searchInput.focus({preventScroll: true});\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n\n // As we are manually handling the dropdown, we need to do some housekeeping manually.\n this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'false');\n this.searchInput.removeAttribute('aria-activedescendant');\n this.searchDropdown.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n }\n }\n\n /**\n * Build up the view all link.\n */\n selectAllResultsLink() {\n throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} userID The ID of the user selected.\n */\n selectOneLink(userID) {\n throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'fullname',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["UserSearch","search_combobox","constructor","forEach","eventType","document","addEventListener","e","this","searchDropdown","classList","contains","combobox","target","toggleDropdown","component","keyHandler","bind","selectors","courseid","groupid","resetPageButton","courseID","querySelector","dataset","groupID","_document$querySelect","_document$querySelect2","instance","renderDefault","componentSelector","dropdownSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchInput","removeAttribute","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","fetchDataset","Error","name","filterableData","getPreppedSearchTerm","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","matchingField","email","changeHandler","window","location","selectOneLink","renderAndShow","closest","stopPropagation","href","focus","preventScroll","add","show","setAttribute","remove","hide","querySelectorAll","option","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"user.min.js","sources":["../../src/comboboxsearch/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allow the user to search for learners.\n *\n * @module core_user/comboboxsearch/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport search_combobox from 'core/comboboxsearch/search_combobox';\nimport {getStrings} from 'core/str';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport $ from 'jquery';\n\nexport default class UserSearch extends search_combobox {\n\n courseID;\n groupID;\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n // Register a couple of events onto the document since we need to check if they are moving off the component.\n ['click', 'focus'].forEach(eventType => {\n // Since we are handling dropdowns manually, ensure we can close it when moving off.\n document.addEventListener(eventType, e => {\n if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) {\n this.toggleDropdown();\n }\n }, true);\n });\n\n // Register keyboard events.\n this.component.addEventListener('keydown', this.keyHandler.bind(this));\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n groupid: '[data-region=\"groupid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n };\n\n this.courseID = this.component.querySelector(this.selectors.courseid).dataset.courseid;\n this.groupID = document.querySelector(this.selectors.groupid)?.dataset?.groupid;\n this.instance = this.component.querySelector(this.selectors.instance).dataset.instance;\n\n // We need to render some content by default for ARIA purposes.\n this.renderDefault();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n matches: this.getMatchedResults().length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n // Remove aria-activedescendant when the available options change.\n this.searchInput.removeAttribute('aria-activedescendant');\n }\n\n /**\n * Build the content then replace the node by default we want our form to exist.\n */\n async renderDefault() {\n this.setMatchedResults(await this.filterDataset(await this.getDataset()));\n this.filterMatchDataset();\n\n await this.renderDropdown();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n throw new Error(`fetchDataset() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n if (this.getPreppedSearchTerm()) {\n const stringMap = await this.getStringMap();\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || user[key] === null || !stringMap.get(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n } else {\n return [];\n }\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n // Sometimes users have null values in their profile fields.\n if (value === null) {\n continue;\n }\n\n const valueString = value.toString().toLowerCase();\n const preppedSearchTerm = this.getPreppedSearchTerm();\n const searchTerm = this.getSearchTerm();\n\n // Ensure we match only on expected keys.\n const matchingFieldName = stringMap.get(key);\n if (matchingFieldName && valueString.includes(preppedSearchTerm)) {\n user.matchingFieldName = matchingFieldName;\n\n // Safely prepare our matching results.\n const escapedValueString = valueString.replace(/${searchTerm.replace(/`\n );\n\n if (user.email) {\n user.matchingField = `${escapedMatchingField} (${user.email})`;\n } else {\n user.matchingField = escapedMatchingField;\n }\n break;\n }\n }\n return user;\n })\n );\n }\n\n /**\n * The handler for when a user changes the value of the component (selects an option from the dropdown).\n *\n * @param {Event} e The change event.\n */\n changeHandler(e) {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n if (e.target.value === '0') {\n window.location = this.selectAllResultsLink();\n } else {\n window.location = this.selectOneLink(e.target.value);\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'ArrowUp':\n case 'ArrowDown':\n if (\n this.getSearchTerm() !== ''\n && !this.searchDropdown.classList.contains('show')\n && e.target.contains(this.combobox)\n ) {\n this.renderAndShow();\n }\n break;\n case 'Enter':\n case ' ':\n if (e.target.closest(this.selectors.resetPageButton)) {\n e.stopPropagation();\n window.location = e.target.closest(this.selectors.resetPageButton).href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n }\n }\n\n /**\n * When called, hide or show the users dropdown.\n *\n * @param {Boolean} on Flag to toggle hiding or showing values.\n */\n toggleDropdown(on = false) {\n if (on) {\n this.searchDropdown.classList.add('show');\n $(this.searchDropdown).show();\n this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');\n this.searchInput.focus({preventScroll: true});\n } else {\n this.searchDropdown.classList.remove('show');\n $(this.searchDropdown).hide();\n\n // As we are manually handling the dropdown, we need to do some housekeeping manually.\n this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'false');\n this.searchInput.removeAttribute('aria-activedescendant');\n this.searchDropdown.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n }\n }\n\n /**\n * Build up the view all link.\n */\n selectAllResultsLink() {\n throw new Error(`selectAllResultsLink() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n * We will call this function when a user interacts with the combobox to redirect them to show their results in the page.\n *\n * @param {Number} userID The ID of the user selected.\n */\n selectOneLink(userID) {\n throw new Error(`selectOneLink(${userID}) must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'fullname',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["UserSearch","search_combobox","constructor","forEach","eventType","document","addEventListener","e","this","searchDropdown","classList","contains","combobox","target","toggleDropdown","component","keyHandler","bind","selectors","courseid","groupid","resetPageButton","courseID","querySelector","dataset","groupID","_document$querySelect","_document$querySelect2","instance","renderDefault","componentSelector","dropdownSelector","html","js","users","getMatchedResults","slice","hasresults","length","matches","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchInput","removeAttribute","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","fetchDataset","Error","name","filterableData","getPreppedSearchTerm","stringMap","getStringMap","filter","user","Object","keys","some","key","get","toString","toLowerCase","includes","map","value","entries","valueString","preppedSearchTerm","searchTerm","matchingFieldName","escapedMatchingField","replace","email","matchingField","changeHandler","window","location","selectOneLink","renderAndShow","closest","stopPropagation","href","focus","preventScroll","add","show","setAttribute","remove","hide","querySelectorAll","option","userID","profilestringmap","requiredStrings","then","stringArray","Map","index"],"mappings":"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"}
\ No newline at end of file
diff --git a/user/amd/src/comboboxsearch/user.js b/user/amd/src/comboboxsearch/user.js
index c1350902980..6c1e3762c37 100644
--- a/user/amd/src/comboboxsearch/user.js
+++ b/user/amd/src/comboboxsearch/user.js
@@ -172,7 +172,11 @@ export default class UserSearch extends search_combobox {
`${searchTerm.replace(/`
);
- user.matchingField = `${escapedMatchingField} (${user.email})`;
+ if (user.email) {
+ user.matchingField = `${escapedMatchingField} (${user.email})`;
+ } else {
+ user.matchingField = escapedMatchingField;
+ }
break;
}
}