MDL-72509 core: De-couple participants filter from core_user to core

This implementation will de-couple the participant filter from
core user to core so its easily usable to any api in core or any
community plugin. This removes the dependency from the core_user
and creates a nice api where it can produce filterable objects.

Co-Authored-By: Safat Shahin <safatshahin@catalyst-au.net>
Co-Authored-By: Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>

AMOS BEGIN
 MOV [addcondition,core_user], [addcondition,core]
 MOV [adverbfor_and,core_user], [operator_and,core]
 MOV [adverbfor_andnot,core_user], [operator_andnot,core]
 MOV [adverbfor_or,core_user], [operator_or,core]
 MOV [applyfilters,core_user], [applyfilters,core]
 MOV [clearfilterrow,core_user], [clearfilterrow,core]
 MOV [clearfilters,core_user], [clearfilters,core]
 MOV [filtersetmatchdescription,core_user], [filtersetmatchdescription,core]
 MOV [filterrowlegend,core_user], [filterrowlegend,core]
 MOV [filtertype,core_user], [filtertype,core]
 MOV [match,core_user], [match,core]
 MOV [matchofthefollowing,core_user], [matchofthefollowing,core]
 MOV [placeholdertypeorselect,core_user], [placeholdertypeorselect,core]
 MOV [selectfiltertype,core_user], [selectfiltertype,core]
AMOS END
This commit is contained in:
Tomo Tsuyuki 2021-10-26 16:08:27 +11:00 committed by Jun Pataleta
parent 8f492a836a
commit 68a5034b67
49 changed files with 948 additions and 704 deletions

View File

@ -57,3 +57,17 @@ notflagged,core_question
cannotswitcheditmodeon,core_error
multilangforceold,core_admin
nopermissionmove,core_question
addcondition,core_user
adverbfor_and,core_user
adverbfor_andnot,core_user
adverbfor_or,core_user
applyfilters,core_user
clearfilterrow,core_user
clearfilters,core_user
filtersetmatchdescription,core_user
filterrowlegend,core_user
filtertype,core_user
match,core_user
matchofthefollowing,core_user
placeholdertypeorselect,core_user
selectfiltertype,core_user

View File

@ -52,6 +52,7 @@ $string['addactivitytosection'] = 'Add an activity to section \'{$a}\'';
$string['addadmin'] = 'Add admin';
$string['addblock'] = 'Add a block';
$string['addcomment'] = 'Add a comment...';
$string['addcondition'] = 'Add condition';
$string['addcountertousername'] = 'Create user by adding number to username';
$string['addcreator'] = 'Add course creator';
$string['adddots'] = 'Add...';
@ -153,6 +154,7 @@ $string['answer'] = 'Answer';
$string['any'] = 'Any';
$string['approve'] = 'Approve';
$string['appearance'] = 'Appearance';
$string['applyfilters'] = 'Apply filters';
$string['areyousure'] = 'Are you sure?';
$string['areyousuretorestorethis'] = 'Do you want to continue?';
$string['areyousuretorestorethisinfo'] = 'Later in this process you will have a choice of adding this backup to an existing course or creating a completely new course.';
@ -255,6 +257,8 @@ $string['changesmadereallygoaway'] = 'You have made changes. Are you sure you wa
$string['city'] = 'City/town';
$string['cleaningtempdata'] = 'Cleaning temp data';
$string['clear'] = 'Clear';
$string['clearfilterrow'] = 'Remove filter row';
$string['clearfilters'] = 'Clear filters';
$string['clearsearch'] = 'Clear search input';
$string['clickhelpiconformoreinfo'] = '... continues ... Click on the help icon to read the full article';
$string['clickhere'] = 'Click here ...';
@ -880,6 +884,7 @@ $string['filesfolders'] = 'Files/folders';
$string['fileuploadwithcontent'] = 'File uploads should not include the content parameter';
$string['filloutallfields'] = 'Please fill out all fields in this form';
$string['filter'] = 'Filter';
$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
$string['findmorecourses'] = 'Find more courses...';
$string['first'] = 'First';
$string['firstaccess'] = 'First access';
@ -1070,7 +1075,9 @@ $string['courseenddateenabled_desc'] = 'This setting determines if the course en
$string['eventcontentviewed'] = 'Content viewed';
$string['filter'] = 'Filter';
$string['filteroption'] = '{$a->criteria}: {$a->value}';
$string['filterrowlegend'] = 'Filter {$a}';
$string['filters'] = 'Filters';
$string['filtertype'] = 'Filter type';
$string['icon'] = 'Icon';
$string['idnumber'] = 'ID number';
$string['idnumbercourse'] = 'Course ID number';
@ -1244,7 +1251,9 @@ $string['manageroles'] = 'Roles and permissions';
$string['markallread'] = 'Mark all as read';
$string['markedthistopic'] = 'This topic is highlighted as the current topic';
$string['markthistopic'] = 'Highlight this topic as the current topic';
$string['match'] = 'Match';
$string['matchingsearchandrole'] = 'Matching \'{$a->search}\' and {$a->role}';
$string['matchofthefollowing'] = 'of the following:';
$string['maxareabytesreached'] = 'The file (or the total size of several files) is larger than the space remaining in this area.';
$string['maxsectionslimit'] = 'Cannot create new section as it would exceed the maximum number of sections allowed for this course ({$a}).';
$string['maxfilesize'] = 'Maximum size for new files: {$a}';
@ -1575,6 +1584,9 @@ $string['olduserdirectory'] = 'This is the OLD users directory, and is no longer
$string['opendrawerblocks'] = 'Open block drawer';
$string['opendrawerindex'] = 'Open course index';
$string['opensinnewwindow'] = 'Opens in new window';
$string['operator_and'] = 'and';
$string['operator_andnot'] = 'and';
$string['operator_or'] = 'or';
$string['optional'] = 'optional';
$string['options'] = 'options';
$string['order'] = 'Order';
@ -1639,6 +1651,7 @@ $string['phone2'] = 'Mobile phone';
$string['phpinfo'] = 'PHP info';
$string['pictureof'] = 'Picture of {$a}';
$string['pictureofuser'] = 'User picture';
$string['placeholdertypeorselect'] = 'Type or select...';
$string['pleaseclose'] = 'Please close this window now.';
$string['pleasesearchmore'] = 'Please search some more';
$string['pleaseusesearch'] = 'Please use the search';
@ -1923,6 +1936,7 @@ $string['selectedfile'] = 'Selected file';
$string['selectedcategories'] = 'Selected categories';
$string['selectednowmove'] = '{$a} files selected for moving. Now go into the destination folder and press \'Move files to here\'';
$string['selectfiles'] = 'Select files';
$string['selectfiltertype'] = 'Select';
$string['selectitem'] = 'Select \'{$a}\'';
$string['selectmoduletoviewhelp'] = 'Select an activity or resource to view its help.

View File

@ -22,23 +22,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['addcondition'] = 'Add condition';
$string['adverbfor_and'] = 'and';
$string['adverbfor_andnot'] = 'and';
$string['adverbfor_or'] = 'or';
$string['applyfilters'] = 'Apply filters';
$string['clearfilterrow'] = 'Remove filter row';
$string['clearfilters'] = 'Clear filters';
$string['clearfilterselection'] = 'Remove "{$a}" from filter';
$string['countparticipantsfound'] = '{$a} participants found';
$string['filterrowlegend'] = 'Filter {$a}';
$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
$string['filtertype'] = 'Filter type';
$string['match'] = 'Match';
$string['matchofthefollowing'] = 'of the following:';
$string['moodlenetprofile'] = 'MoodleNet profile ID';
$string['moodlenetprofile_help'] = 'Your MoodleNet profile ID links your MoodleNet profile with this site.';
$string['placeholdertypeorselect'] = 'Type or select...';
$string['placeholdertype'] = 'Type...';
$string['privacy:courserequestpath'] = 'Requested courses';
$string['privacy:descriptionpath'] = 'Profile description';
@ -146,7 +132,6 @@ $string['privacy:profileimagespath'] = 'Profile images';
$string['privacy:privatefilespath'] = 'Private files';
$string['privacy:sessionpath'] = 'Session data';
$string['filterbykeyword'] = 'Keyword';
$string['selectfiltertype'] = 'Select';
$string['supportmessagesent'] = 'Your message has been sent.';
$string['supportmessagesentforloggedoutuser'] = 'Be careful with this message. The sender was not logged in, so their identity has not been confirmed.';
$string['supportmessagenotsent'] = "Unfortunately your message could not be sent.";
@ -154,3 +139,20 @@ $string['supportmessagealternative'] = 'Instead you can email {$a}.';
$string['target:upcomingactivitiesdue'] = 'Upcoming activities due';
$string['target:upcomingactivitiesdue_help'] = 'This target generates reminders for upcoming activities due.';
$string['target:upcomingactivitiesdueinfo'] = 'All upcoming activities due insights are listed here. These students have received these insights directly.';
// Deprecated since Moodle 4.1.
$string['addcondition'] = 'Add condition';
$string['adverbfor_and'] = 'and';
$string['adverbfor_andnot'] = 'and';
$string['adverbfor_or'] = 'or';
$string['applyfilters'] = 'Apply filters';
$string['clearfilterrow'] = 'Remove filter row';
$string['clearfilters'] = 'Clear filters';
$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
$string['filterrowlegend'] = 'Filter {$a}';
$string['filtertype'] = 'Filter type';
$string['match'] = 'Match';
$string['matchofthefollowing'] = 'of the following:';
$string['selectfiltertype'] = 'Select';
$string['placeholdertypeorselect'] = 'Type or select...';
$string['clearfilterselection'] = 'Remove "{$a}" from filter';

