diff --git a/course/amd/build/actionbar/group.min.js b/course/amd/build/actionbar/group.min.js index 862cc18b85ad5..e53eb8ff717bd 100644 --- a/course/amd/build/actionbar/group.min.js +++ b/course/amd/build/actionbar/group.min.js @@ -6,6 +6,6 @@ define("core_course/actionbar/group",["exports","core_group/comboboxsearch/group * @copyright 2024 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class Group extends _group.default{constructor(baseUrl){super(arguments.length>1&&void 0!==arguments[1]?arguments[1]:null),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"baseUrl",void 0),this.baseUrl=baseUrl}static init(baseUrl){return new Group(baseUrl,arguments.length>1&&void 0!==arguments[1]?arguments[1]:null)}selectOneLink(groupID){const url=new URL(this.baseUrl);return url.searchParams.set("groupsearchvalue",this.getSearchTerm()),url.searchParams.set("group",groupID),url.toString()}}return _exports.default=Group,_exports.default})); +class Group extends _group.default{constructor(baseUrl){super(arguments.length>1&&void 0!==arguments[1]?arguments[1]:null),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"baseUrl",void 0),this.baseUrl=baseUrl}static init(baseUrl){return new Group(baseUrl,arguments.length>1&&void 0!==arguments[1]?arguments[1]:null)}selectOneLink(groupID){let groupingID=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;const url=new URL(this.baseUrl);return url.searchParams.set("groupsearchvalue",this.getSearchTerm()),url.searchParams.set("group",groupID),groupingID>0&&url.searchParams.set("grouping",groupingID),url.toString()}}return _exports.default=Group,_exports.default})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/course/amd/build/actionbar/group.min.js.map b/course/amd/build/actionbar/group.min.js.map index c77ab0b4bfc05..d3c67427e0f00 100644 --- a/course/amd/build/actionbar/group.min.js.map +++ b/course/amd/build/actionbar/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/actionbar/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\nimport GroupSearch from 'core_group/comboboxsearch/group';\n\n/**\n * Allow the user to search for groups in the action bar.\n *\n * @module core_course/actionbar/group\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Group extends GroupSearch {\n\n baseUrl;\n\n /**\n * Construct the class.\n *\n * @param {string} baseUrl The base URL for the page.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(baseUrl, cmid = null) {\n super(cmid);\n this.baseUrl = baseUrl;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} baseUrl The base URL for the page.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n * @returns {Group}\n */\n static init(baseUrl, cmid = null) {\n return new Group(baseUrl, cmid);\n }\n\n /**\n * Build up the 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 const url = new URL(this.baseUrl);\n url.searchParams.set('groupsearchvalue', this.getSearchTerm());\n url.searchParams.set('group', groupID);\n\n return url.toString();\n }\n}\n"],"names":["Group","GroupSearch","constructor","baseUrl","selectOneLink","groupID","url","URL","this","searchParams","set","getSearchTerm","toString"],"mappings":";;;;;;;;MAwBqBA,cAAcC,eAU/BC,YAAYC,sEAAgB,4KAEnBA,QAAUA,oBAUPA,gBACD,IAAIH,MAAMG,+DADO,MAU5BC,cAAcC,eACJC,IAAM,IAAIC,IAAIC,KAAKL,gBACzBG,IAAIG,aAAaC,IAAI,mBAAoBF,KAAKG,iBAC9CL,IAAIG,aAAaC,IAAI,QAASL,SAEvBC,IAAIM"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/actionbar/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\nimport GroupSearch from 'core_group/comboboxsearch/group';\n\n/**\n * Allow the user to search for groups in the action bar.\n *\n * @module core_course/actionbar/group\n * @copyright 2024 Shamim Rezaie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class Group extends GroupSearch {\n\n baseUrl;\n\n /**\n * Construct the class.\n *\n * @param {string} baseUrl The base URL for the page.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(baseUrl, cmid = null) {\n super(cmid);\n this.baseUrl = baseUrl;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} baseUrl The base URL for the page.\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n * @returns {Group}\n */\n static init(baseUrl, cmid = null) {\n return new Group(baseUrl, cmid);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} groupID The ID of the group selected.\n * @param {Number} groupingID The ID of the grouping selected.\n * @returns {string}\n */\n selectOneLink(groupID, groupingID = 0) {\n const url = new URL(this.baseUrl);\n url.searchParams.set('groupsearchvalue', this.getSearchTerm());\n url.searchParams.set('group', groupID);\n if (groupingID > 0) {\n url.searchParams.set('grouping', groupingID);\n }\n\n return url.toString();\n }\n}\n"],"names":["Group","GroupSearch","constructor","baseUrl","selectOneLink","groupID","groupingID","url","URL","this","searchParams","set","getSearchTerm","toString"],"mappings":";;;;;;;;MAwBqBA,cAAcC,eAU/BC,YAAYC,sEAAgB,4KAEnBA,QAAUA,oBAUPA,gBACD,IAAIH,MAAMG,+DADO,MAW5BC,cAAcC,aAASC,kEAAa,QAC1BC,IAAM,IAAIC,IAAIC,KAAKN,gBACzBI,IAAIG,aAAaC,IAAI,mBAAoBF,KAAKG,iBAC9CL,IAAIG,aAAaC,IAAI,QAASN,SAC1BC,WAAa,GACbC,IAAIG,aAAaC,IAAI,WAAYL,YAG9BC,IAAIM"} \ No newline at end of file diff --git a/course/amd/src/actionbar/group.js b/course/amd/src/actionbar/group.js index 2f7ef4ea9f8a7..b0b3a38d1026e 100644 --- a/course/amd/src/actionbar/group.js +++ b/course/amd/src/actionbar/group.js @@ -52,12 +52,16 @@ export default class Group extends GroupSearch { * Build up the link that is dedicated to a particular result. * * @param {Number} groupID The ID of the group selected. + * @param {Number} groupingID The ID of the grouping selected. * @returns {string} */ - selectOneLink(groupID) { + selectOneLink(groupID, groupingID = 0) { const url = new URL(this.baseUrl); url.searchParams.set('groupsearchvalue', this.getSearchTerm()); url.searchParams.set('group', groupID); + if (groupingID > 0) { + url.searchParams.set('grouping', groupingID); + } return url.toString(); } diff --git a/grade/report/lib.php b/grade/report/lib.php index a49effe5746dd..ac01519e08f5b 100644 --- a/grade/report/lib.php +++ b/grade/report/lib.php @@ -433,6 +433,8 @@ public static function supports_mygrades() { * Sets up this object's group variables, mainly to restrict the selection of users to display. */ protected function setup_groups() { + global $DB; + // find out current groups mode if ($this->groupmode = groups_get_course_groupmode($this->course)) { if (empty($this->gpr->groupid)) { @@ -452,6 +454,15 @@ protected function setup_groups() { $this->groupsql = " JOIN {groups_members} gm ON gm.userid = u.id "; $this->groupwheresql = " AND gm.groupid = :gr_grpid "; $this->groupwheresql_params = array('gr_grpid'=>$this->currentgroup); + } else { + $currentgrouping = groups_get_course_grouping($this->course); + if ($currentgrouping) { + $allowedgroups = groups_get_course_allowed_groups($this->course, null, $currentgrouping); + [$insql, $inparams] = $DB->get_in_or_equal(array_keys($allowedgroups), SQL_PARAMS_NAMED, 'gr_grpid'); + $this->groupsql = " JOIN {groups_members} gm ON gm.userid = u.id "; + $this->groupwheresql = " AND gm.groupid $insql "; + $this->groupwheresql_params = $inparams; + } } } } diff --git a/group/amd/build/comboboxsearch/group.min.js b/group/amd/build/comboboxsearch/group.min.js index ee1a6951c64ba..66120767a5b90 100644 --- a/group/amd/build/comboboxsearch/group.min.js +++ b/group/amd/build/comboboxsearch/group.min.js @@ -1,3 +1,3 @@ -define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){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),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl}))))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){window.location=this.selectOneLink(e.target.value)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); +define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){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),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){let cmid=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"cmID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]',togglegroupingbutton:'[data-action="togglegrouping"]',togglegroupingtarget:".toggle-grouping-target"};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.instance=component.querySelector(this.selectors.instance).dataset.instance,this.cmID=cmid;const searchValueElement=this.component.querySelector("#".concat(this.searchInput.dataset.inputElement));searchValueElement.addEventListener("change",(()=>{this.toggleDropdown();const valueElement=this.component.querySelector("#".concat(this.combobox.dataset.inputElement));valueElement.value!==searchValueElement.value&&(valueElement.value=searchValueElement.value,valueElement.dispatchEvent(new Event("change",{bubbles:!0}))),searchValueElement.value=""})),this.$component.on("hide.bs.dropdown",(()=>{this.searchInput.removeAttribute("aria-activedescendant");const listbox=document.querySelector("#".concat(this.searchInput.getAttribute("aria-controls"),'[role="listbox"]'));listbox.querySelectorAll('.active[role="option"]').forEach((option=>{option.classList.remove("active")})),listbox.scrollTop=0,setTimeout((()=>{""!==this.searchInput.value&&(this.searchInput.value="",this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}))})),this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch(arguments.length>0&&void 0!==arguments[0]?arguments[0]:null)}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}async renderDropdown(){let groups=this.getMatchedResults(),groupings=[],groupWithoutGrouping=[];groups.forEach((group=>{0!==group.groupings.length?group.groupings.forEach((grouping=>{groupings[grouping.id]||(groupings[grouping.id]={id:grouping.id,name:grouping.name,allparticipantstext:grouping.allparticipantstext,groupingimageurl:grouping.groupingimageurl,groupingid:grouping.id,groups:[]}),groupings[grouping.id].groups.push(group)})):groupWithoutGrouping.push(group)})),groupings.forEach((grouping=>{grouping.groups.unshift({id:"0,"+grouping.id,name:grouping.allparticipantstext,groupimageurl:grouping.groupingimageurl,groupingid:grouping.id})})),groupings=groupings.filter((grouping=>void 0!==grouping));const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groupings:groupings,groupwithoutgrouping:groupWithoutGrouping,hasresults:groups.length>0,instance:this.instance,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js),this.searchInput.removeAttribute("aria-activedescendant")}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID,this.cmID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,groupimageurl:group.groupimageurl,groupings:group.groupings}))))}async clickHandler(e){if(e.target.closest(this.selectors.togglegroupingbutton)){e.stopPropagation();let toggleButton=e.target.closest(this.selectors.togglegroupingbutton),toggleTarget=document.querySelector(toggleButton.dataset.target);return toggleTarget.classList.toggle("show"),toggleTarget.scrollIntoView(!1),document.querySelectorAll(this.selectors.togglegroupingtarget).forEach((target=>{target!==toggleTarget&&target.classList.remove("show")})),void toggleButton.setAttribute("aria-expanded","true"===toggleButton.getAttribute("aria-expanded")?"false":"true")}e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe())}changeHandler(e){let groupID=e.target.value.split(",")[0],groupingID=e.target.value.split(",")[1]||0;window.location=this.selectOneLink(groupID,groupingID)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}selectOneLink(groupID){let groupingID=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;throw new Error("selectOneLink(".concat(groupID,", ").concat(groupingID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default})); //# sourceMappingURL=group.min.js.map \ No newline at end of file diff --git a/group/amd/build/comboboxsearch/group.min.js.map b/group/amd/build/comboboxsearch/group.min.js.map index 5a5c17602e515..0c0547f7d90fd 100644 --- a/group/amd/build/comboboxsearch/group.min.js.map +++ b/group/amd/build/comboboxsearch/group.min.js.map @@ -1 +1 @@ -{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/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.\n *\n * @module core_group/comboboxsearch/group\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 {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.$component.on('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groups: this.getMatchedResults(),\n hasresults: this.getMatchedResults().length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, 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 this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID, this.cmID).then((r) => r.groups);\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 // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\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 filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\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 window.location = this.selectOneLink(e.target.value);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\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} groupID The ID of the group selected.\n */\n selectOneLink(groupID) {\n throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","cmid","selectors","this","courseid","placeholder","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","html","js","groups","getMatchedResults","hasresults","length","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","filter","group","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","id","name","groupimageurl","e","target","closest","clearSearch","stopPropagation","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","window","location","selectOneLink","registerInputHandlers","async","groupID","Error"],"mappings":"+rBA4BqBA,oBAAoBC,yBAWrCC,kBAAYC,4DAAO,mIAPE,CAAC,KAAM,OAAQ,uBAS3BC,UAAY,IAAIC,KAAKD,UACtBE,SAAU,2BACVC,YAAa,gEAEXC,UAAYC,SAASC,cAAcL,KAAKM,0BACzCC,SAAWJ,UAAUE,cAAcL,KAAKD,UAAUE,UAAUO,QAAQP,cAEpEQ,SAAWN,UAAUE,cAAcL,KAAKD,UAAUU,UAAUD,QAAQC,cACpEC,KAAOZ,WAENa,mBAAqBX,KAAKG,UAAUE,yBAAkBL,KAAKY,YAAYJ,QAAQK,eACrFF,mBAAmBG,iBAAiB,UAAU,UACrCC,uBAECC,aAAehB,KAAKG,UAAUE,yBAAkBL,KAAKiB,SAAST,QAAQK,eACxEG,aAAaE,QAAUP,mBAAmBO,QAC1CF,aAAaE,MAAQP,mBAAmBO,MACxCF,aAAaG,cAAc,IAAIC,MAAM,SAAU,CAACC,SAAS,MAG7DV,mBAAmBO,MAAQ,WAG1BI,WAAWC,GAAG,oBAAoB,UAC9BX,YAAYY,gBAAgB,+BAE3BC,QAAUrB,SAASC,yBAAkBL,KAAKY,YAAYc,aAAa,sCACzED,QAAQE,iBAAiB,0BAA0BC,SAAQC,SACvDA,OAAOC,UAAUC,OAAO,aAE5BN,QAAQO,UAAY,EAGpBC,YAAW,KACwB,KAA3BjC,KAAKY,YAAYM,aACZN,YAAYM,MAAQ,QACpBN,YAAYO,cAAc,IAAIC,MAAM,QAAS,CAACC,SAAS,iBAKnEa,gBAAgBC,MAAMC,sBAAaC,gCASjC,IAAI1C,mEADI,MASnBW,0BACW,gBAQXgC,yBACW,oDAODC,KAACA,KAADC,GAAOA,UAAY,+BAAiB,sCAAuC,CAC7EC,OAAQzC,KAAK0C,oBACbC,WAAY3C,KAAK0C,oBAAoBE,OAAS,EAC9CnC,SAAUT,KAAKS,SACfoC,WAAY7C,KAAK8C,qDAED9C,KAAKD,UAAUG,YAAaqC,KAAMC,SAEjD5B,YAAYY,gBAAgB,oDAO5BuB,wBAAwB/C,KAAKgD,oBAAoBhD,KAAKiD,oBACtDC,2BAEClD,KAAKmD,sBAENC,gDASQ,0BAAWpD,KAAKO,SAAUP,KAAKU,MAAM2C,MAAMC,GAAMA,EAAEb,6BAShDc,sBAEoB,KAAhCvD,KAAKwD,uBACED,eAEJA,eAAeE,QAAQC,OAAUC,OAAOC,KAAKF,OAAOG,MAAMC,KAC1C,KAAfJ,MAAMI,OAAe9D,KAAK+D,mBAAmBC,SAASF,MAGnDJ,MAAMI,KAAKG,WAAWC,cAAcF,SAAShE,KAAKwD,4BAOjEN,0BACSH,kBACD/C,KAAK0C,oBAAoByB,KAAKT,QACnB,CACHU,GAAIV,MAAMU,GACVC,KAAMX,MAAMW,KACZC,cAAeZ,MAAMY,sCAWlBC,GACXA,EAAEC,OAAOC,QAAQzE,KAAKD,UAAU2E,eAChCH,EAAEI,uBAEG/D,YAAYM,MAAQ,QACpB0D,eAAe5E,KAAKY,YAAYM,YAChCN,YAAYiE,aACZC,kBAAkBhD,UAAUiD,IAAI,gBAE/B/E,KAAKgF,oBASnBC,cAAcV,GACVW,OAAOC,SAAWnF,KAAKoF,cAAcb,EAAEC,OAAOtD,OAMlDmE,6BAESzE,YAAYE,iBAAiB,SAAS,oBAASwE,eAC3CV,eAAe5E,KAAKY,YAAYM,OAER,KAAzBlB,KAAK8C,qBAEAgC,kBAAkBhD,UAAUiD,IAAI,eAGhCD,kBAAkBhD,UAAUC,OAAO,gBAGtC/B,KAAKgF,qBACZ,MASPI,cAAcG,eACJ,IAAIC,8BAAuBD,4CAAmCvF,KAAKH,YAAYwE"} \ No newline at end of file +{"version":3,"file":"group.min.js","sources":["../../src/comboboxsearch/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.\n *\n * @module core_group/comboboxsearch/group\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 {groupFetch} from 'core_group/comboboxsearch/repository';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\nimport {debounce} from 'core/utils';\nimport Notification from 'core/notification';\n\nexport default class GroupSearch extends search_combobox {\n\n courseID;\n cmID;\n bannedFilterFields = ['id', 'link', 'groupimageurl'];\n\n /**\n * Construct the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n constructor(cmid = null) {\n super();\n this.selectors = {...this.selectors,\n courseid: '[data-region=\"courseid\"]',\n placeholder: '.groupsearchdropdown [data-region=\"searchplaceholder\"]',\n togglegroupingbutton: '[data-action=\"togglegrouping\"]',\n togglegroupingtarget: '.toggle-grouping-target',\n };\n const component = document.querySelector(this.componentSelector());\n this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid;\n // Override the instance since the body is built outside the constructor for the combobox.\n this.instance = component.querySelector(this.selectors.instance).dataset.instance;\n this.cmID = cmid;\n\n const searchValueElement = this.component.querySelector(`#${this.searchInput.dataset.inputElement}`);\n searchValueElement.addEventListener('change', () => {\n this.toggleDropdown(); // Otherwise the dropdown stays open when user choose an option using keyboard.\n\n const valueElement = this.component.querySelector(`#${this.combobox.dataset.inputElement}`);\n if (valueElement.value !== searchValueElement.value) {\n valueElement.value = searchValueElement.value;\n valueElement.dispatchEvent(new Event('change', {bubbles: true}));\n }\n\n searchValueElement.value = '';\n });\n\n this.$component.on('hide.bs.dropdown', () => {\n this.searchInput.removeAttribute('aria-activedescendant');\n\n const listbox = document.querySelector(`#${this.searchInput.getAttribute('aria-controls')}[role=\"listbox\"]`);\n listbox.querySelectorAll('.active[role=\"option\"]').forEach(option => {\n option.classList.remove('active');\n });\n listbox.scrollTop = 0;\n\n // Use setTimeout to make sure the following code is executed after the click event is handled.\n setTimeout(() => {\n if (this.searchInput.value !== '') {\n this.searchInput.value = '';\n this.searchInput.dispatchEvent(new Event('input', {bubbles: true}));\n }\n });\n });\n\n this.renderDefault().catch(Notification.exception);\n }\n\n /**\n * Initialise an instance of the class.\n *\n * @param {int|null} cmid ID of the course module initiating the group search (optional).\n */\n static init(cmid = null) {\n return new GroupSearch(cmid);\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n componentSelector() {\n return '.group-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n dropdownSelector() {\n return '.groupsearchdropdown';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n let groups = this.getMatchedResults();\n\n // Go through the groups and organise them by groupings.\n let groupings = [];\n\n // Groups without groupings.\n let groupWithoutGrouping = [];\n\n groups.forEach((group) => {\n // If the group has no groupings, add it to the group without grouping list.\n if (group.groupings.length === 0) {\n groupWithoutGrouping.push(group);\n return;\n }\n\n // Otherwise, add it to the groupings object.\n group.groupings.forEach((grouping) => {\n if (!groupings[grouping.id]) {\n groupings[grouping.id] = {\n id: grouping.id,\n name: grouping.name,\n allparticipantstext: grouping.allparticipantstext,\n groupingimageurl: grouping.groupingimageurl,\n groupingid: grouping.id,\n groups: [],\n };\n }\n groupings[grouping.id].groups.push(group);\n });\n });\n\n // Add \"All participants\" option to each grouping.\n groupings.forEach((grouping) => {\n grouping.groups.unshift({\n id: 0 + ',' + grouping.id,\n name: grouping.allparticipantstext,\n groupimageurl: grouping.groupingimageurl,\n groupingid: grouping.id,\n });\n });\n\n // We used grouping id as the key, the array will have undefined values.\n // Hence, we replace the keys here.\n groupings = groupings.filter((grouping) => grouping !== undefined);\n\n const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', {\n groupings: groupings,\n groupwithoutgrouping: groupWithoutGrouping,\n hasresults: groups.length > 0,\n instance: this.instance,\n searchterm: this.getSearchTerm(),\n });\n replaceNodeContents(this.selectors.placeholder, 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 this.updateNodes();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n async fetchDataset() {\n return await groupFetch(this.courseID, this.cmID).then((r) => r.groups);\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 // Sometimes we just want to show everything.\n if (this.getPreppedSearchTerm() === '') {\n return filterableData;\n }\n return filterableData.filter((group) => Object.keys(group).some((key) => {\n if (group[key] === \"\" || this.bannedFilterFields.includes(key)) {\n return false;\n }\n return group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\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 filterMatchDataset() {\n this.setMatchedResults(\n this.getMatchedResults().map((group) => {\n return {\n id: group.id,\n name: group.name,\n groupimageurl: group.groupimageurl,\n groupings: group.groupings,\n };\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n async clickHandler(e) {\n // Toggle a grouping.\n if (e.target.closest(this.selectors.togglegroupingbutton)) {\n e.stopPropagation();\n\n // Toggle button.\n let toggleButton = e.target.closest(this.selectors.togglegroupingbutton);\n\n // Find the target which we want to hide or show.\n let toggleTarget = document.querySelector(toggleButton.dataset.target);\n\n // Toggle show class.\n toggleTarget.classList.toggle('show');\n\n // Scroll to the top of the grouping.\n toggleTarget.scrollIntoView(false);\n\n // Hide all other groupings.\n document.querySelectorAll(this.selectors.togglegroupingtarget).forEach((target) => {\n if (target !== toggleTarget) {\n target.classList.remove('show');\n }\n });\n\n // Toggle the aria-expanded attribute.\n toggleButton.setAttribute('aria-expanded', toggleButton.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');\n\n return;\n }\n\n if (e.target.closest(this.selectors.clearSearch)) {\n e.stopPropagation();\n // Clear the entered search query in the search bar.\n this.searchInput.value = '';\n this.setSearchTerms(this.searchInput.value);\n this.searchInput.focus();\n this.clearSearchButton.classList.add('d-none');\n // Display results.\n await this.filterrenderpipe();\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 let groupID = e.target.value.split(',')[0];\n let groupingID = e.target.value.split(',')[1] || 0;\n window.location = this.selectOneLink(groupID, groupingID);\n }\n\n /**\n * Override the input event listener for the text input area.\n */\n registerInputHandlers() {\n // Register & handle the text input.\n this.searchInput.addEventListener('input', debounce(async() => {\n this.setSearchTerms(this.searchInput.value);\n // We can also require a set amount of input before search.\n if (this.getSearchTerm() === '') {\n // Hide the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.add('d-none');\n } else {\n // Display the \"clear\" search button in the search bar.\n this.clearSearchButton.classList.remove('d-none');\n }\n // User has given something for us to filter against.\n await this.filterrenderpipe();\n }, 300));\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} groupID The ID of the group selected.\n * @param {Number} groupingID The ID of the grouping selected.\n */\n selectOneLink(groupID, groupingID = 0) {\n throw new Error(`selectOneLink(${groupID}, ${groupingID}) must be implemented in ${this.constructor.name}`);\n }\n}\n"],"names":["GroupSearch","search_combobox","constructor","cmid","selectors","this","courseid","placeholder","togglegroupingbutton","togglegroupingtarget","component","document","querySelector","componentSelector","courseID","dataset","instance","cmID","searchValueElement","searchInput","inputElement","addEventListener","toggleDropdown","valueElement","combobox","value","dispatchEvent","Event","bubbles","$component","on","removeAttribute","listbox","getAttribute","querySelectorAll","forEach","option","classList","remove","scrollTop","setTimeout","renderDefault","catch","Notification","exception","dropdownSelector","groups","getMatchedResults","groupings","groupWithoutGrouping","group","length","grouping","id","name","allparticipantstext","groupingimageurl","groupingid","push","unshift","groupimageurl","filter","undefined","html","js","groupwithoutgrouping","hasresults","searchterm","getSearchTerm","setMatchedResults","filterDataset","getDataset","filterMatchDataset","renderDropdown","updateNodes","then","r","filterableData","getPreppedSearchTerm","Object","keys","some","key","bannedFilterFields","includes","toString","toLowerCase","map","e","target","closest","stopPropagation","toggleButton","toggleTarget","toggle","scrollIntoView","setAttribute","clearSearch","setSearchTerms","focus","clearSearchButton","add","filterrenderpipe","changeHandler","groupID","split","groupingID","window","location","selectOneLink","registerInputHandlers","async","Error"],"mappings":"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"} \ No newline at end of file diff --git a/group/amd/src/comboboxsearch/group.js b/group/amd/src/comboboxsearch/group.js index 7789ebbd79f04..e52f55d5344c9 100644 --- a/group/amd/src/comboboxsearch/group.js +++ b/group/amd/src/comboboxsearch/group.js @@ -42,6 +42,8 @@ export default class GroupSearch extends search_combobox { this.selectors = {...this.selectors, courseid: '[data-region="courseid"]', placeholder: '.groupsearchdropdown [data-region="searchplaceholder"]', + togglegroupingbutton: '[data-action="togglegrouping"]', + togglegroupingtarget: '.toggle-grouping-target', }; const component = document.querySelector(this.componentSelector()); this.courseID = component.querySelector(this.selectors.courseid).dataset.courseid; @@ -114,9 +116,55 @@ export default class GroupSearch extends search_combobox { * Build the content then replace the node. */ async renderDropdown() { + let groups = this.getMatchedResults(); + + // Go through the groups and organise them by groupings. + let groupings = []; + + // Groups without groupings. + let groupWithoutGrouping = []; + + groups.forEach((group) => { + // If the group has no groupings, add it to the group without grouping list. + if (group.groupings.length === 0) { + groupWithoutGrouping.push(group); + return; + } + + // Otherwise, add it to the groupings object. + group.groupings.forEach((grouping) => { + if (!groupings[grouping.id]) { + groupings[grouping.id] = { + id: grouping.id, + name: grouping.name, + allparticipantstext: grouping.allparticipantstext, + groupingimageurl: grouping.groupingimageurl, + groupingid: grouping.id, + groups: [], + }; + } + groupings[grouping.id].groups.push(group); + }); + }); + + // Add "All participants" option to each grouping. + groupings.forEach((grouping) => { + grouping.groups.unshift({ + id: 0 + ',' + grouping.id, + name: grouping.allparticipantstext, + groupimageurl: grouping.groupingimageurl, + groupingid: grouping.id, + }); + }); + + // We used grouping id as the key, the array will have undefined values. + // Hence, we replace the keys here. + groupings = groupings.filter((grouping) => grouping !== undefined); + const {html, js} = await renderForPromise('core_group/comboboxsearch/resultset', { - groups: this.getMatchedResults(), - hasresults: this.getMatchedResults().length > 0, + groupings: groupings, + groupwithoutgrouping: groupWithoutGrouping, + hasresults: groups.length > 0, instance: this.instance, searchterm: this.getSearchTerm(), }); @@ -175,6 +223,7 @@ export default class GroupSearch extends search_combobox { id: group.id, name: group.name, groupimageurl: group.groupimageurl, + groupings: group.groupings, }; }) ); @@ -186,6 +235,35 @@ export default class GroupSearch extends search_combobox { * @param {MouseEvent} e The triggering event that we are working with. */ async clickHandler(e) { + // Toggle a grouping. + if (e.target.closest(this.selectors.togglegroupingbutton)) { + e.stopPropagation(); + + // Toggle button. + let toggleButton = e.target.closest(this.selectors.togglegroupingbutton); + + // Find the target which we want to hide or show. + let toggleTarget = document.querySelector(toggleButton.dataset.target); + + // Toggle show class. + toggleTarget.classList.toggle('show'); + + // Scroll to the top of the grouping. + toggleTarget.scrollIntoView(false); + + // Hide all other groupings. + document.querySelectorAll(this.selectors.togglegroupingtarget).forEach((target) => { + if (target !== toggleTarget) { + target.classList.remove('show'); + } + }); + + // Toggle the aria-expanded attribute. + toggleButton.setAttribute('aria-expanded', toggleButton.getAttribute('aria-expanded') === 'true' ? 'false' : 'true'); + + return; + } + if (e.target.closest(this.selectors.clearSearch)) { e.stopPropagation(); // Clear the entered search query in the search bar. @@ -204,7 +282,9 @@ export default class GroupSearch extends search_combobox { * @param {Event} e The change event. */ changeHandler(e) { - window.location = this.selectOneLink(e.target.value); + let groupID = e.target.value.split(',')[0]; + let groupingID = e.target.value.split(',')[1] || 0; + window.location = this.selectOneLink(groupID, groupingID); } /** @@ -232,8 +312,9 @@ export default class GroupSearch extends search_combobox { * We will call this function when a user interacts with the combobox to redirect them to show their results in the page. * * @param {Number} groupID The ID of the group selected. + * @param {Number} groupingID The ID of the grouping selected. */ - selectOneLink(groupID) { - throw new Error(`selectOneLink(${groupID}) must be implemented in ${this.constructor.name}`); + selectOneLink(groupID, groupingID = 0) { + throw new Error(`selectOneLink(${groupID}, ${groupingID}) must be implemented in ${this.constructor.name}`); } } diff --git a/group/classes/external/get_groups_for_selector.php b/group/classes/external/get_groups_for_selector.php index cb2a9ee40c245..d46b5083a9a2f 100644 --- a/group/classes/external/get_groups_for_selector.php +++ b/group/classes/external/get_groups_for_selector.php @@ -139,10 +139,26 @@ public static function execute(int $courseid, ?int $cmid = null): array { $picture = $OUTPUT->image_url('g/g1'); } + // Get the groupings the group belongs to. + $groupings = groups_get_groupings_by_group($group->id); + + // Add all participant string and image url to groupings array. + $groupings = array_map(function($grouping) { + global $OUTPUT; + return (object) [ + 'id' => $grouping->id, + 'name' => $grouping->name, + 'allparticipantstext' => get_string('allparticipants'), + 'groupingimageurl' => $OUTPUT->image_url('g/g1')->out(false), + ]; + }, $groupings); + + return (object) [ 'id' => $group->id, 'name' => format_string($group->name, true, ['context' => $context]), 'groupimageurl' => $picture->out(false), + 'groupings' => $groupings, ]; }, $groupsmenu); } @@ -175,6 +191,12 @@ public static function group_description(): external_description { 'id' => new external_value(PARAM_ALPHANUM, 'An ID for the group', VALUE_REQUIRED), 'name' => new external_value(PARAM_TEXT, 'The full name of the group', VALUE_REQUIRED), 'groupimageurl' => new external_value(PARAM_URL, 'Group image URL', VALUE_OPTIONAL), + 'groupings' => new external_multiple_structure( new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'Grouping ID', VALUE_REQUIRED), + 'name' => new external_value(PARAM_TEXT, 'Grouping name', VALUE_REQUIRED), + 'allparticipantstext' => new external_value(PARAM_TEXT, 'All participants string', VALUE_REQUIRED), + 'groupingimageurl' => new external_value(PARAM_URL, 'Grouping image URL', VALUE_REQUIRED), + ])), ]; return new external_single_structure($groupfields); } diff --git a/group/templates/comboboxsearch/resultset.mustache b/group/templates/comboboxsearch/resultset.mustache index fe077b9841d0b..10768f157c40c 100644 --- a/group/templates/comboboxsearch/resultset.mustache +++ b/group/templates/comboboxsearch/resultset.mustache @@ -24,16 +24,50 @@ Example context (json): { - "groups": [ + "groupwithoutgrouping": [ { "id": 2, "name": "Foo bar", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2" }, { "id": 3, "name": "Bar Foo", - "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3" + } + ], + "groupings": [ + { + "id": 1, + "name": "Grouping 1", + "groups": [ + { + "id": 4, + "name": "Foo bar", + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=4" + }, + { + "id": 5, + "name": "Bar Foo", + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=5" + } + ] + }, + { + "id": 2, + "name": "Grouping 2", + "groups": [ + { + "id": 6, + "name": "Foo bar", + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=6" + }, + { + "id": 7, + "name": "Bar Foo", + "groupimageurl": "http://foo.bar/grade/report/grader/index.php?id=42&userid=7" + } + ] } ], "instance": 25, @@ -44,8 +78,26 @@ {{core_group/comboboxsearch/resultitem}} - {{/groups}} + {{/groupwithoutgrouping}} + + {{#groupings}} + + + +
+ {{#groups}} + {{>core_group/comboboxsearch/resultitem}} + {{/groups}} +
+ {{/groupings}} {{/results}} {{/core/local/comboboxsearch/resultset}} diff --git a/lib/grouplib.php b/lib/grouplib.php index 730887178d3e3..92968226c99f1 100644 --- a/lib/grouplib.php +++ b/lib/grouplib.php @@ -1065,6 +1065,24 @@ function groups_get_course_group($course, $update=false, $allowedgroups=null) { return $SESSION->activegroup[$course->id][$groupmode][$course->defaultgroupingid]; } +/** + * Get course grouping id. + * + * @param stdClass $course course object + * + */ +function groups_get_course_grouping($course) { + global $SESSION; + + if (!$groupmode = groups_get_course_groupmode($course)) { + return 0; + } + + $activegrouping = optional_param('grouping', 0, PARAM_INT); + + return $activegrouping; +} + /** * Returns group active in activity, changes the group by default if 'group' page param present * @@ -1117,6 +1135,53 @@ function groups_get_activity_group($cm, $update=false, $allowedgroups=null) { return $SESSION->activegroup[$cm->course][$groupmode][$cm->groupingid]; } +/** + * Get activity grouping id. + * + * @param stdClass|cm_info $cm course module object + */ +function groups_get_activity_grouping($cm) { + global $SESSION; + + if (!$groupmode = groups_get_activity_groupmode($cm)) { + return 0; + } + + $activegrouping = optional_param('grouping', 0, PARAM_INT); + + return $activegrouping; +} + +/** + * Get course allowed groups. + * + * @param stdClass $courseid course object + * @param int $userid user id + * @param int $groupingid grouping id + */ +function groups_get_course_allowed_groups($course, $userid = 0, $groupingid = 0) { + // Use current user by default + global $USER; + + if(!$userid) { + $userid = $USER->id; + } + + // Group mode for course + $groupmode = groups_get_course_groupmode($course); + + $context = context_course::instance($course->id); + if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $context, $userid)) { + // User has access to all groups. + $userid = 0; + } + + + return groups_get_all_groups($course->id, $userid, $groupingid); +} + + + /** * Gets a list of groups that the user is allowed to access within the * specified activity. @@ -1124,9 +1189,10 @@ function groups_get_activity_group($cm, $update=false, $allowedgroups=null) { * @category group * @param stdClass|cm_info $cm Course-module * @param int $userid User ID (defaults to current user) + * @param int $groupingid Grouping ID (defaults to 0) * @return array An array of group objects, or false if none */ -function groups_get_activity_allowed_groups($cm,$userid=0) { +function groups_get_activity_allowed_groups($cm,$userid = 0, $groupingid = 0) { // Use current user by default global $USER; if(!$userid) { @@ -1139,12 +1205,15 @@ function groups_get_activity_allowed_groups($cm,$userid=0) { // If visible groups mode, or user has the accessallgroups capability, // then they can access all groups for the activity... $context = context_module::instance($cm->id); - if ($groupmode == VISIBLEGROUPS or has_capability('moodle/site:accessallgroups', $context, $userid)) { - return groups_get_all_groups($cm->course, 0, $cm->groupingid, 'g.*', false, true); - } else { - // ...otherwise they can only access groups they belong to - return groups_get_all_groups($cm->course, $userid, $cm->groupingid, 'g.*', false, true); + if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $context, $userid)) { + // User has access to all groups. + $userid = 0; } + + // If grouping is specified, then we get all groups for the grouping. + $groupingid = $groupingid > 0 ? $groupingid : $cm->groupingid; + + return groups_get_all_groups($cm->course, $userid, $groupingid); } /** @@ -1653,3 +1722,20 @@ function groups_get_activity_shared_group_members($cm, $userid = null) { } return groups_get_groups_members($groupsids); } + +/** + * Return groupings which a group belongs to. + * + * @param int $groupid The group id. + * @return array list of groupings the group belongs to. + */ +function groups_get_groupings_by_group(int $groupid): array { + global $DB; + + $sql = "SELECT g.id, g.name + FROM {groupings_groups} gg + JOIN {groupings} g ON gg.groupingid = g.id + WHERE gg.groupid = :groupid"; + + return $DB->get_records_sql($sql, ['groupid' => $groupid]); +} diff --git a/mod/assign/gradingtable.php b/mod/assign/gradingtable.php index cc69aa0691a1d..bcff5dd5a4723 100644 --- a/mod/assign/gradingtable.php +++ b/mod/assign/gradingtable.php @@ -108,9 +108,6 @@ public function __construct(assign $assignment, $url = new moodle_url($CFG->wwwroot . '/mod/assign/view.php', $urlparams); $this->define_baseurl($url); - // Do some business - then set the sql. - $currentgroup = groups_get_activity_group($assignment->get_course_module(), true); - if ($rowoffset) { $this->rownum = $rowoffset - 1; } @@ -121,7 +118,7 @@ public function __construct(assign $assignment, // string with the full name of the selected user. $usersearch = $userid ? fullname(\core_user::get_user($userid)) : optional_param('search', '', PARAM_NOTAGS); $assignment->set_usersearch($userid, $groupid, $usersearch); - $users = array_keys( $assignment->list_participants($currentgroup, true)); + $users = $assignment->list_grouping_participants(); if (count($users) == 0) { // Insert a record that will never match to the sql is still valid. $users[] = -1; diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index cd05752a2e8f8..0e9afa789e58d 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -4621,8 +4621,8 @@ protected function view_grading_table() { $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist; } - $currentgroup = groups_get_activity_group($this->get_course_module(), true); - $users = array_keys($this->list_participants($currentgroup, true)); + $users = $this->list_grouping_participants(); + if (count($users) != 0 && $this->can_grade()) { $jsparams = []; $jsparams['message'] = !empty($CFG->messaging) @@ -4662,6 +4662,33 @@ protected function view_grading_table() { return $o; } + /** + * List participants in a groupings, optional filter by a group. + * + * @param int $idsonly If true, return only user ids, otherwise return full user records. + * @param bool $tablesort If true, sort the table + */ + public function list_grouping_participants($idsonly = true, $tablesort = false) { + global $USER; + + $currentgroup = groups_get_activity_group($this->get_course_module(), true); + $currentgrouping = groups_get_activity_grouping($this->get_course_module()); + + if ($currentgroup == 0 && ($currentgrouping > 0)) { + // Find all groups in the grouping. + $groups = groups_get_activity_allowed_groups($this->get_course_module(), $USER->id, $currentgrouping); + + // Get users from all groups in the grouping. + $users = []; + foreach ($groups as $group) { + $users = array_merge($users, array_keys($this->list_participants($group->id, $idsonly, $tablesort))); + } + } else { + $users = array_keys($this->list_participants($currentgroup, $idsonly, $tablesort)); + } + return $users; + } + /** * View entire grader app. *