From 69844e76dab584e09c52c60a06202155372ac97f Mon Sep 17 00:00:00 2001 From: Mathew May Date: Tue, 16 May 2023 15:29:55 +0800 Subject: [PATCH] MDL-77991 gradereport_user: Migrate to core search dropdown --- grade/report/user/amd/build/group.min.js | 3 + grade/report/user/amd/build/group.min.js.map | 1 + grade/report/user/amd/build/user.min.js | 8 +- grade/report/user/amd/build/user.min.js.map | 2 +- grade/report/user/amd/src/group.js | 58 ++++ grade/report/user/amd/src/user.js | 189 ++++------- .../report/user/classes/output/action_bar.php | 10 +- grade/report/user/index.php | 4 + grade/report/user/renderer.php | 50 +-- .../report/user/templates/action_bar.mustache | 14 +- .../user/tests/behat/groupsearch.feature | 66 +++- .../user/tests/behat/usersearch.feature | 305 ++++++++++++++++-- .../user/tests/behat/view_usereport.feature | 5 +- 13 files changed, 507 insertions(+), 208 deletions(-) create mode 100644 grade/report/user/amd/build/group.min.js create mode 100644 grade/report/user/amd/build/group.min.js.map create mode 100644 grade/report/user/amd/src/group.js diff --git a/grade/report/user/amd/build/group.min.js b/grade/report/user/amd/build/group.min.js new file mode 100644 index 0000000000000..a8f95c0173435 --- /dev/null +++ b/grade/report/user/amd/build/group.min.js @@ -0,0 +1,3 @@ +define("gradereport_user/group",["exports","core_group/comboboxsearch/group","core/url"],(function(_exports,_group,_url){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_group=_interopRequireDefault(_group),_url=_interopRequireDefault(_url);class Group extends _group.default{constructor(){var obj,key,value;super(),value=void 0,(key="courseID")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.selectors={...this.selectors,courseid:'[data-region="courseid"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid}static init(){return new Group}selectOneLink(groupID){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,groupsearchvalue:this.getSearchTerm(),group:groupID},!1)}}return _exports.default=Group,_exports.default})); + +//# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/grade/report/user/amd/build/group.min.js.map b/grade/report/user/amd/build/group.min.js.map new file mode 100644 index 0000000000000..530e5c0c7fa38 --- /dev/null +++ b/grade/report/user/amd/build/group.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"group.min.js","sources":["../src/group.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 groups within the user report.\n *\n * @module gradereport_user/group\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GroupSearch from 'core_group/comboboxsearch/group';\nimport Url from 'core/url';\n\nexport default class Group extends GroupSearch {\n\n courseID;\n\n constructor() {\n super();\n\n // Define our standard lookups.\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n }\n\n static init() {\n return new Group();\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @returns {string|*}\n */\n selectOneLink(groupID) {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n groupsearchvalue: this.getSearchTerm(),\n group: groupID,\n }, false);\n }\n}\n"],"names":["Group","GroupSearch","constructor","selectors","this","courseid","component","document","querySelector","componentSelector","courseID","dataset","selectOneLink","groupID","Url","relativeUrl","id","groupsearchvalue","getSearchTerm","group"],"mappings":"yWAyBqBA,cAAcC,eAI/BC,6LAISC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,kCAERC,UAAYC,SAASC,cAAcJ,KAAKK,0BACzCC,SAAWJ,UAAUE,cAAcJ,KAAKD,UAAUE,UAAUM,QAAQN,8BAIlE,IAAIL,MASfY,cAAcC,gBACHC,aAAIC,YAAY,+BAAgC,CACnDC,GAAIZ,KAAKM,SACTO,iBAAkBb,KAAKc,gBACvBC,MAAON,UACR"} \ No newline at end of file diff --git a/grade/report/user/amd/build/user.min.js b/grade/report/user/amd/build/user.min.js index 02979875cc045..f3581f620c385 100644 --- a/grade/report/user/amd/build/user.min.js +++ b/grade/report/user/amd/build/user.min.js @@ -1,10 +1,10 @@ -define("gradereport_user/user",["exports","core/local/aria/focuslock","core/pending","core/templates","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str","core/url","jquery","core_grades/searchwidget/selectors"],(function(_exports,FocusLockManager,_pending,Templates,Repository,WidgetBase,_str,_url,_jquery,Selectors){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +define("gradereport_user/user",["exports","core_user/comboboxsearch/user","core/url","core/templates","core_grades/searchwidget/repository"],(function(_exports,_user,_url,_templates,Repository){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** - * A widget to search users within the gradebook. + * Allow the user to search for learners within the user report. * * @module gradereport_user/user - * @copyright 2022 Mathew May + * @copyright 2023 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,FocusLockManager=_interopRequireWildcard(FocusLockManager),_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url),_jquery=_interopRequireDefault(_jquery),Selectors=_interopRequireWildcard(Selectors);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();const dropdownMenuContainer=document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector("user")),menuContainer=document.querySelector(Selectors.elements.getSearchWidgetSelector("user")),inputElement=menuContainer.querySelector('input[name="userid"]');(0,_jquery.default)(menuContainer).on("show.bs.dropdown",(async e=>{const courseID=e.relatedTarget.dataset.courseid,groupId=e.relatedTarget.dataset.groupid;await WidgetBase.showLoader(dropdownMenuContainer);const data=await Repository.userFetch(courseID,groupId).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;const allUsersOptionName=await(0,_str.get_string)("allusersnum","gradereport_user",data.users.length),allUsersOption=await Templates.render("gradereport_user/all_users_item",{id:0,name:allUsersOptionName,url:_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:0},!1)});await WidgetBase.init(dropdownMenuContainer,bodyPromise,data.users,searchUsers(),allUsersOption,afterSelect),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",{displayunsearchablecontent:!0})),FocusLockManager.trapFocus(dropdownMenuContainer)})),(0,_jquery.default)(menuContainer).on("hide.bs.dropdown",(()=>{FocusLockManager.untrapFocus()})),inputElement.addEventListener("change",(e=>{const courseID=menuContainer.querySelector(".dropdown-toggle").dataset.courseid,actionUrl=_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:e.target.value},!1);location.href=actionUrl,e.stopPropagation()}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults},afterSelect=selected=>{const menuContainer=document.querySelector(Selectors.elements.getSearchWidgetSelector("user")),inputElement=menuContainer.querySelector('input[name="userid"]');(0,_jquery.default)(menuContainer).dropdown("hide"),inputElement.value!=selected&&(inputElement.value=selected,inputElement.dispatchEvent(new Event("change",{bubbles:!0})))}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=_interopRequireDefault(_user),_url=_interopRequireDefault(_url),Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class User extends _user.default{constructor(){super()}static init(){return new User}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,matches:this.getDatasetSize(),searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,userid:0,searchvalue:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/user/index.php",{id:this.courseID,searchvalue:this.getSearchTerm(),userid:userID},!1)}fetchDataset(){const gts="string"==typeof this.groupID&&""===this.groupID?0:this.groupID;return Repository.userFetch(this.courseID,gts).then((r=>r.users))}}return _exports.default=User,_exports.default})); //# sourceMappingURL=user.min.js.map \ No newline at end of file diff --git a/grade/report/user/amd/build/user.min.js.map b/grade/report/user/amd/build/user.min.js.map index df2bbef282b78..697e8b48502b5 100644 --- a/grade/report/user/amd/build/user.min.js.map +++ b/grade/report/user/amd/build/user.min.js.map @@ -1 +1 @@ -{"version":3,"file":"user.min.js","sources":["../src/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 * A widget to search users within the gradebook.\n *\n * @module gradereport_user/user\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as FocusLockManager from 'core/local/aria/focuslock';\nimport Pending from 'core/pending';\nimport * as Templates from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport * as WidgetBase from 'core_grades/searchwidget/basewidget';\nimport {get_string as getString} from 'core/str';\nimport Url from 'core/url';\nimport $ from 'jquery';\nimport * as Selectors from 'core_grades/searchwidget/selectors';\n\n/**\n * Our entry point into starting to build the search widget.\n * It'll eventually, based upon the listeners, open the search widget and allow filtering.\n *\n * @method init\n */\nexport const init = () => {\n const pendingPromise = new Pending();\n registerListenerEvents();\n pendingPromise.resolve();\n};\n\n/**\n * Register user search widget related event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();\n const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('user'));\n const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('user'));\n const inputElement = menuContainer.querySelector('input[name=\"userid\"]');\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(menuContainer).on('show.bs.dropdown', async(e) => {\n const courseID = e.relatedTarget.dataset.courseid;\n const groupId = e.relatedTarget.dataset.groupid;\n // Display a loading icon in the dropdown menu container until the body promise is resolved.\n await WidgetBase.showLoader(dropdownMenuContainer);\n\n // If an error occurs while fetching the data, display the error within the dropdown menu.\n const data = await Repository.userFetch(courseID, groupId).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(\n await Templates.render('core_grades/searchwidget/error', errorTemplateData)\n );\n });\n\n // Early return if there is no module data.\n if (data === []) {\n return;\n }\n\n // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget.\n const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length);\n const allUsersOption = await Templates.render('gradereport_user/all_users_item', {\n id: 0,\n name: allUsersOptionName,\n url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false),\n });\n\n await WidgetBase.init(\n dropdownMenuContainer,\n bodyPromise,\n data.users,\n searchUsers(),\n allUsersOption,\n afterSelect\n );\n\n // Resolvers for passed functions in the dropdown menu creation.\n bodyPromiseResolver(Templates.render(\n 'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true}\n ));\n\n // Lock tab control. It has to be locked because the dropdown's role is dialog.\n FocusLockManager.trapFocus(dropdownMenuContainer);\n });\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(menuContainer).on('hide.bs.dropdown', () => {\n FocusLockManager.untrapFocus();\n });\n\n inputElement.addEventListener('change', e => {\n const toggle = menuContainer.querySelector('.dropdown-toggle');\n const courseID = toggle.dataset.courseid;\n const actionUrl = Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: e.target.value}, false);\n location.href = actionUrl;\n\n e.stopPropagation();\n });\n};\n\n/**\n * Define how we want to search and filter users when the user decides to input a search value.\n *\n * @method searchUsers\n * @returns {function(): function(*, *): (*)}\n */\nconst searchUsers = () => {\n return () => {\n return (users, searchTerm) => {\n if (searchTerm === '') {\n return users;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n users.forEach((user) => {\n const userName = user.fullname.toLowerCase();\n if (userName.includes(searchTerm)) {\n searchResults.push(user);\n }\n });\n return searchResults;\n };\n };\n};\n\n/**\n * Define the action to be performed when an item is selected by the search widget.\n *\n * @param {String} selected The selected item's value.\n */\nconst afterSelect = (selected) => {\n const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('user'));\n const inputElement = menuContainer.querySelector('input[name=\"userid\"]');\n\n $(menuContainer).dropdown('hide'); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n if (inputElement.value != selected) {\n inputElement.value = selected;\n inputElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n};\n"],"names":["pendingPromise","Pending","registerListenerEvents","resolve","bodyPromiseResolver","bodyPromise","WidgetBase","promisesAndResolvers","dropdownMenuContainer","document","querySelector","Selectors","elements","getSearchWidgetDropdownSelector","menuContainer","getSearchWidgetSelector","inputElement","on","async","courseID","e","relatedTarget","dataset","courseid","groupId","groupid","showLoader","data","Repository","userFetch","catch","errorTemplateData","message","Templates","render","allUsersOptionName","users","length","allUsersOption","id","name","url","Url","relativeUrl","userid","init","searchUsers","afterSelect","displayunsearchablecontent","FocusLockManager","trapFocus","untrapFocus","addEventListener","actionUrl","target","value","location","href","stopPropagation","searchTerm","toLowerCase","searchResults","forEach","user","fullname","includes","push","selected","dropdown","dispatchEvent","Event","bubbles"],"mappings":";;;;;;;ucAuCoB,WACVA,eAAiB,IAAIC,iBAC3BC,yBACAF,eAAeG,iBAQbD,uBAAyB,SACvBE,oBAACA,oBAADC,YAAsBA,aAAeC,WAAWC,6BAC9CC,sBAAwBC,SAASC,cAAcC,UAAUC,SAASC,gCAAgC,SAClGC,cAAgBL,SAASC,cAAcC,UAAUC,SAASG,wBAAwB,SAClFC,aAAeF,cAAcJ,cAAc,4CAG/CI,eAAeG,GAAG,oBAAoBC,MAAAA,UAC9BC,SAAWC,EAAEC,cAAcC,QAAQC,SACnCC,QAAUJ,EAAEC,cAAcC,QAAQG,cAElCnB,WAAWoB,WAAWlB,6BAGtBmB,WAAaC,WAAWC,UAAUV,SAAUK,SAASM,OAAMZ,MAAAA,UACvDa,kBAAoB,cACNX,EAAEY,SAEtB5B,0BACU6B,UAAUC,OAAO,iCAAkCH,0BAK7DJ,OAAS,gBAKPQ,yBAA2B,mBAAU,cAAe,mBAAoBR,KAAKS,MAAMC,QACnFC,qBAAuBL,UAAUC,OAAO,kCAAmC,CAC7EK,GAAI,EACJC,KAAML,mBACNM,IAAKC,aAAIC,YAAY,+BAAgC,CAACJ,GAAIpB,SAAUyB,OAAQ,IAAI,WAG9EtC,WAAWuC,KACbrC,sBACAH,YACAsB,KAAKS,MACLU,cACAR,eACAS,aAIJ3C,oBAAoB6B,UAAUC,OAC1B,gDAAiD,CAACc,4BAA4B,KAIlFC,iBAAiBC,UAAU1C,8CAI7BM,eAAeG,GAAG,oBAAoB,KACpCgC,iBAAiBE,iBAGrBnC,aAAaoC,iBAAiB,UAAUhC,UAE9BD,SADSL,cAAcJ,cAAc,oBACnBY,QAAQC,SAC1B8B,UAAYX,aAAIC,YAAY,+BAAgC,CAACJ,GAAIpB,SAAUyB,OAAQxB,EAAEkC,OAAOC,QAAQ,GAC1GC,SAASC,KAAOJ,UAEhBjC,EAAEsC,sBAUJZ,YAAc,IACT,IACI,CAACV,MAAOuB,iBACQ,KAAfA,kBACOvB,MAEXuB,WAAaA,WAAWC,oBAClBC,cAAgB,UACtBzB,MAAM0B,SAASC,OACMA,KAAKC,SAASJ,cAClBK,SAASN,aAClBE,cAAcK,KAAKH,SAGpBF,eAUbd,YAAeoB,iBACXrD,cAAgBL,SAASC,cAAcC,UAAUC,SAASG,wBAAwB,SAClFC,aAAeF,cAAcJ,cAAc,4CAE/CI,eAAesD,SAAS,QAEtBpD,aAAauC,OAASY,WACtBnD,aAAauC,MAAQY,SACrBnD,aAAaqD,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS"} \ No newline at end of file +{"version":3,"file":"user.min.js","sources":["../src/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 within the user report.\n *\n * @module gradereport_user/user\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport * as Repository from 'core_grades/searchwidget/repository';\n\nexport default class User extends UserSearch {\n\n constructor() {\n super();\n }\n\n static init() {\n return new User();\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 matches: this.getDatasetSize(),\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n userid: 0,\n searchvalue: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the view all link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n return Url.relativeUrl('/grade/report/user/index.php', {\n id: this.courseID,\n searchvalue: this.getSearchTerm(),\n userid: userID,\n }, false);\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n // Small typing checks as sometimes groups don't exist therefore the element returns a empty string.\n const gts = typeof (this.groupID) === \"string\" && this.groupID === '' ? 0 : this.groupID;\n return Repository.userFetch(this.courseID, gts).then((r) => r.users);\n }\n}\n"],"names":["User","UserSearch","constructor","html","js","users","this","getMatchedResults","slice","hasresults","length","matches","getDatasetSize","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","Url","relativeUrl","id","courseID","userid","searchvalue","selectOneLink","userID","fetchDataset","gts","groupID","Repository","userFetch","then","r"],"mappings":";;;;;;;q0BA2BqBA,aAAaC,cAE9BC,2CAKW,IAAIF,kCAOLG,KAACA,KAADC,GAAOA,UAAY,+BAAiB,qCAAsC,CAC5EC,MAAOC,KAAKC,oBAAoBC,MAAM,EAAG,GACzCC,WAAYH,KAAKC,oBAAoBG,OAAS,EAC9CC,QAASL,KAAKM,iBACdC,WAAYP,KAAKQ,gBACjBC,UAAWT,KAAKU,4DAEAV,KAAKW,kBAAkBC,eAAgBf,KAAMC,IAQrEY,8BACWG,aAAIC,YAAY,+BAAgC,CACnDC,GAAIf,KAAKgB,SACTC,OAAQ,EACRC,YAAalB,KAAKQ,kBACnB,GASPW,cAAcC,eACHP,aAAIC,YAAY,+BAAgC,CACnDC,GAAIf,KAAKgB,SACTE,YAAalB,KAAKQ,gBAClBS,OAAQG,SACT,GAQPC,qBAEUC,IAAgC,iBAAlBtB,KAAKuB,SAA0C,KAAjBvB,KAAKuB,QAAiB,EAAIvB,KAAKuB,eAC1EC,WAAWC,UAAUzB,KAAKgB,SAAUM,KAAKI,MAAMC,GAAMA,EAAE5B"} \ No newline at end of file diff --git a/grade/report/user/amd/src/group.js b/grade/report/user/amd/src/group.js new file mode 100644 index 0000000000000..44387cb356a98 --- /dev/null +++ b/grade/report/user/amd/src/group.js @@ -0,0 +1,58 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Allow the user to search for groups within the user report. + * + * @module gradereport_user/group + * @copyright 2023 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import GroupSearch from 'core_group/comboboxsearch/group'; +import Url from 'core/url'; + +export default class Group extends GroupSearch { + + courseID; + + constructor() { + super(); + + // Define our standard lookups. + this.selectors = {...this.selectors, + courseid: '[data-region="courseid"]', + }; + const component = document.querySelector(this.componentSelector()); + this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid; + } + + static init() { + return new Group(); + } + + /** + * Build up the view all link that is dedicated to a particular result. + * + * @param {Number} groupID The ID of the group selected. + * @returns {string|*} + */ + selectOneLink(groupID) { + return Url.relativeUrl('/grade/report/user/index.php', { + id: this.courseID, + groupsearchvalue: this.getSearchTerm(), + group: groupID, + }, false); + } +} diff --git a/grade/report/user/amd/src/user.js b/grade/report/user/amd/src/user.js index 93935cb114edd..8383f337f72a7 100644 --- a/grade/report/user/amd/src/user.js +++ b/grade/report/user/amd/src/user.js @@ -14,147 +14,76 @@ // along with Moodle. If not, see . /** - * A widget to search users within the gradebook. + * Allow the user to search for learners within the user report. * * @module gradereport_user/user - * @copyright 2022 Mathew May + * @copyright 2023 Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - -import * as FocusLockManager from 'core/local/aria/focuslock'; -import Pending from 'core/pending'; -import * as Templates from 'core/templates'; -import * as Repository from 'core_grades/searchwidget/repository'; -import * as WidgetBase from 'core_grades/searchwidget/basewidget'; -import {get_string as getString} from 'core/str'; +import UserSearch from 'core_user/comboboxsearch/user'; import Url from 'core/url'; -import $ from 'jquery'; -import * as Selectors from 'core_grades/searchwidget/selectors'; - -/** - * Our entry point into starting to build the search widget. - * It'll eventually, based upon the listeners, open the search widget and allow filtering. - * - * @method init - */ -export const init = () => { - const pendingPromise = new Pending(); - registerListenerEvents(); - pendingPromise.resolve(); -}; - -/** - * Register user search widget related event listeners. - * - * @method registerListenerEvents - */ -const registerListenerEvents = () => { - let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers(); - const dropdownMenuContainer = document.querySelector(Selectors.elements.getSearchWidgetDropdownSelector('user')); - const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('user')); - const inputElement = menuContainer.querySelector('input[name="userid"]'); +import {renderForPromise, replaceNodeContents} from 'core/templates'; +import * as Repository from 'core_grades/searchwidget/repository'; - // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed). - $(menuContainer).on('show.bs.dropdown', async(e) => { - const courseID = e.relatedTarget.dataset.courseid; - const groupId = e.relatedTarget.dataset.groupid; - // Display a loading icon in the dropdown menu container until the body promise is resolved. - await WidgetBase.showLoader(dropdownMenuContainer); +export default class User extends UserSearch { - // If an error occurs while fetching the data, display the error within the dropdown menu. - const data = await Repository.userFetch(courseID, groupId).catch(async(e) => { - const errorTemplateData = { - 'errormessage': e.message - }; - bodyPromiseResolver( - await Templates.render('core_grades/searchwidget/error', errorTemplateData) - ); - }); + constructor() { + super(); + } - // Early return if there is no module data. - if (data === []) { - return; - } + static init() { + return new User(); + } - // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget. - const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length); - const allUsersOption = await Templates.render('gradereport_user/all_users_item', { - id: 0, - name: allUsersOptionName, - url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false), + /** + * Build the content then replace the node. + */ + async renderDropdown() { + const {html, js} = await renderForPromise('core_user/comboboxsearch/resultset', { + users: this.getMatchedResults().slice(0, 5), + hasresults: this.getMatchedResults().length > 0, + matches: this.getDatasetSize(), + searchterm: this.getSearchTerm(), + selectall: this.selectAllResultsLink(), }); + replaceNodeContents(this.getHTMLElements().searchDropdown, html, js); + } - await WidgetBase.init( - dropdownMenuContainer, - bodyPromise, - data.users, - searchUsers(), - allUsersOption, - afterSelect - ); - - // Resolvers for passed functions in the dropdown menu creation. - bodyPromiseResolver(Templates.render( - 'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true} - )); - - // Lock tab control. It has to be locked because the dropdown's role is dialog. - FocusLockManager.trapFocus(dropdownMenuContainer); - }); - - // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed). - $(menuContainer).on('hide.bs.dropdown', () => { - FocusLockManager.untrapFocus(); - }); - - inputElement.addEventListener('change', e => { - const toggle = menuContainer.querySelector('.dropdown-toggle'); - const courseID = toggle.dataset.courseid; - const actionUrl = Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: e.target.value}, false); - location.href = actionUrl; - - e.stopPropagation(); - }); -}; - -/** - * Define how we want to search and filter users when the user decides to input a search value. - * - * @method searchUsers - * @returns {function(): function(*, *): (*)} - */ -const searchUsers = () => { - return () => { - return (users, searchTerm) => { - if (searchTerm === '') { - return users; - } - searchTerm = searchTerm.toLowerCase(); - const searchResults = []; - users.forEach((user) => { - const userName = user.fullname.toLowerCase(); - if (userName.includes(searchTerm)) { - searchResults.push(user); - } - }); - return searchResults; - }; - }; -}; - -/** - * Define the action to be performed when an item is selected by the search widget. - * - * @param {String} selected The selected item's value. - */ -const afterSelect = (selected) => { - const menuContainer = document.querySelector(Selectors.elements.getSearchWidgetSelector('user')); - const inputElement = menuContainer.querySelector('input[name="userid"]'); + /** + * Build up the view all link. + * + * @returns {string|*} + */ + selectAllResultsLink() { + return Url.relativeUrl('/grade/report/user/index.php', { + id: this.courseID, + userid: 0, + searchvalue: this.getSearchTerm() + }, false); + } - $(menuContainer).dropdown('hide'); // Otherwise the dropdown stays open when user choose an option using keyboard. + /** + * Build up the view all link that is dedicated to a particular result. + * + * @param {Number} userID The ID of the user selected. + * @returns {string|*} + */ + selectOneLink(userID) { + return Url.relativeUrl('/grade/report/user/index.php', { + id: this.courseID, + searchvalue: this.getSearchTerm(), + userid: userID, + }, false); + } - if (inputElement.value != selected) { - inputElement.value = selected; - inputElement.dispatchEvent(new Event('change', {bubbles: true})); + /** + * Get the data we will be searching against in this component. + * + * @returns {Promise<*>} + */ + fetchDataset() { + // Small typing checks as sometimes groups don't exist therefore the element returns a empty string. + const gts = typeof (this.groupID) === "string" && this.groupID === '' ? 0 : this.groupID; + return Repository.userFetch(this.courseID, gts).then((r) => r.users); } -}; +} diff --git a/grade/report/user/classes/output/action_bar.php b/grade/report/user/classes/output/action_bar.php index feb83c28ecaed..f096d82976391 100644 --- a/grade/report/user/classes/output/action_bar.php +++ b/grade/report/user/classes/output/action_bar.php @@ -80,12 +80,12 @@ public function export_for_template(\renderer_base $output): array { // If the user has the capability to view all grades, display the group selector (if applicable), the user selector // and the view mode selector (if applicable). if (has_capability('moodle/grade:viewall', $this->context)) { - $course = get_course($courseid); - $gradesrenderer = $PAGE->get_renderer('core_grades'); $userreportrenderer = $PAGE->get_renderer('gradereport_user'); - - $data['groupselector'] = $gradesrenderer->group_selector($course); - $data['userselector'] = $userreportrenderer->users_selector($course, $this->userid, $this->currentgroupid); + $data['groupselector'] = $PAGE->get_renderer('core_grades')->group_selector(get_course($courseid)); + $data['userselector'] = [ + 'courseid' => $courseid, + 'content' => $userreportrenderer->users_selector(get_course($courseid), $this->userid, $this->currentgroupid) + ]; // Do not output the 'view mode' selector when in zero state or when the current user is viewing its own report. if (!is_null($this->userid) && $USER->id != $this->userid) { diff --git a/grade/report/user/index.php b/grade/report/user/index.php index 81429474a1d0f..d170c6d28cb94 100644 --- a/grade/report/user/index.php +++ b/grade/report/user/index.php @@ -93,6 +93,10 @@ // Verify if we are using groups or not. $groupmode = groups_get_course_groupmode($course); $currentgroup = $gpr->groupid; + // Conditionally add the group JS if we have groups enabled. + if ($groupmode) { + $PAGE->requires->js_call_amd('gradereport_user/group', 'init'); + } // To make some other functions work better later. if (!$currentgroup) { diff --git a/grade/report/user/renderer.php b/grade/report/user/renderer.php index 3e59981b61867..b9c49985f81fa 100644 --- a/grade/report/user/renderer.php +++ b/grade/report/user/renderer.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use core\output\comboboxsearch; + /** * Custom renderer for the user grade report * @@ -90,47 +92,27 @@ public function view_user_selector(int $userid, int $userview): string { * @throws coding_exception */ public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string { - + $resetlink = new moodle_url('/grade/report/user/index.php', ['id' => $course->id, 'group' => 0]); $data = [ + 'currentvalue' => optional_param('searchvalue', '', PARAM_NOTAGS), + 'resetlink' => $resetlink->out(false), 'name' => 'userid', 'courseid' => $course->id, 'groupid' => $groupid ?? 0, ]; - // If a particular option is selected (not in zero state). - if (!is_null($userid)) { - if ($userid) { // A single user selected. - $user = core_user::get_user($userid); - $data['selectedoption'] = [ - 'image' => $this->user_picture($user, ['size' => 40, 'link' => false]), - 'text' => fullname($user), - 'additionaltext' => $user->email, - ]; - } else { // All users selected. - // Get the total number of users. - $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); - $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); - $showonlyactiveenrol = $showonlyactiveenrol || - !has_capability('moodle/course:viewsuspendedusers', context_course::instance($course->id)); - $gui = new graded_users_iterator($course, null, $groupid); - $gui->require_active_enrolment($showonlyactiveenrol); - $gui->init(); - $totalusersnum = 0; - while ($userdata = $gui->next_user()) { - $totalusersnum++; - } - $gui->close(); - - $data['selectedoption'] = [ - 'text' => get_string('allusersnum', 'gradereport_user', $totalusersnum), - ]; - } - - $data['userid'] = $userid; - } - + $searchdropdown = new comboboxsearch( + true, + $this->render_from_template('core_user/comboboxsearch/user_selector', $data), + null, + 'user-search dropdown d-flex', + null, + 'usersearchdropdown overflow-auto', + null, + false, + ); $this->page->requires->js_call_amd('gradereport_user/user', 'init'); - return $this->render_from_template('core_grades/user_selector', $data); + return $this->render_from_template($searchdropdown->get_template(), $searchdropdown->export_for_template($this)); } /** diff --git a/grade/report/user/templates/action_bar.mustache b/grade/report/user/templates/action_bar.mustache index 0ddb3ac1be336..fbbd01812ac84 100644 --- a/grade/report/user/templates/action_bar.mustache +++ b/grade/report/user/templates/action_bar.mustache @@ -19,7 +19,7 @@ Context variables required for this template: * generalnavselector - The data object containing the required properties to render the general navigation selector. * groupselector - (optional) HTML that outputs the group selector - * userselector - (optional) HTML that outputs the user selector + * userselector - (optional) The data object containing the required properties to render the user selector * viewasselector - (optional) HTML that outputs the 'view report as' selector Example context (json): @@ -63,7 +63,10 @@ ] }, "groupselector": "
", - "userselector": "
", + "userselector": { + "content": "
", + "courseid": 25 + }, "viewasselector": "
" } }} @@ -82,8 +85,11 @@ {{/groupselector}} {{#userselector}} -