3
lib/amd/build/datafilter.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
define("core/datafilter/filtertype",["exports","core/form-autocomplete","core/datafilter/selectors","core/str"],(function(_exports,_formAutocomplete,_selectors,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Base Filter class for a filter type in the filter UI.
*
* @module core/datafilter/filtertype
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_formAutocomplete=_interopRequireDefault(_formAutocomplete),_selectors=_interopRequireDefault(_selectors);return _exports.default=class{constructor(filterType,rootNode,initialValues){this.filterType=filterType,this.rootNode=rootNode,this.addValueSelector(initialValues)}tearDown(){}get placeholder(){return(0,_str.get_string)("placeholdertypeorselect","core")}get showSuggestions(){return!0}async addValueSelector(){let initialValues=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const filterValueNode=this.getFilterValueNode(),sourceDataNode=this.getSourceDataForFilter();if(!sourceDataNode)return;filterValueNode.innerHTML=sourceDataNode.outerHTML;const dataSource=filterValueNode.querySelector("select");dataSource.id="filter-value-"+dataSource.getAttribute("data-field-name");const filterValueLabel=document.createElement("label");filterValueLabel.setAttribute("for",dataSource.id),filterValueLabel.classList.add("sr-only"),filterValueLabel.innerText=dataSource.getAttribute("data-field-title"),filterValueNode.appendChild(filterValueLabel),initialValues.forEach((filterValue=>{let selectedOption=dataSource.querySelector('option[value="'.concat(filterValue,'"]'));selectedOption?selectedOption.selected=!0:this.showSuggestions||(selectedOption=document.createElement("option"),selectedOption.value=filterValue,selectedOption.innerHTML=filterValue,selectedOption.selected=!0,dataSource.append(selectedOption))})),_formAutocomplete.default.enhance(dataSource,"1"==dataSource.dataset.allowCustom,null,await this.placeholder,!1,this.showSuggestions,null,!dataSource.multiple,{items:"core/datafilter/autocomplete_selection_items",layout:"core/datafilter/autocomplete_layout",selection:"core/datafilter/autocomplete_selection"})}get filterRoot(){return this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType))}getSourceDataForFilter(){return this.rootNode.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(this.filterType))}getFilterValueNode(){return this.filterRoot.querySelector(_selectors.default.filter.regions.values)}get name(){return this.filterType}get jointype(){return parseInt(this.filterRoot.querySelector(_selectors.default.filter.fields.join).value,10)}get rawValues(){const filterValueSelect=this.getFilterValueNode().querySelector("select");return Object.values((select=filterValueSelect,select.querySelectorAll(":checked"))).map((option=>option.value));var select}get values(){return this.rawValues.map((option=>parseInt(option,10)))}get filterValue(){return{name:this.name,jointype:this.jointype,values:this.values}}},_exports.default}));
//# sourceMappingURL=filtertype.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
define("core/datafilter/filtertypes/country",["exports","core/datafilter/filtertype"],(function(_exports,_filtertype){var obj;
/**
* Country filter
*
* @module core/datafilter/filtertypes/country
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{get values(){return this.rawValues}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=country.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"country.min.js","sources":["../../../src/datafilter/filtertypes/country.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Country filter\n *\n * @module core/datafilter/filtertypes/country\n * @copyright 2021 Paul Holden <paulh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from 'core/datafilter/filtertype';\n\nexport default class extends Filter {\n\n /**\n * For country the final value is an array of country code strings\n *\n * @return {Object}\n */\n get values() {\n return this.rawValues;\n }\n}\n"],"names":["Filter","values","this","rawValues"],"mappings":";;;;;;;4KAyB6BA,oBAOrBC,oBACOC,KAAKC"}

View File

@ -0,0 +1,10 @@
define("core/datafilter/filtertypes/courseid",["exports","core/datafilter/filtertype"],(function(_exports,_filtertype){var obj;
/**
* Course ID filter.
*
* @module core/datafilter/filtertypes/courseid
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{constructor(filterType,filterSet){super(filterType,filterSet)}async addValueSelector(){}get filterValue(){return{name:this.name,jointype:1,values:[parseInt(this.rootNode.dataset.tableCourseId,10)]}}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=courseid.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"courseid.min.js","sources":["../../../src/datafilter/filtertypes/courseid.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course ID filter.\n *\n * @module core/datafilter/filtertypes/courseid\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from 'core/datafilter/filtertype';\n\nexport default class extends Filter {\n constructor(filterType, filterSet) {\n super(filterType, filterSet);\n }\n\n async addValueSelector() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: 1,\n values: [parseInt(this.rootNode.dataset.tableCourseId, 10)],\n };\n }\n}\n"],"names":["Filter","constructor","filterType","filterSet","filterValue","name","this","jointype","values","parseInt","rootNode","dataset","tableCourseId"],"mappings":";;;;;;;4KAwB6BA,oBACzBC,YAAYC,WAAYC,iBACdD,WAAYC,qCAYlBC,wBACO,CACHC,KAAMC,KAAKD,KACXE,SAAU,EACVC,OAAQ,CAACC,SAASH,KAAKI,SAASC,QAAQC,cAAe"}

View File

@ -0,0 +1,10 @@
define("core/datafilter/filtertypes/keyword",["exports","core/datafilter/filtertype","core/str"],(function(_exports,_filtertype,_str){var obj;
/**
* Keyword filter.
*
* @module core/datafilter/filtertypes/keyword
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=(obj=_filtertype)&&obj.__esModule?obj:{default:obj};class _default extends _filtertype.default{get values(){return this.rawValues}get placeholder(){return(0,_str.get_string)("placeholdertype","core_user")}get showSuggestions(){return!1}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=keyword.min.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"keyword.min.js","sources":["../../../src/datafilter/filtertypes/keyword.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Keyword filter.\n *\n * @module core/datafilter/filtertypes/keyword\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from 'core/datafilter/filtertype';\nimport {get_string as getString} from 'core/str';\n\nexport default class extends Filter {\n /**\n * For keywords the final value is an Array of strings.\n *\n * @returns {Object}\n */\n get values() {\n return this.rawValues;\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertype', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return false;\n }\n}\n"],"names":["Filter","values","this","rawValues","placeholder","showSuggestions"],"mappings":";;;;;;;4KAyB6BA,oBAMrBC,oBACOC,KAAKC,UAQZC,yBACO,mBAAU,kBAAmB,aAQpCC,6BACO"}

View File

@ -1,8 +1,8 @@
define("core_user/local/participantsfilter/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
define("core/datafilter/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
/**
* Module containing the selectors for user filters.
*
* @module core_user/local/user_filter/selectors
* @module core/datafilter/selectors
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

View File

@ -0,0 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../../src/datafilter/selectors.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core/datafilter/selectors\n * @copyright 2020 Michael Hawkins <michaelh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n typeListSelect: `select${getFilterRegion('filtertypelist')}`,\n },\n};\n"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","data","all","typeList","typeListSelect"],"mappings":";;;;;;;;MAuBMA,gBAAkBC,sCAAiCA,aACnDC,gBAAkBC,sCAAiCA,aACnDC,eAAiBC,oCAA+BA,yBAEvC,CACXC,OAAQ,CACJL,OAAQD,gBAAgB,UACxBO,QAAS,CACLC,OAAQN,gBAAgB,WAE5BO,OAAQ,CACJC,KAAMN,eAAe,QACrBO,KAAMP,eAAe,SAEzBQ,QAAS,CACLC,OAAQb,gBAAgB,UAE5Bc,OAAQC,gBAAWf,gBAAgB,wCAA+Be,YAEtEC,UAAW,CACPf,OAAQD,gBAAgB,WACxBO,QAAS,CACLU,OAAQf,gBAAgB,OACxBgB,aAAchB,gBAAgB,SAC9BiB,aAAcjB,gBAAgB,UAElCU,QAAS,CACLQ,YAAapB,gBAAgB,eAC7BqB,WAAYrB,gBAAgB,WAC5BsB,WAAYtB,gBAAgB,mBAEhCS,OAAQ,CACJC,eAASV,gBAAgB,2BAAkBI,eAAe,WAGlEmB,KAAM,CACFd,OAAQ,CACJK,OAAQC,kCAA6BA,WACrCS,cAAQxB,gBAAgB,yCAE5ByB,SAAUzB,gBAAgB,kBAC1B0B,+BAAyB1B,gBAAgB"}

436
lib/amd/src/datafilter.js Normal file
View File

@ -0,0 +1,436 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Data filter management.
*
* @module core/datafilter
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import CourseFilter from 'core/datafilter/filtertypes/courseid';
import GenericFilter from 'core/datafilter/filtertype';
import {get_strings as getStrings} from 'core/str';
import Notification from 'core/notification';
import Pending from 'core/pending';
import Selectors from 'core/datafilter/selectors';
import Templates from 'core/templates';
import CustomEvents from 'core/custom_interaction_events';
import jQuery from 'jquery';
export default class {
/**
* Initialise the filter on the element with the given filterSet and callback.
*
* @param {HTMLElement} filterSet The filter element.
* @param {Function} applyCallback Callback function when updateTableFromFilter
*/
constructor(filterSet, applyCallback) {
this.filterSet = filterSet;
this.applyCallback = applyCallback;
// Keep a reference to all of the active filters.
this.activeFilters = {
courseid: new CourseFilter('courseid', filterSet),
};
}
/**
* Initialise event listeners to the filter.
*/
init() {
// Add listeners for the main actions.
this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {
if (e.target.closest(Selectors.filterset.actions.addRow)) {
e.preventDefault();
this.addFilterRow();
}
if (e.target.closest(Selectors.filterset.actions.applyFilters)) {
e.preventDefault();
this.updateTableFromFilter();
}
if (e.target.closest(Selectors.filterset.actions.resetFilters)) {
e.preventDefault();
this.removeAllFilters();
}
});
// Add the listener to remove a single filter.
this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {
if (e.target.closest(Selectors.filter.actions.remove)) {
e.preventDefault();
this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);
}
});
// Add listeners for the filter type selection.
let filterRegion = jQuery(this.getFilterRegion());
CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);
filterRegion.on(CustomEvents.events.accessibleChange, e => {
const typeField = e.target.closest(Selectors.filter.fields.type);
if (typeField && typeField.value) {
const filter = e.target.closest(Selectors.filter.region);
this.addFilter(filter, typeField.value);
}
});
this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {
this.filterSet.dataset.filterverb = e.target.value;
});
}
/**
* Get the filter list region.
*
* @return {HTMLElement}
*/
getFilterRegion() {
return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);
}
/**
* Add an unselected filter row.
*
* @return {Promise}
*/
addFilterRow() {
const pendingPromise = new Pending('core/datafilter:addFilterRow');
const rownum = 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rownum})
.then(({html, js}) => {
const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);
return newContentNodes;
})
.then(filterRow => {
// Note: This is a nasty hack.
// We should try to find a better way of doing this.
// We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
// it in place.
const typeList = this.filterSet.querySelector(Selectors.data.typeList);
filterRow.forEach(contentNode => {
const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
if (contentTypeList) {
contentTypeList.innerHTML = typeList.innerHTML;
}
});
return filterRow;
})
.then(filterRow => {
this.updateFiltersOptions();
return filterRow;
})
.then(result => {
pendingPromise.resolve();
return result;
})
.catch(Notification.exception);
}
/**
* Get the filter data source node fro the specified filter type.
*
* @param {String} filterType
* @return {HTMLElement}
*/
getFilterDataSource(filterType) {
const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);
return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));
}
/**
* Add a filter to the list of active filters, performing any necessary setup.
*
* @param {HTMLElement} filterRow
* @param {String} filterType
* @param {Array} initialFilterValues The initially selected values for the filter
* @returns {Filter}
*/
async addFilter(filterRow, filterType, initialFilterValues) {
// Name the filter on the filter row.
filterRow.dataset.filterType = filterType;
const filterDataNode = this.getFilterDataSource(filterType);
// Instantiate the Filter class.
let Filter = GenericFilter;
if (filterDataNode.dataset.filterTypeClass) {
Filter = await import(filterDataNode.dataset.filterTypeClass);
}
this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues);
// Disable the select.
const typeField = filterRow.querySelector(Selectors.filter.fields.type);
typeField.value = filterType;
typeField.disabled = 'disabled';
// Update the list of available filter types.
this.updateFiltersOptions();
return this.activeFilters[filterType];
}
/**
* Get the registered filter class for the named filter.
*
* @param {String} name
* @return {Object} See the Filter class.
*/
getFilterObject(name) {
return this.activeFilters[name];
}
/**
* Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,
* that it is replaced instead of being removed.
*
* @param {HTMLElement} filterRow
* @param {Bool} refreshContent Whether to refresh the table content when removing
*/
removeOrReplaceFilterRow(filterRow, refreshContent) {
const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
if (filterCount === 1) {
this.replaceFilterRow(filterRow, refreshContent);
} else {
this.removeFilterRow(filterRow, refreshContent);
}
}
/**
* Remove the specified filter row and associated class.
*
* @param {HTMLElement} filterRow
* @param {Bool} refreshContent Whether to refresh the table content when removing
*/
async removeFilterRow(filterRow, refreshContent = true) {
const filterType = filterRow.querySelector(Selectors.filter.fields.type);
const hasFilterValue = !!filterType.value;
// Remove the filter object.
this.removeFilterObject(filterRow.dataset.filterType);
// Remove the actual filter HTML.
filterRow.remove();
// Update the list of available filter types.
this.updateFiltersOptions();
if (hasFilterValue && refreshContent) {
// Refresh the table if there was any content in this row.
this.updateTableFromFilter();
}
// Update filter fieldset legends.
const filterLegends = await this.getAvailableFilterLegends();
this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {
filterRow.querySelector('legend').innerText = filterLegends[index];
});
}
/**
* Replace the specified filter row with a new one.
*
* @param {HTMLElement} filterRow
* @param {Bool} refreshContent Whether to refresh the table content when removing
* @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).
* @return {Promise}
*/
replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {
// Remove the filter object.
this.removeFilterObject(filterRow.dataset.filterType);
return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rowNum})
.then(({html, js}) => {
const newContentNodes = Templates.replaceNode(filterRow, html, js);
return newContentNodes;
})
.then(filterRow => {
// Note: This is a nasty hack.
// We should try to find a better way of doing this.
// We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
// it in place.
const typeList = this.filterSet.querySelector(Selectors.data.typeList);
filterRow.forEach(contentNode => {
const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
if (contentTypeList) {
contentTypeList.innerHTML = typeList.innerHTML;
}
});
return filterRow;
})
.then(filterRow => {
this.updateFiltersOptions();
return filterRow;
})
.then(filterRow => {
// Refresh the table.
if (refreshContent) {
return this.updateTableFromFilter();
} else {
return filterRow;
}
})
.catch(Notification.exception);
}
/**
* Remove the Filter Object from the register.
*
* @param {string} filterName The name of the filter to be removed
*/
removeFilterObject(filterName) {
if (filterName) {
const filter = this.getFilterObject(filterName);
if (filter) {
filter.tearDown();
// Remove from the list of active filters.
delete this.activeFilters[filterName];
}
}
}
/**
* Remove all filters.
*
* @returns {Promise}
*/
removeAllFilters() {
const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));
// Refresh the table.
return this.updateTableFromFilter();
}
/**
* Remove any empty filters.
*/
removeEmptyFilters() {
const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => {
const filterType = filterRow.querySelector(Selectors.filter.fields.type);
if (!filterType.value) {
this.removeOrReplaceFilterRow(filterRow, false);
}
});
}
/**
* Update the list of filter types to filter out those already selected.
*/
updateFiltersOptions() {
const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => {
const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');
options.forEach(option => {
if (option.value === filterRow.dataset.filterType) {
option.classList.remove('hidden');
option.disabled = false;
} else if (this.activeFilters[option.value]) {
option.classList.add('hidden');
option.disabled = true;
} else {
option.classList.remove('hidden');
option.disabled = false;
}
});
});
// Configure the state of the "Add row" button.
// This button is disabled when there is a filter row available for each condition.
const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);
const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);
if (filterDataNode.length <= filters.length) {
addRowButton.setAttribute('disabled', 'disabled');
} else {
addRowButton.removeAttribute('disabled');
}
if (filters.length === 1) {
this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');
this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;
this.filterSet.dataset.filterverb = 2;
} else {
this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');
}
}
/**
* Update the Dynamic table based upon the current filter.
*/
updateTableFromFilter() {
const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');
const filters = {};
Object.values(this.activeFilters).forEach(filter => {
filters[filter.filterValue.name] = filter.filterValue;
});
if (this.applyCallback) {
this.applyCallback(filters, pendingPromise);
}
}
/**
* Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.
*
* @return {array}
*/
async getAvailableFilterLegends() {
const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;
let requests = [];
[...Array(maxFilters)].forEach((_, rowIndex) => {
requests.push({
"key": "filterrowlegend",
"component": "core_user",
// Add 1 since rows begin at 1 (index begins at zero).
"param": rowIndex + 1
});
});
const legendStrings = await getStrings(requests)
.then(fetchedStrings => {
return fetchedStrings;
})
.catch(Notification.exception);
return legendStrings;
}
}

View File

@ -14,14 +14,14 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base Filter class for a filter type in the participants filter UI.
* Base Filter class for a filter type in the filter UI.
*
* @module core_user/local/participantsfilter/filter
* @module core/datafilter/filtertype
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Autocomplete from 'core/form-autocomplete';
import Selectors from './selectors';
import Selectors from 'core/datafilter/selectors';
import {get_string as getString} from 'core/str';
/**
@ -65,7 +65,7 @@ export default class {
* @return {Promise} Resolving to a String
*/
get placeholder() {
return getString('placeholdertypeorselect', 'core_user');
return getString('placeholdertypeorselect', 'core');
}
/**
@ -148,9 +148,9 @@ export default class {
// Template overrides.
{
items: 'core_user/local/participantsfilter/autocomplete_selection_items',
layout: 'core_user/local/participantsfilter/autocomplete_layout',
selection: 'core_user/local/participantsfilter/autocomplete_selection',
items: 'core/datafilter/autocomplete_selection_items',
layout: 'core/datafilter/autocomplete_layout',
selection: 'core/datafilter/autocomplete_selection',
}
);
}

View File

@ -16,12 +16,12 @@
/**
* Country filter
*
* @module core_user/local/participantsfilter/filtertypes/country
* @module core/datafilter/filtertypes/country
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Filter from '../filter';
import Filter from 'core/datafilter/filtertype';
export default class extends Filter {

View File

@ -16,11 +16,11 @@
/**
* Course ID filter.
*
* @module core_user/local/participantsfilter/filtertypes/courseid
* @module core/datafilter/filtertypes/courseid
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Filter from '../filter';
import Filter from 'core/datafilter/filtertype';
export default class extends Filter {
constructor(filterType, filterSet) {

View File

@ -16,11 +16,11 @@
/**
* Keyword filter.
*
* @module core_user/local/participantsfilter/filtertypes/keyword
* @module core/datafilter/filtertypes/keyword
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Filter from '../filter';
import Filter from 'core/datafilter/filtertype';
import {get_string as getString} from 'core/str';
export default class extends Filter {

View File

@ -16,7 +16,7 @@
/**
* Module containing the selectors for user filters.
*
* @module core_user/local/user_filter/selectors
* @module core/datafilter/selectors
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

View File

@ -0,0 +1,101 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace core\output;
use context;
use renderable;
use stdClass;
use templatable;
/**
* The filter renderable class.
*
* @package core
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class datafilter implements renderable, templatable {
/** @var context $context The context where the filters are being rendered. */
protected $context;
/** @var string $tableregionid Container of the table to be updated by this filter, is used to retrieve the table */
protected $tableregionid;
/** @var stdClass $course The course shown */
protected $course;
/**
* Filter constructor.
*
* @param context $context The context where the filters are being rendered
* @param string|null $tableregionid Container of the table which will be updated by this filter
*/
public function __construct(context $context, ?string $tableregionid = null) {
$this->context = $context;
$this->tableregionid = $tableregionid;
if ($context instanceof \context_course) {
$this->course = get_course($context->instanceid);
}
}
/**
* Get data for all filter types.
*
* @return array
*/
abstract protected function get_filtertypes(): array;
/**
* Get a standardised filter object.
*
* @param string $name
* @param string $title
* @param bool $custom
* @param bool $multiple
* @param string|null $filterclass
* @param array $values
* @param bool $allowempty
* @return stdClass|null
*/
protected function get_filter_object(
string $name,
string $title,
bool $custom,
bool $multiple,
?string $filterclass,
array $values,
bool $allowempty = false
): ?stdClass {
if (!$allowempty && empty($values)) {
// Do not show empty filters.
return null;
}
return (object) [
'name' => $name,
'title' => $title,
'allowcustom' => $custom,
'allowmultiple' => $multiple,
'filtertypeclass' => $filterclass,
'values' => $values,
];
}
}

View File

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/local/participantsfilter/autocomplete_layout
@template core/datafilter/autocomplete_layout
Moodle template for the layout of autocomplete elements.

View File

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/local/participantsfilter/autocomplete_selection
@template core/datafilter/autocomplete_selection
Moodle template for the wrapper of currently selected items in an autocomplate form element.

View File

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/local/participantsfilter/autocomplete_selection_items
@template core/datafilter/autocomplete_selection_items
Moodle template for the currently selected items in an autocomplete form element.
@ -43,7 +43,7 @@
{{#items}}
<span role="option" data-value="{{value}}" aria-selected="true"
class="badge badge-secondary clickable text-wrap text-break line-height-4 m-1">
{{label}}<i class="icon fa fa-times pl-2 mr-0"></i>
{{{label}}}<i class="icon fa fa-times pl-2 mr-0"></i>
</span>
{{/items}}
{{^items}}

View File

@ -0,0 +1,63 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/datafilter/filter
Template for the form containing one or more filter rows.
Example context (json):
{
"filtertypes": [
{
"name": "status",
"title": "Status",
"values": [
{
"value": 1,
"title": "Active"
},
{
"value": 0,
"title": "Suspended"
}
]
}
]
}
}}
<div id="core-filter-{{uniqid}}" class="filter-group my-2 p-2 bg-light border-radius border" data-table-region="{{tableregionid}}" data-table-course-id="{{courseid}}" data-filterverb="2">
{{> core/datafilter/filter_match }}
<div data-filterregion="filters">
{{> core/datafilter/filter_row }}
</div>
<div class="d-flex" data-filterregion="actions">
&nbsp;
<button type="button" class="btn btn-link text-reset" data-filteraction="add">
{{#pix}}t/add{{/pix}}<span class="pl-3">{{#str}}addcondition{{/str}}</span>
</button>
<button data-filteraction="reset" type="button" class="btn btn-secondary ml-auto mr-2">{{#str}}clearfilters{{/str}}</button>
<button data-filteraction="apply" type="button" class="btn btn-primary">{{#str}}applyfilters{{/str}}</button>
</div>
{{> core/datafilter/filter_types }}
</div>

View File

@ -0,0 +1,55 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/datafilter/filter_match
Template for the form containing one or more filter rows.
Example context (json):
{
"filtertypes": [
{
"name": "status",
"title": "Status",
"values": [
{
"value": 1,
"title": "Active"
},
{
"value": 0,
"title": "Suspended"
}
]
}
]
}
}}
<div data-filterregion="filtermatch" class="hidden">
<label for="core-filter-jointype-{{uniqid}}" class="my-0" aria-hidden="true">
{{#str}}match{{/str}}
</label>
<select class="custom-select" data-filterfield="join" id="core-filter-jointype-{{uniqid}}"
aria-label="{{#str}}filtersetmatchdescription{{/str}}">
<option value="0">{{#str}}none{{/str}}</option>
<option value="1">{{#str}}any{{/str}}</option>
<option value="2" selected>{{#str}}all{{/str}}</option>
</select>
<span aria-hidden="true">{{#str}}matchofthefollowing{{/str}}</span>
</div>

View File

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/local/participantsfilter/filterrow
@template core/datafilter/filter_row
Template for use by each filter condition.
@ -35,20 +35,20 @@
}}
<div data-filterregion="filter">
<fieldset>
<legend class="sr-only">{{#str}}filterrowlegend, core_user, {{rownumber}}{{/str}}</legend>
<legend class="sr-only">{{#str}}filterrowlegend, core, {{rownumber}}{{/str}}</legend>
<div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start">
<div class="d-flex flex-column flex-md-row align-items-md-center">
<label for="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match, core_user{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}">
<label for="core-filter_row-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core-filter_row-jointype-{{uniqid}}">
<option value="0">{{#str}}none{{/str}}</option>
<option selected=selected value="1">{{#str}}any{{/str}}</option>
<option value="2">{{#str}}all{{/str}}</option>
</select>
</div>
<label class="sr-only pt-2" for="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">{{#str}}filtertype, core_user{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">
<option value="">{{#str}}selectfiltertype, core_user{{/str}}</option>
<label class="sr-only pt-2" for="core-filter_row-filtertype-{{uniqid}}">{{#str}}filtertype{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core-filter_row-filtertype-{{uniqid}}">
<option value="">{{#str}}selectfiltertype{{/str}}</option>
{{#filtertypes}}
<option value="{{name}}">{{title}}</option>
{{/filtertypes}}
@ -56,14 +56,14 @@
<div data-filterregion="value" class="d-md-block flex-column align-items-start flex-lg-row"></div>
<button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow, core_user{{/str}}">
<i class="icon fa fa-times-circle"></i>
<button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow{{/str}}">
{{#pix}}e/cancel_solid_circle{{/pix}}
</button>
</div>
<div data-filterregion="joinadverb" class="pl-1 text-uppercase font-weight-bold">
<div data-filterverbfor="0">{{#str}}adverbfor_andnot, core_user{{/str}}</div>
<div data-filterverbfor="1">{{#str}}adverbfor_or, core_user{{/str}}</div>
<div data-filterverbfor="2">{{#str}}adverbfor_and, core_user{{/str}}</div>
<div data-filterverbfor="0">{{#str}}operator_andnot{{/str}}</div>
<div data-filterverbfor="1">{{#str}}operator_or{{/str}}</div>
<div data-filterverbfor="2">{{#str}}operator_and{{/str}}</div>
</div>
</fieldset>
</div>

View File

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/local/participantsfilter/filtertype
@template core/datafilter/filter_type
Filter type data, not shown to users but used as a source of data for form autocompletion.
@ -33,8 +33,8 @@
"name": "status",
"title": "Enrolment Status",
"allowcustom": "0",
"allowmultiple" false,
"filtertypeclass": "core_user/local/participantsfilter/filtertypes/courseid",
"allowmultiple": false,
"filtertypeclass": "core/filter_types/courseid",
"values": [
{
"value": "0",

View File

@ -15,7 +15,7 @@
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_user/local/participantsfilter/filtertypes
@template core/datafilter/filter_types
Placeholder to fetch all filter types.
@ -51,12 +51,12 @@
}}
<div class="hidden" data-filterregion="filtertypedata">
{{#filtertypes}}
{{> core_user/local/participantsfilter/filtertype}}
{{> core/datafilter/filter_type}}
{{/filtertypes}}
</div>
<div class="hidden">
<select disabled="disabled" data-filterfield="type" data-filterregion="filtertypelist">
<option value="">{{#str}}selectfiltertype, core_user{{/str}}</option>
<option value="">{{#str}}selectfiltertype{{/str}}</option>
{{#filtertypes}}
<option value="{{name}}">{{title}}</option>
{{/filtertypes}}

View File

@ -1,10 +0,0 @@
define("core_user/local/participantsfilter/filter",["exports","core/form-autocomplete","./selectors","core/str"],(function(_exports,_formAutocomplete,_selectors,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Base Filter class for a filter type in the participants filter UI.
*
* @module core_user/local/participantsfilter/filter
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_formAutocomplete=_interopRequireDefault(_formAutocomplete),_selectors=_interopRequireDefault(_selectors);return _exports.default=class{constructor(filterType,rootNode,initialValues){this.filterType=filterType,this.rootNode=rootNode,this.addValueSelector(initialValues)}tearDown(){}get placeholder(){return(0,_str.get_string)("placeholdertypeorselect","core_user")}get showSuggestions(){return!0}async addValueSelector(){let initialValues=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const filterValueNode=this.getFilterValueNode(),sourceDataNode=this.getSourceDataForFilter();if(!sourceDataNode)return;filterValueNode.innerHTML=sourceDataNode.outerHTML;const dataSource=filterValueNode.querySelector("select");dataSource.id="filter-value-"+dataSource.getAttribute("data-field-name");const filterValueLabel=document.createElement("label");filterValueLabel.setAttribute("for",dataSource.id),filterValueLabel.classList.add("sr-only"),filterValueLabel.innerText=dataSource.getAttribute("data-field-title"),filterValueNode.appendChild(filterValueLabel),initialValues.forEach((filterValue=>{let selectedOption=dataSource.querySelector('option[value="'.concat(filterValue,'"]'));selectedOption?selectedOption.selected=!0:this.showSuggestions||(selectedOption=document.createElement("option"),selectedOption.value=filterValue,selectedOption.innerHTML=filterValue,selectedOption.selected=!0,dataSource.append(selectedOption))})),_formAutocomplete.default.enhance(dataSource,"1"==dataSource.dataset.allowCustom,null,await this.placeholder,!1,this.showSuggestions,null,!dataSource.multiple,{items:"core_user/local/participantsfilter/autocomplete_selection_items",layout:"core_user/local/participantsfilter/autocomplete_layout",selection:"core_user/local/participantsfilter/autocomplete_selection"})}get filterRoot(){return this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType))}getSourceDataForFilter(){return this.rootNode.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(this.filterType))}getFilterValueNode(){return this.filterRoot.querySelector(_selectors.default.filter.regions.values)}get name(){return this.filterType}get jointype(){return parseInt(this.filterRoot.querySelector(_selectors.default.filter.fields.join).value,10)}get rawValues(){const filterValueSelect=this.getFilterValueNode().querySelector("select");return Object.values((select=filterValueSelect,select.querySelectorAll(":checked"))).map((option=>option.value));var select}get values(){return this.rawValues.map((option=>parseInt(option,10)))}get filterValue(){return{name:this.name,jointype:this.jointype,values:this.values}}},_exports.default}));
//# sourceMappingURL=filter.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,10 +0,0 @@
define("core_user/local/participantsfilter/filtertypes/country",["exports","../filter"],(function(_exports,_filter){var obj;
/**
* Country filter
*
* @module core_user/local/participantsfilter/filtertypes/country
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filter=(obj=_filter)&&obj.__esModule?obj:{default:obj};class _default extends _filter.default{get values(){return this.rawValues}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=country.min.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"country.min.js","sources":["../../../../src/local/participantsfilter/filtertypes/country.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Country filter\n *\n * @module core_user/local/participantsfilter/filtertypes/country\n * @copyright 2021 Paul Holden <paulh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Filter from '../filter';\n\nexport default class extends Filter {\n\n /**\n * For country the final value is an array of country code strings\n *\n * @return {Object}\n */\n get values() {\n return this.rawValues;\n }\n}\n"],"names":["Filter","values","this","rawValues"],"mappings":";;;;;;;oKAyB6BA,gBAOrBC,oBACOC,KAAKC"}

View File

@ -1,10 +0,0 @@
define("core_user/local/participantsfilter/filtertypes/courseid",["exports","../filter"],(function(_exports,_filter){var obj;
/**
* Course ID filter.
*
* @module core_user/local/participantsfilter/filtertypes/courseid
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filter=(obj=_filter)&&obj.__esModule?obj:{default:obj};class _default extends _filter.default{constructor(filterType,filterSet){super(filterType,filterSet)}async addValueSelector(){}get filterValue(){return{name:this.name,jointype:1,values:[parseInt(this.rootNode.dataset.tableCourseId,10)]}}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=courseid.min.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"courseid.min.js","sources":["../../../../src/local/participantsfilter/filtertypes/courseid.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Course ID filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/courseid\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from '../filter';\n\nexport default class extends Filter {\n constructor(filterType, filterSet) {\n super(filterType, filterSet);\n }\n\n async addValueSelector() {\n // eslint-disable-line no-empty-function\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: 1,\n values: [parseInt(this.rootNode.dataset.tableCourseId, 10)],\n };\n }\n}\n"],"names":["Filter","constructor","filterType","filterSet","filterValue","name","this","jointype","values","parseInt","rootNode","dataset","tableCourseId"],"mappings":";;;;;;;oKAwB6BA,gBACzBC,YAAYC,WAAYC,iBACdD,WAAYC,qCAYlBC,wBACO,CACHC,KAAMC,KAAKD,KACXE,SAAU,EACVC,OAAQ,CAACC,SAASH,KAAKI,SAASC,QAAQC,cAAe"}

View File

@ -1,10 +0,0 @@
define("core_user/local/participantsfilter/filtertypes/keyword",["exports","../filter","core/str"],(function(_exports,_filter,_str){var obj;
/**
* Keyword filter.
*
* @module core_user/local/participantsfilter/filtertypes/keyword
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filter=(obj=_filter)&&obj.__esModule?obj:{default:obj};class _default extends _filter.default{get values(){return this.rawValues}get placeholder(){return(0,_str.get_string)("placeholdertype","core_user")}get showSuggestions(){return!1}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=keyword.min.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"keyword.min.js","sources":["../../../../src/local/participantsfilter/filtertypes/keyword.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Keyword filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/keyword\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from '../filter';\nimport {get_string as getString} from 'core/str';\n\nexport default class extends Filter {\n /**\n * For keywords the final value is an Array of strings.\n *\n * @returns {Object}\n */\n get values() {\n return this.rawValues;\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertype', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return false;\n }\n}\n"],"names":["Filter","values","this","rawValues","placeholder","showSuggestions"],"mappings":";;;;;;;oKAyB6BA,gBAMrBC,oBACOC,KAAKC,UAQZC,yBACO,mBAAU,kBAAmB,aAQpCC,6BACO"}

View File

@ -1 +0,0 @@
{"version":3,"file":"selectors.min.js","sources":["../../../src/local/participantsfilter/selectors.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core_user/local/user_filter/selectors\n * @copyright 2020 Michael Hawkins <michaelh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n typeListSelect: `select${getFilterRegion('filtertypelist')}`,\n },\n};\n"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","data","all","typeList","typeListSelect"],"mappings":";;;;;;;;MAuBMA,gBAAkBC,sCAAiCA,aACnDC,gBAAkBC,sCAAiCA,aACnDC,eAAiBC,oCAA+BA,yBAEvC,CACXC,OAAQ,CACJL,OAAQD,gBAAgB,UACxBO,QAAS,CACLC,OAAQN,gBAAgB,WAE5BO,OAAQ,CACJC,KAAMN,eAAe,QACrBO,KAAMP,eAAe,SAEzBQ,QAAS,CACLC,OAAQb,gBAAgB,UAE5Bc,OAAQC,gBAAWf,gBAAgB,wCAA+Be,YAEtEC,UAAW,CACPf,OAAQD,gBAAgB,WACxBO,QAAS,CACLU,OAAQf,gBAAgB,OACxBgB,aAAchB,gBAAgB,SAC9BiB,aAAcjB,gBAAgB,UAElCU,QAAS,CACLQ,YAAapB,gBAAgB,eAC7BqB,WAAYrB,gBAAgB,WAC5BsB,WAAYtB,gBAAgB,mBAEhCS,OAAQ,CACJC,eAASV,gBAAgB,2BAAkBI,eAAe,WAGlEmB,KAAM,CACFd,OAAQ,CACJK,OAAQC,kCAA6BA,WACrCS,cAAQxB,gBAAgB,yCAE5ByB,SAAUzB,gBAAgB,kBAC1B0B,+BAAyB1B,gBAAgB"}

View File

@ -0,0 +1,10 @@
define("core_user/participants_filter",["exports","core/datafilter","core_table/dynamic","core/datafilter/selectors","core/notification","core/pending"],(function(_exports,_datafilter,DynamicTable,_selectors,_notification,_pending){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}}
/**
* Participants filter management.
*
* @module core_user/participants_filter
* @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_datafilter=_interopRequireDefault(_datafilter),DynamicTable=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}(DynamicTable),_selectors=_interopRequireDefault(_selectors),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);_exports.init=filterRegionId=>{const filterSet=document.getElementById(filterRegionId),coreFilter=new _datafilter.default(filterSet,(function(filters,pendingPromise){DynamicTable.setFilters(DynamicTable.getTableFromId(filterSet.dataset.tableRegion),{jointype:parseInt(filterSet.querySelector(_selectors.default.filterset.fields.join).value,10),filters:filters}).then((result=>(pendingPromise.resolve(),result))).catch(_notification.default.exception)}));coreFilter.init();const tableRoot=DynamicTable.getTableFromId(filterSet.dataset.tableRegion),initialFilters=DynamicTable.getFilters(tableRoot);if(initialFilters){const initialFilterPromise=new _pending.default("core/filter:setFilterFromConfig");(config=>{const filterConfig=Object.entries(config.filters);if(!filterConfig.length)return Promise.resolve();filterSet.querySelector(_selectors.default.filterset.fields.join).value=config.jointype;const filterPromises=filterConfig.map((_ref=>{let[filterType,filterData]=_ref;if("courseid"===filterType)return!1;const filterValues=filterData.values;return!!filterValues.length&&coreFilter.addFilterRow().then((_ref2=>{let[filterRow]=_ref2;coreFilter.addFilter(filterRow,filterType,filterValues)}))})).filter((promise=>promise));return filterPromises.length?Promise.all(filterPromises).then((()=>coreFilter.removeEmptyFilters())).then((()=>{coreFilter.updateFiltersOptions()})).then((()=>{coreFilter.updateTableFromFilter()})):Promise.resolve()})(initialFilters).then((()=>initialFilterPromise.resolve())).catch()}}}));
//# sourceMappingURL=participants_filter.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,125 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Participants filter management.
*
* @module core_user/participants_filter
* @copyright 2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import CoreFilter from 'core/datafilter';
import * as DynamicTable from 'core_table/dynamic';
import Selectors from 'core/datafilter/selectors';
import Notification from 'core/notification';
import Pending from 'core/pending';
/**
* Initialise the participants filter on the element with the given id.
*
* @param {String} filterRegionId The id for the filter element.
*/
export const init = filterRegionId => {
const filterSet = document.getElementById(filterRegionId);
// Create and initialize filter.
const coreFilter = new CoreFilter(filterSet, function(filters, pendingPromise) {
DynamicTable.setFilters(
DynamicTable.getTableFromId(filterSet.dataset.tableRegion),
{
jointype: parseInt(filterSet.querySelector(Selectors.filterset.fields.join).value, 10),
filters,
}
)
.then(result => {
pendingPromise.resolve();
return result;
})
.catch(Notification.exception);
});
coreFilter.init();
/**
* Set the current filter options based on a provided configuration.
*
* @param {Object} config
* @param {Number} config.jointype
* @param {Object} config.filters
* @returns {Promise}
*/
const setFilterFromConfig = config => {
const filterConfig = Object.entries(config.filters);
if (!filterConfig.length) {
// There are no filters to set from.
return Promise.resolve();
}
// Set the main join type.
filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;
const filterPromises = filterConfig.map(([filterType, filterData]) => {
if (filterType === 'courseid') {
// The courseid is a special case.
return false;
}
const filterValues = filterData.values;
if (!filterValues.length) {
// There are no values for this filter.
// Skip it.
return false;
}
return coreFilter.addFilterRow()
.then(([filterRow]) => {
coreFilter.addFilter(filterRow, filterType, filterValues);
return;
});
}).filter(promise => promise);
if (!filterPromises.length) {
return Promise.resolve();
}
return Promise.all(filterPromises)
.then(() => {
return coreFilter.removeEmptyFilters();
})
.then(() => {
coreFilter.updateFiltersOptions();
return;
})
.then(() => {
coreFilter.updateTableFromFilter();
return;
});
};
// Initialize DynamicTable for showing result.
const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);
const initialFilters = DynamicTable.getFilters(tableRoot);
if (initialFilters) {
const initialFilterPromise = new Pending('core/filter:setFilterFromConfig');
// Apply the initial filter configuration.
setFilterFromConfig(initialFilters)
.then(() => initialFilterPromise.resolve())
.catch();
}
};

View File

@ -1,503 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Participants filter managemnet.
*
* @module core_user/participants_filter
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import CourseFilter from './local/participantsfilter/filtertypes/courseid';
import * as DynamicTable from 'core_table/dynamic';
import GenericFilter from './local/participantsfilter/filter';
import {get_strings as getStrings} from 'core/str';
import Notification from 'core/notification';
import Pending from 'core/pending';
import Selectors from './local/participantsfilter/selectors';
import Templates from 'core/templates';
import CustomEvents from 'core/custom_interaction_events';
import jQuery from 'jquery';
/**
* Initialise the participants filter on the element with the given id.
*
* @param {String} participantsRegionId
*/
export const init = participantsRegionId => {
// Keep a reference to the filterset.
const filterSet = document.querySelector(`#${participantsRegionId}`);
// Keep a reference to all of the active filters.
const activeFilters = {
courseid: new CourseFilter('courseid', filterSet),
};
/**
* Get the filter list region.
*
* @return {HTMLElement}
*/
const getFilterRegion = () => filterSet.querySelector(Selectors.filterset.regions.filterlist);
/**
* Add an unselected filter row.
*
* @return {Promise}
*/
const addFilterRow = () => {
const pendingPromise = new Pending('core_user/participantsfilter:addFilterRow');
const rownum = 1 + getFilterRegion().querySelectorAll(Selectors.filter.region).length;
return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {"rownumber": rownum})
.then(({html, js}) => {
const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js);
return newContentNodes;
})
.then(filterRow => {
// Note: This is a nasty hack.
// We should try to find a better way of doing this.
// We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
// it in place.
const typeList = filterSet.querySelector(Selectors.data.typeList);
filterRow.forEach(contentNode => {
const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
if (contentTypeList) {
contentTypeList.innerHTML = typeList.innerHTML;
}
});
return filterRow;
})
.then(filterRow => {
updateFiltersOptions();
return filterRow;
})
.then(result => {
pendingPromise.resolve();
return result;
})
.catch(Notification.exception);
};
/**
* Get the filter data source node fro the specified filter type.
*
* @param {String} filterType
* @return {HTMLElement}
*/
const getFilterDataSource = filterType => {
const filterDataNode = filterSet.querySelector(Selectors.filterset.regions.datasource);
return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));
};
/**
* Add a filter to the list of active filters, performing any necessary setup.
*
* @param {HTMLElement} filterRow
* @param {String} filterType
* @param {Array} initialFilterValues The initially selected values for the filter
* @returns {Filter}
*/
const addFilter = async(filterRow, filterType, initialFilterValues) => {
// Name the filter on the filter row.
filterRow.dataset.filterType = filterType;
const filterDataNode = getFilterDataSource(filterType);
// Instantiate the Filter class.
let Filter = GenericFilter;
if (filterDataNode?.dataset.filterTypeClass) {
Filter = await import(filterDataNode.dataset.filterTypeClass);
}
activeFilters[filterType] = new Filter(filterType, filterSet, initialFilterValues);
// Disable the select.
const typeField = filterRow.querySelector(Selectors.filter.fields.type);
typeField.value = filterType;
typeField.disabled = 'disabled';
// Update the list of available filter types.
updateFiltersOptions();
return activeFilters[filterType];
};
/**
* Get the registered filter class for the named filter.
*
* @param {String} name
* @return {Object} See the Filter class.
*/
const getFilterObject = name => {
return activeFilters[name];
};
/**
* Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,
* that it is replaced instead of being removed.
*
* @param {HTMLElement} filterRow
* @param {Bool} refreshContent Whether to refresh the table content when removing
*/
const removeOrReplaceFilterRow = (filterRow, refreshContent) => {
const filterCount = getFilterRegion().querySelectorAll(Selectors.filter.region).length;
if (filterCount === 1) {
replaceFilterRow(filterRow, refreshContent);
} else {
removeFilterRow(filterRow, refreshContent);
}
};
/**
* Remove the specified filter row and associated class.
*
* @param {HTMLElement} filterRow
* @param {Bool} refreshContent Whether to refresh the table content when removing
*/
const removeFilterRow = async(filterRow, refreshContent = true) => {
const filterType = filterRow.querySelector(Selectors.filter.fields.type);
const hasFilterValue = !!filterType.value;
// Remove the filter object.
removeFilterObject(filterRow.dataset.filterType);
// Remove the actual filter HTML.
filterRow.remove();
// Update the list of available filter types.
updateFiltersOptions();
if (hasFilterValue && refreshContent) {
// Refresh the table if there was any content in this row.
updateTableFromFilter();
}
// Update filter fieldset legends.
const filterLegends = await getAvailableFilterLegends();
getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {
filterRow.querySelector('legend').innerText = filterLegends[index];
});
};
/**
* Replace the specified filter row with a new one.
*
* @param {HTMLElement} filterRow
* @param {Bool} refreshContent Whether to refresh the table content when removing
* @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).
* @return {Promise}
*/
const replaceFilterRow = (filterRow, refreshContent = true, rowNum = 1) => {
// Remove the filter object.
removeFilterObject(filterRow.dataset.filterType);
return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {"rownumber": rowNum})
.then(({html, js}) => {
const newContentNodes = Templates.replaceNode(filterRow, html, js);
return newContentNodes;
})
.then(filterRow => {
// Note: This is a nasty hack.
// We should try to find a better way of doing this.
// We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy
// it in place.
const typeList = filterSet.querySelector(Selectors.data.typeList);
filterRow.forEach(contentNode => {
const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);
if (contentTypeList) {
contentTypeList.innerHTML = typeList.innerHTML;
}
});
return filterRow;
})
.then(filterRow => {
updateFiltersOptions();
return filterRow;
})
.then(filterRow => {
// Refresh the table.
if (refreshContent) {
return updateTableFromFilter();
} else {
return filterRow;
}
})
.catch(Notification.exception);
};
/**
* Remove the Filter Object from the register.
*
* @param {string} filterName The name of the filter to be removed
*/
const removeFilterObject = filterName => {
if (filterName) {
const filter = getFilterObject(filterName);
if (filter) {
filter.tearDown();
// Remove from the list of active filters.
delete activeFilters[filterName];
}
}
};
/**
* Remove all filters.
*
* @returns {Promise}
*/
const removeAllFilters = () => {
const pendingPromise = new Pending('core_user/participantsfilter:setFilterFromConfig');
const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => removeOrReplaceFilterRow(filterRow, false));
// Refresh the table.
return updateTableFromFilter()
.then(result => {
pendingPromise.resolve();
return result;
});
};
/**
* Remove any empty filters.
*/
const removeEmptyFilters = () => {
const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => {
const filterType = filterRow.querySelector(Selectors.filter.fields.type);
if (!filterType.value) {
removeOrReplaceFilterRow(filterRow, false);
}
});
};
/**
* Update the list of filter types to filter out those already selected.
*/
const updateFiltersOptions = () => {
const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => {
const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');
options.forEach(option => {
if (option.value === filterRow.dataset.filterType) {
option.classList.remove('hidden');
option.disabled = false;
} else if (activeFilters[option.value]) {
option.classList.add('hidden');
option.disabled = true;
} else {
option.classList.remove('hidden');
option.disabled = false;
}
});
});
// Configure the state of the "Add row" button.
// This button is disabled when there is a filter row available for each condition.
const addRowButton = filterSet.querySelector(Selectors.filterset.actions.addRow);
const filterDataNode = filterSet.querySelectorAll(Selectors.data.fields.all);
if (filterDataNode.length <= filters.length) {
addRowButton.setAttribute('disabled', 'disabled');
} else {
addRowButton.removeAttribute('disabled');
}
if (filters.length === 1) {
filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');
filterSet.querySelector(Selectors.filterset.fields.join).value = 2;
filterSet.dataset.filterverb = 2;
} else {
filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');
}
};
/**
* Set the current filter options based on a provided configuration.
*
* @param {Object} config
* @param {Number} config.jointype
* @param {Object} config.filters
* @returns {Promise}
*/
const setFilterFromConfig = config => {
const filterConfig = Object.entries(config.filters);
if (!filterConfig.length) {
// There are no filters to set from.
return Promise.resolve();
}
// Set the main join type.
filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;
const filterPromises = filterConfig.map(([filterType, filterData]) => {
if (filterType === 'courseid') {
// The courseid is a special case.
return false;
}
const filterValues = filterData.values;
if (!filterValues.length) {
// There are no values for this filter.
// Skip it.
return false;
}
return addFilterRow().then(([filterRow]) => addFilter(filterRow, filterType, filterValues));
}).filter(promise => promise);
if (!filterPromises.length) {
return Promise.resolve();
}
return Promise.all(filterPromises).then(() => {
return removeEmptyFilters();
})
.then(updateFiltersOptions)
.then(updateTableFromFilter);
};
/**
* Update the Dynamic table based upon the current filter.
*
* @return {Promise}
*/
const updateTableFromFilter = () => {
const pendingPromise = new Pending('core_user/participantsfilter:updateTableFromFilter');
const filters = {};
Object.values(activeFilters).forEach(filter => {
filters[filter.filterValue.name] = filter.filterValue;
});
return DynamicTable.setFilters(
DynamicTable.getTableFromId(filterSet.dataset.tableRegion),
{
jointype: parseInt(filterSet.querySelector(Selectors.filterset.fields.join).value, 10),
filters,
}
)
.then(result => {
pendingPromise.resolve();
return result;
})
.catch(Notification.exception);
};
/**
* Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.
*
* @return {array}
*/
const getAvailableFilterLegends = async() => {
const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;
let requests = [];
[...Array(maxFilters)].forEach((_, rowIndex) => {
requests.push({
"key": "filterrowlegend",
"component": "core_user",
// Add 1 since rows begin at 1 (index begins at zero).
"param": rowIndex + 1
});
});
const legendStrings = await getStrings(requests)
.then(fetchedStrings => {
return fetchedStrings;
})
.catch(Notification.exception);
return legendStrings;
};
// Add listeners for the main actions.
filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {
if (e.target.closest(Selectors.filterset.actions.addRow)) {
e.preventDefault();
addFilterRow();
}
if (e.target.closest(Selectors.filterset.actions.applyFilters)) {
e.preventDefault();
updateTableFromFilter();
}
if (e.target.closest(Selectors.filterset.actions.resetFilters)) {
e.preventDefault();
removeAllFilters();
}
});
// Add the listener to remove a single filter.
filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {
if (e.target.closest(Selectors.filter.actions.remove)) {
e.preventDefault();
removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);
}
});
// Add listeners for the filter type selection.
let filterRegion = jQuery(getFilterRegion());
CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);
filterRegion.on(CustomEvents.events.accessibleChange, e => {
const typeField = e.target.closest(Selectors.filter.fields.type);
if (typeField && typeField.value) {
const filter = e.target.closest(Selectors.filter.region);
addFilter(filter, typeField.value);
}
});
filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {
filterSet.dataset.filterverb = e.target.value;
});
const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);
const initialFilters = DynamicTable.getFilters(tableRoot);
if (initialFilters) {
const initialFilterPromise = new Pending('core_user/participantsfilter:setFilterFromConfig');
// Apply the initial filter configuration.
setFilterFromConfig(initialFilters)
.then(() => initialFilterPromise.resolve())
.catch();
}
};

View File

@ -23,12 +23,9 @@
*/
namespace core_user\output;
use context_course;
use core_user\fields;
use renderable;
use renderer_base;
use stdClass;
use templatable;
/**
* Class for rendering user filters on the course participants page.
@ -36,29 +33,7 @@ use templatable;
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class participants_filter implements renderable, templatable {
/** @var context_course $context The context where the filters are being rendered. */
protected $context;
/** @var string $tableregionid The table to be updated by this filter */
protected $tableregionid;
/** @var stdClass $course The course shown */
protected $course;
/**
* Participants filter constructor.
*
* @param context_course $context The context where the filters are being rendered.
* @param string $tableregionid The table to be updated by this filter
*/
public function __construct(context_course $context, string $tableregionid) {
$this->context = $context;
$this->tableregionid = $tableregionid;
$this->course = get_course($context->instanceid);
}
class participants_filter extends \core\output\datafilter {
/**
* Get data for all filter types.
@ -345,7 +320,7 @@ class participants_filter implements renderable, templatable {
get_string('country'),
false,
true,
'core_user/local/participantsfilter/filtertypes/country',
'core/datafilter/filtertypes/country',
array_map(function(string $code, string $name): stdClass {
return (object) [
'value' => $code,
@ -366,7 +341,7 @@ class participants_filter implements renderable, templatable {
get_string('filterbykeyword', 'core_user'),
true,
true,
'core_user/local/participantsfilter/filtertypes/keyword',
'core/datafilter/filtertypes/keyword',
[],
true
);
@ -388,41 +363,4 @@ class participants_filter implements renderable, templatable {
return $data;
}
/**
* Get a standardised filter object.
*
* @param string $name
* @param string $title
* @param bool $custom
* @param bool $multiple
* @param string|null $filterclass
* @param array $values
* @param bool $allowempty
* @return stdClass|null
*/
protected function get_filter_object(
string $name,
string $title,
bool $custom,
bool $multiple,
?string $filterclass,
array $values,
bool $allowempty = false
): ?stdClass {
if (!$allowempty && empty($values)) {
// Do not show empty filters.
return null;
}
return (object) [
'name' => $name,
'title' => $title,
'allowcustom' => $custom,
'allowmultiple' => $multiple,
'filtertypeclass' => $filterclass,
'values' => $values,
];
}
}

View File

@ -266,7 +266,7 @@ class core_user_renderer extends plugin_renderer_base {
* Render the data required for the participants filter on the course participants page.
*
* @param context $context The context of the course being displayed
* @param string $tableregionid The table to be updated by this filter
* @param string $tableregionid Container of the table to be updated by this filter, is used to retrieve the table
* @return string
*/
public function participants_filter(context $context, string $tableregionid): string {

View File

@ -39,39 +39,10 @@
]
}
}}
<div id="core_user-participantsfilter-{{uniqid}}" class="filter-group my-2 p-2 bg-light border-radius border" data-table-region="{{tableregionid}}" data-table-course-id="{{courseid}}" data-filterverb="2">
<div data-filterregion="filtermatch" class="hidden">
<label for="core_user-local-participantsfilter-jointype-{{uniqid}}" class="my-0" aria-hidden="true">
{{#str}}match, core_user{{/str}}
</label>
<select class="custom-select" data-filterfield="join" id="core_user-local-participantsfilter-jointype-{{uniqid}}"
aria-label="{{#str}}filtersetmatchdescription, core_user{{/str}}">
<option value="0">{{#str}}none{{/str}}</option>
<option value="1">{{#str}}any{{/str}}</option>
<option value="2" selected>{{#str}}all{{/str}}</option>
</select>
<span aria-hidden="true">{{#str}}matchofthefollowing, core_user{{/str}}</span>
</div>
<div data-filterregion="filters">
{{> core_user/local/participantsfilter/filterrow }}
</div>
<div class="d-flex" data-filterregion="actions">
&nbsp;
<button type="button" class="btn btn-link text-reset" data-filteraction="add">
<i class="fa fa-plus"></i><span class="pl-3">{{#str}}addcondition, core_user{{/str}}</span>
</button>
<button data-filteraction="reset" type="button" class="btn btn-secondary ml-auto mr-2">{{#str}}clearfilters, core_user{{/str}}</button>
<button data-filteraction="apply" type="button" class="btn btn-primary">{{#str}}applyfilters, core_user{{/str}}</button>
</div>
{{> core_user/local/participantsfilter/filtertypes}}
</div>
{{> core/datafilter/filter }}
{{#js}}
require(['core_user/participantsfilter'], function(ParticipantsFilter) {
ParticipantsFilter.init('core_user-participantsfilter-{{uniqid}}');
});
require(['core_user/participants_filter'], function(ParticipantsFilter) {
ParticipantsFilter.init('core-filter-{{uniqid}}');
});
{{/js}}

View File

@ -9,6 +9,23 @@ This files describes API changes for code that uses the user API.
* deleted
* all potential fullname fields
* Participant filter is moved to core as an API which can be used in different areas of core by implementing the API
and filterable objects. As a part of making the API mature as a core one, these are the js files moved from core
user to core library:
* user/amd/src/local/participantsfilter/filter.js → lib/amd/src/datafilter/filtertype.js
* user/amd/src/local/participantsfilter/filtertypes/country.js → lib/amd/src/datafilter/filtertypes/country.js
* user/amd/src/local/participantsfilter/filtertypes/courseid.js → lib/amd/src/datafilter/filtertypes/courseid.js
* user/amd/src/local/participantsfilter/filtertypes/keyword.js → lib/amd/src/datafilter/filtertypes/keyword.js
* user/amd/src/local/participantsfilter/selectors.js → lib/amd/src/datafilter/selectors.js
The following mustache have been moved from core user to core library:
* user/templates/local/participantsfilter/filterrow.mustache → lib/templates/datafilter/filter_row.mustache
* user/templates/local/participantsfilter/filtertype.mustache → lib/templates/datafilter/filter_type.mustache
* user/templates/local/participantsfilter/filtertypes.mustache → lib/templates/datafilter/filter_types.mustache
* user/templates/local/participantsfilter/autocomplete_layout.mustache → lib/templates/datafilter/autocomplete_layout.mustache
* user/templates/local/participantsfilter/autocomplete_selection.mustache → lib/templates/datafilter/autocomplete_selection.mustache
* user/templates/local/participantsfilter/autocomplete_selection_items.mustache → lib/templates/datafilter/autocomplete_selection_items.mustache
Class participant_filter now extends core filter api in core user.
=== 4.0 ===
* External function core_user_external::update_users() will now fail on a per user basis. Previously if one user