diff --git a/question/bank/columnsortorder/actions.php b/question/bank/columnsortorder/actions.php new file mode 100644 index 00000000000..5f4c8411e0c --- /dev/null +++ b/question/bank/columnsortorder/actions.php @@ -0,0 +1,98 @@ +. + +/** + * Actions controller + * + * Perform a synchronous action to modify the question bank UI and redirect back to the previous page. + * These features are mostly progressively enhanced by actions.js and web services, but this remains as a fallback. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$action = required_param('action', PARAM_TEXT); +$global = optional_param('global', false, PARAM_BOOL); +$returnurl = optional_param('returnurl', '/question/bank/columnsortorder/sortcolumns.php', PARAM_LOCALURL); + +require_login(); + +if ($global) { + require_capability('moodle/site:config', context_system::instance()); +} + +if ($action === 'debugreset' && $CFG->debug === DEBUG_DEVELOPER) { + $columnmanager = new \qbank_columnsortorder\column_manager($global); + $columnmanager::set_hidden_columns([], $global); + $columnmanager::set_column_order([], $global); + $columnmanager::set_column_size('', $global); + redirect(new moodle_url($returnurl)); +} + +require_sesskey(); +$columnmanager = new \qbank_columnsortorder\column_manager($global); +switch ($action) { + case 'add': + case 'remove': + $column = required_param('column', PARAM_RAW); + [$columnclass, ] = explode(\core_question\local\bank\column_base::ID_SEPARATOR, $column); + if (!class_exists($columnclass)) { + throw new invalid_parameter_exception("'{$columnclass}' is not a valid column class."); + } + $hiddencolumns = $columnmanager->hiddencolumns; + if ($action === 'add') { + $key = array_search($column, $hiddencolumns); + if ($key !== false) { + unset($hiddencolumns[$key]); + } + } else { + if (!in_array($column, $hiddencolumns)) { + $hiddencolumns[] = $column; + } + } + $columnmanager::set_hidden_columns($hiddencolumns, $global); + break; + + case 'savewidths': + $rawwidths = optional_param_array('width', [], PARAM_INT); + $widths = []; + foreach (array_filter($rawwidths) as $escapedclass => $width) { + $class = str_replace('__', '\\', $escapedclass); + // Validate that the class exists and the width is valid. + // Since the browser uses Constraint Validation to prevent the form being submitted with an invalid width, + // the only way we'll get one here is if someone is messing around, so don't worry about re-displaying the + // form with an error message, just ignore the invalid value. + if (class_exists($class) && $width >= 10) { + $widths[] = (object)[ + 'column' => $class, + 'width' => $width, + ]; + } + } + $columnmanager::set_column_size(json_encode($widths), $global); + break; + + case 'reset': + $columnmanager::set_hidden_columns([], $global); + $columnmanager::set_column_order([], $global); + $columnmanager::set_column_size('', $global); + break; +} +redirect(new moodle_url($returnurl)); diff --git a/question/bank/columnsortorder/amd/build/actions.min.js b/question/bank/columnsortorder/amd/build/actions.min.js new file mode 100644 index 00000000000..495515bccca --- /dev/null +++ b/question/bank/columnsortorder/amd/build/actions.min.js @@ -0,0 +1,11 @@ +define("qbank_columnsortorder/actions",["exports","core/sortable_list","jquery","qbank_columnsortorder/repository","core/notification","core/fragment","core/templates"],(function(_exports,_sortable_list,_jquery,repository,_notification,_fragment,_templates){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}} +/** + * Common javascript for handling actions on the admin page and the user's view of the question bank. + * + * @module qbank_columnsortorder/actions + * @copyright 2023 onwards Catalyst IT Europe Ltd + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupSortableLists=_exports.setupActionButtons=_exports.getColumnOrder=_exports.SELECTORS=void 0,_sortable_list=_interopRequireDefault(_sortable_list),_jquery=_interopRequireDefault(_jquery),repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(repository),_notification=_interopRequireDefault(_notification),_fragment=_interopRequireDefault(_fragment),_templates=_interopRequireDefault(_templates);const SELECTORS={columnList:".qbank-column-list",sortableColumn:".qbank-sortable-column",removeLink:"[data-action=remove]",moveHandler:"[data-drag-type=move]",addColumn:".addcolumn",addLink:"[data-action=add]",actionLink:".action-link"};_exports.SELECTORS=SELECTORS;_exports.setupSortableLists=function(listRoot){let vertical=arguments.length>1&&void 0!==arguments[1]&&arguments[1],global=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const sortableList=new _sortable_list.default(listRoot,{moveHandlerSelector:SELECTORS.moveHandler,isHorizontal:!vertical});sortableList.getElementName=element=>Promise.resolve(element.data("name"));const sortableColumns=(0,_jquery.default)(SELECTORS.sortableColumn);return sortableColumns.on(_sortable_list.default.EVENTS.DROP,(()=>{repository.setColumnbankOrder(getColumnOrder(listRoot),global).catch(_notification.default.exception),listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach((item=>item.classList.remove("active")))})),sortableColumns.on(_sortable_list.default.EVENTS.DRAGSTART,(event=>{event.currentTarget.classList.add("active")})),sortableColumns};_exports.setupActionButtons=function(uiRoot){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];uiRoot.addEventListener("click",(async e=>{const actionLink=e.target.closest(SELECTORS.actionLink);if(actionLink)try{e.preventDefault();const action=actionLink.dataset.action;if("add"===action||"remove"===action){const hiddenColumns=[],addColumnList=document.querySelector(SELECTORS.addColumn);addColumnList&&addColumnList.querySelectorAll(SELECTORS.addLink).forEach((item=>{"add"===action&&item===actionLink||hiddenColumns.push(item.dataset.column)})),"remove"===action&&hiddenColumns.push(actionLink.dataset.column),await repository.setHiddenColumns(hiddenColumns,global)}else"reset"===action&&await Promise.all([repository.setColumnbankOrder([],global),repository.setHiddenColumns([],global),repository.setColumnSize("",global)]);const fragmentData=uiRoot.dataset;_fragment.default.loadFragment(fragmentData.component,fragmentData.callback,fragmentData.contextid).then(((html,js)=>_templates.default.replaceNode(uiRoot,html,js))).catch(_notification.default.exception)}catch(ex){await _notification.default.exception(ex)}}))};const getColumnOrder=listRoot=>{const columns=Array.from(listRoot.querySelectorAll("[data-columnid]")).map((column=>column.dataset.columnid));return columns.filter(((value,index)=>columns.indexOf(value)===index))};_exports.getColumnOrder=getColumnOrder})); + +//# sourceMappingURL=actions.min.js.map \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/actions.min.js.map b/question/bank/columnsortorder/amd/build/actions.min.js.map new file mode 100644 index 00000000000..dccff042564 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"actions.min.js","sources":["../src/actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Common javascript for handling actions on the admin page and the user's view of the question bank.\n *\n * @module qbank_columnsortorder/actions\n * @copyright 2023 onwards Catalyst IT Europe Ltd\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport SortableList from 'core/sortable_list';\nimport $ from 'jquery';\nimport * as repository from 'qbank_columnsortorder/repository';\nimport Notification from \"core/notification\";\nimport Fragment from \"core/fragment\";\nimport Templates from \"core/templates\";\n\nexport const SELECTORS = {\n columnList: '.qbank-column-list',\n sortableColumn: '.qbank-sortable-column',\n removeLink: '[data-action=remove]',\n moveHandler: '[data-drag-type=move]',\n addColumn: '.addcolumn',\n addLink: '[data-action=add]',\n actionLink: '.action-link',\n};\n\n/**\n * Sets up sortable list in the column sort order page.\n *\n * @param {Element} listRoot Element containing the sortable list.\n * @param {Boolean} vertical Is the list in vertical orientation, rather than horizonal?\n * @param {Boolean} global Should changes be saved to global config, rather than user preferences?\n * @return {jQuery} sortable column elements, for attaching additional event listeners.\n */\nexport const setupSortableLists = (listRoot, vertical = false, global = false) => {\n const sortableList = new SortableList(listRoot, {\n moveHandlerSelector: SELECTORS.moveHandler,\n isHorizontal: !vertical,\n });\n sortableList.getElementName = element => Promise.resolve(element.data('name'));\n\n const sortableColumns = $(SELECTORS.sortableColumn);\n\n sortableColumns.on(SortableList.EVENTS.DROP, () => {\n repository.setColumnbankOrder(getColumnOrder(listRoot), global).catch(Notification.exception);\n listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach(item => item.classList.remove('active'));\n });\n\n sortableColumns.on(SortableList.EVENTS.DRAGSTART, (event) => {\n event.currentTarget.classList.add('active');\n });\n\n return sortableColumns;\n};\n\n/**\n * Set up event handlers for action buttons.\n *\n * For each action, call the web service to update the appropriate setting or user preference, then call the fragment to\n * refresh the view.\n *\n * @param {Element} uiRoot The root of the question bank UI.\n * @param {Boolean} global Should changes be saved to global config, rather than user preferences?\n */\nexport const setupActionButtons = (uiRoot, global = false) => {\n uiRoot.addEventListener('click', async(e) => {\n const actionLink = e.target.closest(SELECTORS.actionLink);\n if (!actionLink) {\n return;\n }\n try {\n e.preventDefault();\n const action = actionLink.dataset.action;\n if (action === 'add' || action === 'remove') {\n const hiddenColumns = [];\n const addColumnList = document.querySelector(SELECTORS.addColumn);\n if (addColumnList) {\n addColumnList.querySelectorAll(SELECTORS.addLink).forEach(item => {\n if (action === 'add' && item === actionLink) {\n return;\n }\n hiddenColumns.push(item.dataset.column);\n });\n }\n if (action === 'remove') {\n hiddenColumns.push(actionLink.dataset.column);\n }\n await repository.setHiddenColumns(hiddenColumns, global);\n } else if (action === 'reset') {\n await Promise.all([\n repository.setColumnbankOrder([], global),\n repository.setHiddenColumns([], global),\n repository.setColumnSize('', global),\n ]);\n }\n const fragmentData = uiRoot.dataset;\n // We have to use then() there, as loadFragment doesn't appear to work with await.\n Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid)\n .then((html, js) => {\n return Templates.replaceNode(uiRoot, html, js);\n })\n .catch(Notification.exception);\n } catch (ex) {\n await Notification.exception(ex);\n }\n });\n};\n\n/**\n * Gets the newly reordered columns to display in the question bank view.\n * @param {Element} listRoot\n * @returns {Array}\n */\nexport const getColumnOrder = listRoot => {\n const columns = Array.from(listRoot.querySelectorAll('[data-columnid]'))\n .map(column => column.dataset.columnid);\n\n return columns.filter((value, index) => columns.indexOf(value) === index);\n};\n"],"names":["SELECTORS","columnList","sortableColumn","removeLink","moveHandler","addColumn","addLink","actionLink","listRoot","vertical","global","sortableList","SortableList","moveHandlerSelector","isHorizontal","getElementName","element","Promise","resolve","data","sortableColumns","on","EVENTS","DROP","repository","setColumnbankOrder","getColumnOrder","catch","Notification","exception","querySelectorAll","forEach","item","classList","remove","DRAGSTART","event","currentTarget","add","uiRoot","addEventListener","async","e","target","closest","preventDefault","action","dataset","hiddenColumns","addColumnList","document","querySelector","push","column","setHiddenColumns","all","setColumnSize","fragmentData","loadFragment","component","callback","contextid","then","html","js","Templates","replaceNode","ex","columns","Array","from","map","columnid","filter","value","index","indexOf"],"mappings":";;;;;;;;6jCA+BaA,UAAY,CACrBC,WAAY,qBACZC,eAAgB,yBAChBC,WAAY,uBACZC,YAAa,wBACbC,UAAW,aACXC,QAAS,oBACTC,WAAY,yEAWkB,SAACC,cAAUC,iEAAkBC,qEACrDC,aAAe,IAAIC,uBAAaJ,SAAU,CAC5CK,oBAAqBb,UAAUI,YAC/BU,cAAeL,WAEnBE,aAAaI,eAAiBC,SAAWC,QAAQC,QAAQF,QAAQG,KAAK,eAEhEC,iBAAkB,mBAAEpB,UAAUE,uBAEpCkB,gBAAgBC,GAAGT,uBAAaU,OAAOC,MAAM,KACzCC,WAAWC,mBAAmBC,eAAelB,UAAWE,QAAQiB,MAAMC,sBAAaC,WACnFrB,SAASsB,iBAAiB9B,UAAUE,gBAAgB6B,SAAQC,MAAQA,KAAKC,UAAUC,OAAO,eAG9Fd,gBAAgBC,GAAGT,uBAAaU,OAAOa,WAAYC,QAC/CA,MAAMC,cAAcJ,UAAUK,IAAI,aAG/BlB,6CAYuB,SAACmB,YAAQ7B,+DACvC6B,OAAOC,iBAAiB,SAASC,MAAAA,UACvBlC,WAAamC,EAAEC,OAAOC,QAAQ5C,UAAUO,eACzCA,eAIDmC,EAAEG,uBACIC,OAASvC,WAAWwC,QAAQD,UACnB,QAAXA,QAA+B,WAAXA,OAAqB,OACnCE,cAAgB,GAChBC,cAAgBC,SAASC,cAAcnD,UAAUK,WACnD4C,eACAA,cAAcnB,iBAAiB9B,UAAUM,SAASyB,SAAQC,OACvC,QAAXc,QAAoBd,OAASzB,YAGjCyC,cAAcI,KAAKpB,KAAKe,QAAQM,WAGzB,WAAXP,QACAE,cAAcI,KAAK7C,WAAWwC,QAAQM,cAEpC7B,WAAW8B,iBAAiBN,cAAetC,YAC/B,UAAXoC,cACD7B,QAAQsC,IAAI,CACd/B,WAAWC,mBAAmB,GAAIf,QAClCc,WAAW8B,iBAAiB,GAAI5C,QAChCc,WAAWgC,cAAc,GAAI9C,gBAG/B+C,aAAelB,OAAOQ,0BAEnBW,aAAaD,aAAaE,UAAWF,aAAaG,SAAUH,aAAaI,WAC7EC,MAAK,CAACC,KAAMC,KACFC,mBAAUC,YAAY3B,OAAQwB,KAAMC,MAE9CrC,MAAMC,sBAAaC,WAC1B,MAAOsC,UACCvC,sBAAaC,UAAUsC,eAU5BzC,eAAiBlB,iBACpB4D,QAAUC,MAAMC,KAAK9D,SAASsB,iBAAiB,oBAChDyC,KAAIlB,QAAUA,OAAON,QAAQyB,kBAE3BJ,QAAQK,QAAO,CAACC,MAAOC,QAAUP,QAAQQ,QAAQF,SAAWC"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/admin_actions.min.js b/question/bank/columnsortorder/amd/build/admin_actions.min.js new file mode 100644 index 00000000000..5754b27c2c0 --- /dev/null +++ b/question/bank/columnsortorder/amd/build/admin_actions.min.js @@ -0,0 +1,11 @@ +define("qbank_columnsortorder/admin_actions",["exports","qbank_columnsortorder/actions","qbank_columnsortorder/repository","core/notification","core/pending"],(function(_exports,actions,repository,_notification,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * Javascript for handling actions on the admin page + * + * @module qbank_columnsortorder/admin_actions + * @copyright 2023 onwards Catalyst IT Europe Ltd + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,actions=_interopRequireWildcard(actions),repository=_interopRequireWildcard(repository),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending);_exports.init=id=>{const uiRoot=document.getElementById(id),listRoot=uiRoot.querySelector(actions.SELECTORS.columnList);actions.setupSortableLists(listRoot,!0,!0),actions.setupActionButtons(uiRoot,!0),(listRoot=>{listRoot.addEventListener("change",(async()=>{const pendingPromise=new _pending.default("saveWidths"),columns=listRoot.querySelectorAll(actions.SELECTORS.sortableColumn),widths=[];columns.forEach((column=>{const widthInput=column.querySelector(".width-input"),valid=widthInput.checkValidity();widthInput.closest(".has-validation").classList.add("was-validated"),valid&&widths.push({column:column.dataset.columnid,width:widthInput.value})})),await repository.setColumnSize(JSON.stringify(widths),!0).catch(_notification.default.exception),pendingPromise.resolve()}))})(listRoot)}})); + +//# sourceMappingURL=admin_actions.min.js.map \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/admin_actions.min.js.map b/question/bank/columnsortorder/amd/build/admin_actions.min.js.map new file mode 100644 index 00000000000..1576e7991cc --- /dev/null +++ b/question/bank/columnsortorder/amd/build/admin_actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"admin_actions.min.js","sources":["../src/admin_actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript for handling actions on the admin page\n *\n * @module qbank_columnsortorder/admin_actions\n * @copyright 2023 onwards Catalyst IT Europe Ltd\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as actions from 'qbank_columnsortorder/actions';\nimport * as repository from 'qbank_columnsortorder/repository';\nimport Notification from \"core/notification\";\nimport Pending from 'core/pending';\n\n/**\n * Event handler to save the custom column widths when a field is edited.\n *\n * @param {Element} listRoot The root element of the list of columns.\n */\nconst setupSaveWidths = listRoot => {\n listRoot.addEventListener('change', async() => {\n const pendingPromise = new Pending('saveWidths');\n const columns = listRoot.querySelectorAll(actions.SELECTORS.sortableColumn);\n const widths = [];\n columns.forEach(column => {\n const widthInput = column.querySelector('.width-input');\n const valid = widthInput.checkValidity();\n widthInput.closest('.has-validation').classList.add('was-validated');\n if (!valid) {\n return;\n }\n widths.push({\n column: column.dataset.columnid,\n width: widthInput.value,\n });\n });\n await repository.setColumnSize(JSON.stringify(widths), true).catch(Notification.exception);\n pendingPromise.resolve();\n });\n};\n\n/**\n * Initialize module\n *\n * Set up event handlers for the action buttons, width fields and initialise column sorting.\n *\n * @param {String} id ID for the admin UI root element.\n */\nexport const init = id => {\n const uiRoot = document.getElementById(id);\n const listRoot = uiRoot.querySelector(actions.SELECTORS.columnList);\n actions.setupSortableLists(listRoot, true, true);\n actions.setupActionButtons(uiRoot, true);\n setupSaveWidths(listRoot);\n};\n"],"names":["id","uiRoot","document","getElementById","listRoot","querySelector","actions","SELECTORS","columnList","setupSortableLists","setupActionButtons","addEventListener","async","pendingPromise","Pending","columns","querySelectorAll","sortableColumn","widths","forEach","column","widthInput","valid","checkValidity","closest","classList","add","push","dataset","columnid","width","value","repository","setColumnSize","JSON","stringify","catch","Notification","exception","resolve","setupSaveWidths"],"mappings":";;;;;;;;sRA+DoBA,WACVC,OAASC,SAASC,eAAeH,IACjCI,SAAWH,OAAOI,cAAcC,QAAQC,UAAUC,YACxDF,QAAQG,mBAAmBL,UAAU,GAAM,GAC3CE,QAAQI,mBAAmBT,QAAQ,GAjCfG,CAAAA,WACpBA,SAASO,iBAAiB,UAAUC,gBAC1BC,eAAiB,IAAIC,iBAAQ,cAC7BC,QAAUX,SAASY,iBAAiBV,QAAQC,UAAUU,gBACtDC,OAAS,GACfH,QAAQI,SAAQC,eACNC,WAAaD,OAAOf,cAAc,gBAClCiB,MAAQD,WAAWE,gBACzBF,WAAWG,QAAQ,mBAAmBC,UAAUC,IAAI,iBAC/CJ,OAGLJ,OAAOS,KAAK,CACRP,OAAQA,OAAOQ,QAAQC,SACvBC,MAAOT,WAAWU,iBAGpBC,WAAWC,cAAcC,KAAKC,UAAUjB,SAAS,GAAMkB,MAAMC,sBAAaC,WAChFzB,eAAe0B,cAgBnBC,CAAgBpC"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/repository.min.js b/question/bank/columnsortorder/amd/build/repository.min.js new file mode 100644 index 00000000000..adc57edd0ae --- /dev/null +++ b/question/bank/columnsortorder/amd/build/repository.min.js @@ -0,0 +1,3 @@ +define("qbank_columnsortorder/repository",["exports","core/ajax"],(function(_exports,_ajax){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setHiddenColumns=_exports.setColumnbankOrder=_exports.setColumnSize=void 0;_exports.setHiddenColumns=function(columns){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const responses=(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_hidden_columns",args:{columns:columns,global:global}}]);return responses[0]};_exports.setColumnbankOrder=function(columns){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_columnbank_order",args:{columns:columns,global:global}}])[0]};_exports.setColumnSize=function(sizes){let global=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return(0,_ajax.call)([{methodname:"qbank_columnsortorder_set_column_size",args:{sizes:sizes,global:global}}])[0]}})); + +//# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/repository.min.js.map b/question/bank/columnsortorder/amd/build/repository.min.js.map new file mode 100644 index 00000000000..7ac212df0cf --- /dev/null +++ b/question/bank/columnsortorder/amd/build/repository.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * External function calls for qbank_columnsortorder\n *\n * @module qbank_columnsortorder/repository\n * @copyright 2023 Catalyst IT Europe Ltd.\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\n\n/**\n * Save the list of hidden columns\n *\n * @param {String[]} columns List of hidden column names\n * @param {Boolean} global Set global config setting, rather than user preference\n * @return {Promise}\n */\nexport const setHiddenColumns = (columns, global = false) => {\n const responses = fetchMany([{\n methodname: 'qbank_columnsortorder_set_hidden_columns',\n args: {columns: columns, global: global},\n }]);\n return responses[0];\n};\n\n/**\n * Save the order of columns\n *\n * @param {String[]} columns List of column names in the desired order\n * @param {Boolean} global Set global config setting, rather than user preference\n * @return {Promise}\n */\nexport const setColumnbankOrder = (columns, global = false) => {\n return fetchMany([{\n methodname: 'qbank_columnsortorder_set_columnbank_order',\n args: {columns: columns, global: global},\n }])[0];\n};\n\n/**\n * Save the column widths\n *\n * @param {String} sizes JSON string encoding an array of objects with \"column\" and \"width\" properties.\n * @param {Boolean} global Set global config setting, rather than user preference\n * @return {Promise}\n */\nexport const setColumnSize = (sizes, global = false) => {\n return fetchMany([{\n methodname: 'qbank_columnsortorder_set_column_size',\n args: {sizes: sizes, global: global},\n }])[0];\n};\n"],"names":["columns","global","responses","methodname","args","sizes"],"mappings":"kQAiCgC,SAACA,aAASC,qEAChCC,WAAY,cAAU,CAAC,CACzBC,WAAY,2CACZC,KAAM,CAACJ,QAASA,QAASC,OAAQA,kBAE9BC,UAAU,gCAUa,SAACF,aAASC,sEACjC,cAAU,CAAC,CACdE,WAAY,6CACZC,KAAM,CAACJ,QAASA,QAASC,OAAQA,WACjC,2BAUqB,SAACI,WAAOJ,sEAC1B,cAAU,CAAC,CACdE,WAAY,wCACZC,KAAM,CAACC,MAAOA,MAAOJ,OAAQA,WAC7B"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/user_actions.min.js b/question/bank/columnsortorder/amd/build/user_actions.min.js new file mode 100644 index 00000000000..16f3f9ee4ef --- /dev/null +++ b/question/bank/columnsortorder/amd/build/user_actions.min.js @@ -0,0 +1,11 @@ +define("qbank_columnsortorder/user_actions",["exports","qbank_columnsortorder/actions","qbank_columnsortorder/repository","core/str","core/modal_events","core/modal_factory","core/notification","core/sortable_list","core/templates"],(function(_exports,actions,repository,_str,_modal_events,_modal_factory,_notification,_sortable_list,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * Javascript for customising the user's view of the question bank + * + * @module qbank_columnsortorder/user_actions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Ghaly Marc-Alexandre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,actions=_interopRequireWildcard(actions),repository=_interopRequireWildcard(repository),_modal_events=_interopRequireDefault(_modal_events),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification),_sortable_list=_interopRequireDefault(_sortable_list),_templates=_interopRequireDefault(_templates);const SELECTORS_uiRoot=".questionbankwindow",SELECTORS_moveAction=".menu-action[data-action=move]",SELECTORS_resizeAction=".menu-action[data-action=resize]",SELECTORS_resizeHandle=".qbank_columnsortorder-action-handle.resize",SELECTORS_handleContainer=".handle-container",SELECTORS_headerContainer=".header-container",SELECTORS_tableColumn=identifier=>'td[data-columnid="'.concat(identifier.replace(/["\\]/g,"\\$&"),'"]');let currentHeader,currentX,suspendShowHideHandles=!1;const serialiseColumnSizes=uiRoot=>{const columnSizes=[];return uiRoot.querySelectorAll("th").forEach((header=>{const width=parseInt(header.style.width);width&&!isNaN(width)&&columnSizes.push({column:header.dataset.columnid,width:width})})),JSON.stringify(columnSizes)},showResizeModal=async(currentHeader,uiRoot)=>{const initialWidth=currentHeader.offsetWidth,modal=await _modal_factory.default.create({title:(0,_str.get_string)("resizecolumn","qbank_columnsortorder",currentHeader.textContent),type:_modal_factory.default.types.SAVE_CANCEL,body:_templates.default.render("qbank_columnsortorder/resize_modal",{})}),root=modal.getRoot();root.on(_modal_events.default.cancel,(()=>{currentHeader.style.width=initialWidth+"px"})),root.on(_modal_events.default.save,(()=>{repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(_notification.default.exception)})),modal.show();const input=(await modal.bodyPromise).get(0).querySelector("input");input.value=initialWidth,input.addEventListener("change",(e=>{const newWidth=e.target.value;currentHeader.style.width=newWidth+"px"}))},reorderColumns=event=>{const header=event.target,insertAfter=header.previousElementSibling;document.querySelector(SELECTORS_uiRoot).querySelectorAll(SELECTORS_tableColumn(header.dataset.columnid)).forEach((column=>{const row=column.parentElement;if(insertAfter){row.querySelector(SELECTORS_tableColumn(insertAfter.dataset.columnid)).after(column)}else row.insertBefore(column,row.firstChild)}))};_exports.init=async()=>{const uiRoot=document.querySelector(".questionbankwindow");await(uiRoot=>new Promise((resolve=>{const headerContainers=uiRoot.querySelectorAll(SELECTORS_headerContainer);_templates.default.renderForPromise("qbank_columnsortorder/handle_container",{}).then((_ref=>{let{html:html,js:js}=_ref;return headerContainers.forEach((container=>{_templates.default.prependNodeContents(container,html,js)})),resolve(),headerContainers})).catch(_notification.default.exception)})))(uiRoot),uiRoot.querySelectorAll(SELECTORS_moveAction).forEach((moveAction=>{const header=moveAction.closest("th");header.classList.add("qbank-sortable-column");const handleContainer=header.querySelector(SELECTORS_handleContainer),context={action:"move",dragtype:"move",target:"",title:moveAction.title,pixicon:"i/dragdrop",pixcomponent:"core",popup:!0};return _templates.default.renderForPromise("qbank_columnsortorder/action_handle",context).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.prependNodeContents(handleContainer,html,js),handleContainer})).catch(_notification.default.exception)})),(uiRoot=>{uiRoot.querySelectorAll(SELECTORS_resizeAction).forEach((resizeAction=>{const handleContainer=resizeAction.closest(SELECTORS_headerContainer).querySelector(SELECTORS_handleContainer),context={action:"resize",target:"",title:resizeAction.title,pixicon:"i/twoway",pixcomponent:"core",popup:!0};return _templates.default.renderForPromise("qbank_columnsortorder/action_handle",context).then((_ref3=>{let{html:html,js:js}=_ref3;return _templates.default.appendNodeContents(handleContainer,html,js),handleContainer})).catch(_notification.default.exception)}));let moveTracker=!1,currentResizeHandle=null;uiRoot.addEventListener("mousedown",(e=>{currentResizeHandle=e.target.closest(SELECTORS_resizeHandle),currentResizeHandle&&(currentX=e.pageX,currentHeader=e.target.closest(actions.SELECTORS.sortableColumn),moveTracker=!1,suspendShowHideHandles=!0)})),document.addEventListener("mousemove",(e=>{if(!currentHeader||!currentResizeHandle||0===currentX)return;document.getSelection().removeAllRanges();const offset=e.pageX-currentX;currentX=e.pageX;const newWidth=currentHeader.offsetWidth+offset;currentHeader.style.width=newWidth+"px",moveTracker=!0})),document.addEventListener("mouseup",(()=>{currentHeader&¤tResizeHandle&&0!==currentX&&(moveTracker?repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(_notification.default.exception):showResizeModal(currentHeader,uiRoot),currentHeader=null,currentResizeHandle=null,currentX=0,moveTracker=!1,suspendShowHideHandles=!1)}))})(uiRoot),(uiRoot=>{let shownHeader=null,tableHead=uiRoot.querySelector("thead");uiRoot.addEventListener("mouseover",(e=>{if(suspendShowHideHandles)return;const header=e.target.closest(actions.SELECTORS.sortableColumn);var _tableHead$querySelec;(header||shownHeader)&&(header&&header===shownHeader||(null===(_tableHead$querySelec=tableHead.querySelector(".show-handles"))||void 0===_tableHead$querySelec||_tableHead$querySelec.classList.remove("show-handles"),shownHeader=header,header&&header.classList.add("show-handles")))}))})(uiRoot);const sortableColumns=actions.setupSortableLists(uiRoot.querySelector(actions.SELECTORS.columnList));sortableColumns.on(_sortable_list.default.EVENTS.DROP,reorderColumns),sortableColumns.on(_sortable_list.default.EVENTS.DRAGSTART,(()=>{suspendShowHideHandles=!0})),sortableColumns.on(_sortable_list.default.EVENTS.DRAGEND,(()=>{suspendShowHideHandles=!1})),(uiRoot=>{uiRoot.addEventListener("click",(e=>{const moveAction=e.target.closest(SELECTORS_moveAction);moveAction&&(e.preventDefault(),moveAction.closest(actions.SELECTORS.sortableColumn).querySelector(actions.SELECTORS.moveHandler).click())}))})(uiRoot),(uiRoot=>{uiRoot.addEventListener("click",(e=>{const resizeAction=e.target.closest(SELECTORS_resizeAction);if(resizeAction){e.preventDefault();const currentHeader=resizeAction.closest("th");showResizeModal(currentHeader,uiRoot)}}))})(uiRoot)}})); + +//# sourceMappingURL=user_actions.min.js.map \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/build/user_actions.min.js.map b/question/bank/columnsortorder/amd/build/user_actions.min.js.map new file mode 100644 index 00000000000..d0f812cf50d --- /dev/null +++ b/question/bank/columnsortorder/amd/build/user_actions.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"user_actions.min.js","sources":["../src/user_actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript for customising the user's view of the question bank\n *\n * @module qbank_columnsortorder/user_actions\n * @copyright 2021 Catalyst IT Australia Pty Ltd\n * @author Ghaly Marc-Alexandre \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as actions from 'qbank_columnsortorder/actions';\nimport * as repository from 'qbank_columnsortorder/repository';\nimport {get_string as getString} from 'core/str';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from \"core/notification\";\nimport SortableList from 'core/sortable_list';\nimport Templates from \"core/templates\";\n\n\nconst SELECTORS = {\n uiRoot: '.questionbankwindow',\n moveAction: '.menu-action[data-action=move]',\n resizeAction: '.menu-action[data-action=resize]',\n resizeHandle: '.qbank_columnsortorder-action-handle.resize',\n handleContainer: '.handle-container',\n headerContainer: '.header-container',\n tableColumn: identifier => `td[data-columnid=\"${identifier.replace(/[\"\\\\]/g, '\\\\$&')}\"]`,\n};\n\n/** To track mouse event on a table header */\nlet currentHeader;\n\n/** Current mouse x postion, to track mouse event on a table header */\nlet currentX;\n\n/**\n * Flag to temporarily prevent move and resize handles from being shown or hidden.\n *\n * @type {boolean}\n */\nlet suspendShowHideHandles = false;\n\n/**\n * Add handle containers for move and resize handles.\n *\n * @param {Element} uiRoot The root element of the quesiton bank UI.\n * @return {Promise} Resolved after the containers have been added to each column header.\n */\nconst addHandleContainers = uiRoot => {\n return new Promise((resolve) => {\n const headerContainers = uiRoot.querySelectorAll(SELECTORS.headerContainer);\n Templates.renderForPromise('qbank_columnsortorder/handle_container', {})\n .then(({html, js}) => {\n headerContainers.forEach(container => {\n Templates.prependNodeContents(container, html, js);\n });\n resolve();\n return headerContainers;\n }).catch(Notification.exception);\n });\n};\n\n/**\n * Render move handles in each container.\n *\n * This takes a list of the move actions rendered in each column header, and creates a corresponding drag handle for each.\n *\n * @param {NodeList} moveActions Menu actions for moving columns.\n */\nconst setUpMoveHandles = moveActions => {\n moveActions.forEach(moveAction => {\n const header = moveAction.closest('th');\n header.classList.add('qbank-sortable-column');\n const handleContainer = header.querySelector(SELECTORS.handleContainer);\n const context = {\n action: \"move\",\n dragtype: \"move\",\n target: '',\n title: moveAction.title,\n pixicon: \"i/dragdrop\",\n pixcomponent: \"core\",\n popup: true\n };\n return Templates.renderForPromise('qbank_columnsortorder/action_handle', context)\n .then(({html, js}) => {\n Templates.prependNodeContents(handleContainer, html, js);\n return handleContainer;\n }).catch(Notification.exception);\n });\n};\n\n/**\n * Serialise the current column sizes.\n *\n * This finds the current width set in each column header's style property, and returns them encoded as a JSON string.\n *\n * @param {Element} uiRoot The root element of the quesiton bank UI.\n * @return {String} JSON array containing a list of objects with column and width properties.\n */\nconst serialiseColumnSizes = (uiRoot) => {\n const columnSizes = [];\n const tableHeaders = uiRoot.querySelectorAll('th');\n tableHeaders.forEach(header => {\n // Only get the width set via style attribute (set by move action).\n const width = parseInt(header.style.width);\n if (!width || isNaN(width)) {\n return;\n }\n columnSizes.push({\n column: header.dataset.columnid,\n width: width\n });\n });\n return JSON.stringify(columnSizes);\n};\n\n/**\n * Render resize handles in each container.\n *\n * This takes a list of the resize actions rendered in each column header, and creates a corresponding drag handle for each.\n * It also initialises the event handlers for the drag handles and resize modal.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setUpResizeHandles = (uiRoot) => {\n const resizeActions = uiRoot.querySelectorAll(SELECTORS.resizeAction);\n resizeActions.forEach(resizeAction => {\n const headerContainer = resizeAction.closest(SELECTORS.headerContainer);\n const handleContainer = headerContainer.querySelector(SELECTORS.handleContainer);\n const context = {\n action: \"resize\",\n target: '',\n title: resizeAction.title,\n pixicon: 'i/twoway',\n pixcomponent: 'core',\n popup: true\n };\n return Templates.renderForPromise('qbank_columnsortorder/action_handle', context)\n .then(({html, js}) => {\n Templates.appendNodeContents(handleContainer, html, js);\n return handleContainer;\n }).catch(Notification.exception);\n });\n\n let moveTracker = false;\n let currentResizeHandle = null;\n // Start mouse event on headers.\n uiRoot.addEventListener('mousedown', e => {\n currentResizeHandle = e.target.closest(SELECTORS.resizeHandle);\n // Return if it is not ' resize' button.\n if (!currentResizeHandle) {\n return;\n }\n // Save current position.\n currentX = e.pageX;\n // Find the header.\n currentHeader = e.target.closest(actions.SELECTORS.sortableColumn);\n moveTracker = false;\n suspendShowHideHandles = true;\n });\n\n // Resize column as the mouse move.\n document.addEventListener('mousemove', e => {\n if (!currentHeader || !currentResizeHandle || currentX === 0) {\n return;\n }\n\n // Prevent text selection as the handle is dragged.\n document.getSelection().removeAllRanges();\n\n // Adjust the column width according the amount the handle was dragged.\n const offset = e.pageX - currentX;\n currentX = e.pageX;\n const newWidth = currentHeader.offsetWidth + offset;\n currentHeader.style.width = newWidth + 'px';\n moveTracker = true;\n });\n\n // Set new size when mouse is up.\n document.addEventListener('mouseup', () => {\n if (!currentHeader || !currentResizeHandle || currentX === 0) {\n return;\n }\n if (moveTracker) {\n // If the mouse moved, we are changing the size by drag, so save the change.\n repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception);\n } else {\n // If the mouse didn't move, display a modal to change the size using a form.\n showResizeModal(currentHeader, uiRoot);\n }\n currentHeader = null;\n currentResizeHandle = null;\n currentX = 0;\n moveTracker = false;\n suspendShowHideHandles = false;\n });\n};\n\n/**\n * Event handler for resize actions in each column header.\n *\n * This will listen for a click on any resize action, and activate the corresponding resize modal.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setUpResizeActions = uiRoot => {\n uiRoot.addEventListener('click', (e) => {\n const resizeAction = e.target.closest(SELECTORS.resizeAction);\n if (resizeAction) {\n e.preventDefault();\n const currentHeader = resizeAction.closest('th');\n showResizeModal(currentHeader, uiRoot);\n }\n });\n};\n\n/**\n * Show a modal containing a number input for changing a column width without click-and-drag.\n *\n * @param {Element} currentHeader The header element that is being resized.\n * @param {Element} uiRoot The question bank UI root element.\n * @returns {Promise}\n */\nconst showResizeModal = async(currentHeader, uiRoot) => {\n\n const initialWidth = currentHeader.offsetWidth;\n\n const modal = await ModalFactory.create({\n title: getString('resizecolumn', 'qbank_columnsortorder', currentHeader.textContent),\n type: ModalFactory.types.SAVE_CANCEL,\n body: Templates.render('qbank_columnsortorder/resize_modal', {})\n });\n const root = modal.getRoot();\n root.on(ModalEvents.cancel, () => {\n currentHeader.style.width = initialWidth + 'px';\n });\n root.on(ModalEvents.save, () => {\n repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception);\n });\n modal.show();\n\n const body = await modal.bodyPromise;\n const input = body.get(0).querySelector('input');\n input.value = initialWidth;\n\n input.addEventListener('change', e => {\n const newWidth = e.target.value;\n currentHeader.style.width = newWidth + 'px';\n });\n};\n\n/**\n * Event handler for move actions in each column header.\n *\n * This will listen for a click on any move action, pass the click to the corresponding move handle, causing its modal to be shown.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setUpMoveActions = uiRoot => {\n uiRoot.addEventListener('click', e => {\n const moveAction = e.target.closest(SELECTORS.moveAction);\n if (moveAction) {\n e.preventDefault();\n const sortableColumn = moveAction.closest(actions.SELECTORS.sortableColumn);\n const moveHandle = sortableColumn.querySelector(actions.SELECTORS.moveHandler);\n moveHandle.click();\n }\n });\n};\n\n/**\n * Event handler for showing and hiding handles when the mouse is over a column header.\n *\n * Implementing this behaviour using the :hover CSS pseudoclass is not sufficient, as the mouse may move over the neighbouring\n * header while dragging, leading to some odd behaviour. This allows us to suspend the show/hide behaviour while a handle is being\n * dragged, and so keep the active handle visible until the drag is finished.\n *\n * @param {Element} uiRoot Question bank UI root element.\n */\nconst setupShowHideHandles = uiRoot => {\n let shownHeader = null;\n let tableHead = uiRoot.querySelector('thead');\n uiRoot.addEventListener('mouseover', e => {\n if (suspendShowHideHandles) {\n return;\n }\n const header = e.target.closest(actions.SELECTORS.sortableColumn);\n if (!header && !shownHeader) {\n return;\n }\n if (!header || header !== shownHeader) {\n tableHead.querySelector('.show-handles')?.classList.remove('show-handles');\n shownHeader = header;\n if (header) {\n header.classList.add('show-handles');\n }\n }\n });\n};\n\n/**\n * Event handler for sortable list DROP event.\n *\n * Find all table cells corresponding to the column of the dropped header, and move them to the new position.\n *\n * @param {Event} event\n */\nconst reorderColumns = event => {\n // Current header.\n const header = event.target;\n // Find the previous sibling of the header, which will be used when moving columns.\n const insertAfter = header.previousElementSibling;\n // Move columns.\n const uiRoot = document.querySelector(SELECTORS.uiRoot);\n const columns = uiRoot.querySelectorAll(SELECTORS.tableColumn(header.dataset.columnid));\n columns.forEach(column => {\n const row = column.parentElement;\n if (insertAfter) {\n // Find the column to insert after.\n const insertAfterColumn = row.querySelector(SELECTORS.tableColumn(insertAfter.dataset.columnid));\n // Insert the column.\n insertAfterColumn.after(column);\n } else {\n // Insert as the first child (first column in the table).\n row.insertBefore(column, row.firstChild);\n }\n });\n};\n\n/**\n * Initialize module\n *\n * Add containers for the drag handles to each column header, then render handles, enable show/hide behaviour, set up drag/drop\n * column sorting, then enable the move and resize modals to be triggered from menu actions.\n */\nexport const init = async() => {\n const uiRoot = document.querySelector('.questionbankwindow');\n await addHandleContainers(uiRoot);\n setUpMoveHandles(uiRoot.querySelectorAll(SELECTORS.moveAction));\n setUpResizeHandles(uiRoot);\n setupShowHideHandles(uiRoot);\n const sortableColumns = actions.setupSortableLists(uiRoot.querySelector(actions.SELECTORS.columnList));\n sortableColumns.on(SortableList.EVENTS.DROP, reorderColumns);\n sortableColumns.on(SortableList.EVENTS.DRAGSTART, () => {\n suspendShowHideHandles = true;\n });\n sortableColumns.on(SortableList.EVENTS.DRAGEND, () => {\n suspendShowHideHandles = false;\n });\n setUpMoveActions(uiRoot);\n setUpResizeActions(uiRoot);\n};\n"],"names":["SELECTORS","identifier","replace","currentHeader","currentX","suspendShowHideHandles","serialiseColumnSizes","uiRoot","columnSizes","querySelectorAll","forEach","header","width","parseInt","style","isNaN","push","column","dataset","columnid","JSON","stringify","showResizeModal","async","initialWidth","offsetWidth","modal","ModalFactory","create","title","textContent","type","types","SAVE_CANCEL","body","Templates","render","root","getRoot","on","ModalEvents","cancel","save","repository","setColumnSize","catch","Notification","exception","show","input","bodyPromise","get","querySelector","value","addEventListener","e","newWidth","target","reorderColumns","event","insertAfter","previousElementSibling","document","row","parentElement","after","insertBefore","firstChild","Promise","resolve","headerContainers","renderForPromise","then","_ref","html","js","container","prependNodeContents","addHandleContainers","moveAction","closest","classList","add","handleContainer","context","action","dragtype","pixicon","pixcomponent","popup","_ref2","resizeAction","_ref3","appendNodeContents","moveTracker","currentResizeHandle","pageX","actions","sortableColumn","getSelection","removeAllRanges","offset","setUpResizeHandles","shownHeader","tableHead","remove","setupShowHideHandles","sortableColumns","setupSortableLists","columnList","SortableList","EVENTS","DROP","DRAGSTART","DRAGEND","preventDefault","moveHandler","click","setUpMoveActions","setUpResizeActions"],"mappings":";;;;;;;;kbAkCMA,iBACM,sBADNA,qBAEU,iCAFVA,uBAGY,mCAHZA,uBAIY,8CAJZA,0BAKe,oBALfA,0BAMe,oBANfA,sBAOWC,wCAAmCA,WAAWC,QAAQ,SAAU,kBAI7EC,cAGAC,SAOAC,wBAAyB,QA2DvBC,qBAAwBC,eACpBC,YAAc,UACCD,OAAOE,iBAAiB,MAChCC,SAAQC,eAEXC,MAAQC,SAASF,OAAOG,MAAMF,OAC/BA,QAASG,MAAMH,QAGpBJ,YAAYQ,KAAK,CACbC,OAAQN,OAAOO,QAAQC,SACvBP,MAAOA,WAGRQ,KAAKC,UAAUb,cA8GpBc,gBAAkBC,MAAMpB,cAAeI,gBAEnCiB,aAAerB,cAAcsB,YAE7BC,YAAcC,uBAAaC,OAAO,CACpCC,OAAO,mBAAU,eAAgB,wBAAyB1B,cAAc2B,aACxEC,KAAMJ,uBAAaK,MAAMC,YACzBC,KAAMC,mBAAUC,OAAO,qCAAsC,MAE3DC,KAAOX,MAAMY,UACnBD,KAAKE,GAAGC,sBAAYC,QAAQ,KACxBtC,cAAcW,MAAMF,MAAQY,aAAe,QAE/Ca,KAAKE,GAAGC,sBAAYE,MAAM,KACtBC,WAAWC,cAActC,qBAAqBC,SAASsC,MAAMC,sBAAaC,cAE9ErB,MAAMsB,aAGAC,aADavB,MAAMwB,aACNC,IAAI,GAAGC,cAAc,SACxCH,MAAMI,MAAQ7B,aAEdyB,MAAMK,iBAAiB,UAAUC,UACvBC,SAAWD,EAAEE,OAAOJ,MAC1BlD,cAAcW,MAAMF,MAAQ4C,SAAW,SA4DzCE,eAAiBC,cAEbhD,OAASgD,MAAMF,OAEfG,YAAcjD,OAAOkD,uBAEZC,SAASV,cAAcpD,kBACfS,iBAAiBT,sBAAsBW,OAAOO,QAAQC,WACrET,SAAQO,eACN8C,IAAM9C,OAAO+C,iBACfJ,YAAa,CAEaG,IAAIX,cAAcpD,sBAAsB4D,YAAY1C,QAAQC,WAEpE8C,MAAMhD,aAGxB8C,IAAIG,aAAajD,OAAQ8C,IAAII,8BAWrB5C,gBACVhB,OAASuD,SAASV,cAAc,4BAhSd7C,CAAAA,QACjB,IAAI6D,SAASC,gBACVC,iBAAmB/D,OAAOE,iBAAiBT,8CACvCuE,iBAAiB,yCAA0C,IAChEC,MAAKC,WAACC,KAACA,KAADC,GAAOA,gBACVL,iBAAiB5D,SAAQkE,+BACXC,oBAAoBD,UAAWF,KAAMC,OAEnDN,UACOC,oBACRzB,MAAMC,sBAAaC,cAuRxB+B,CAAoBvE,QACTA,OAAOE,iBAAiBT,sBA5Q7BU,SAAQqE,mBACVpE,OAASoE,WAAWC,QAAQ,MAClCrE,OAAOsE,UAAUC,IAAI,+BACfC,gBAAkBxE,OAAOyC,cAAcpD,2BACvCoF,QAAU,CACZC,OAAQ,OACRC,SAAU,OACV7B,OAAQ,GACR5B,MAAOkD,WAAWlD,MAClB0D,QAAS,aACTC,aAAc,OACdC,OAAO,UAEJtD,mBAAUoC,iBAAiB,sCAAuCa,SACpEZ,MAAKkB,YAAChB,KAACA,KAADC,GAAOA,oCACAE,oBAAoBM,gBAAiBT,KAAMC,IAC9CQ,mBACRtC,MAAMC,sBAAaC,cAqCNxC,CAAAA,SACFA,OAAOE,iBAAiBT,wBAChCU,SAAQiF,qBAEZR,gBADkBQ,aAAaX,QAAQhF,2BACLoD,cAAcpD,2BAChDoF,QAAU,CACZC,OAAQ,SACR5B,OAAQ,GACR5B,MAAO8D,aAAa9D,MACpB0D,QAAS,WACTC,aAAc,OACdC,OAAO,UAEJtD,mBAAUoC,iBAAiB,sCAAuCa,SACpEZ,MAAKoB,YAAClB,KAACA,KAADC,GAAOA,oCACAkB,mBAAmBV,gBAAiBT,KAAMC,IAC7CQ,mBACRtC,MAAMC,sBAAaC,kBAG1B+C,aAAc,EACdC,oBAAsB,KAE1BxF,OAAO+C,iBAAiB,aAAaC,IACjCwC,oBAAsBxC,EAAEE,OAAOuB,QAAQhF,wBAElC+F,sBAIL3F,SAAWmD,EAAEyC,MAEb7F,cAAgBoD,EAAEE,OAAOuB,QAAQiB,QAAQjG,UAAUkG,gBACnDJ,aAAc,EACdzF,wBAAyB,MAI7ByD,SAASR,iBAAiB,aAAaC,QAC9BpD,gBAAkB4F,qBAAoC,IAAb3F,gBAK9C0D,SAASqC,eAAeC,wBAGlBC,OAAS9C,EAAEyC,MAAQ5F,SACzBA,SAAWmD,EAAEyC,YACPxC,SAAWrD,cAAcsB,YAAc4E,OAC7ClG,cAAcW,MAAMF,MAAQ4C,SAAW,KACvCsC,aAAc,KAIlBhC,SAASR,iBAAiB,WAAW,KAC5BnD,eAAkB4F,qBAAoC,IAAb3F,WAG1C0F,YAEAnD,WAAWC,cAActC,qBAAqBC,SAASsC,MAAMC,sBAAaC,WAG1EzB,gBAAgBnB,cAAeI,QAEnCJ,cAAgB,KAChB4F,oBAAsB,KACtB3F,SAAW,EACX0F,aAAc,EACdzF,wBAAyB,OAiJ7BiG,CAAmB/F,QA5DMA,CAAAA,aACrBgG,YAAc,KACdC,UAAYjG,OAAO6C,cAAc,SACrC7C,OAAO+C,iBAAiB,aAAaC,OAC7BlD,oCAGEM,OAAS4C,EAAEE,OAAOuB,QAAQiB,QAAQjG,UAAUkG,2CAC7CvF,QAAW4F,eAGX5F,QAAUA,SAAW4F,4CACtBC,UAAUpD,cAAc,yEAAkB6B,UAAUwB,OAAO,gBAC3DF,YAAc5F,OACVA,QACAA,OAAOsE,UAAUC,IAAI,sBA8CjCwB,CAAqBnG,cACfoG,gBAAkBV,QAAQW,mBAAmBrG,OAAO6C,cAAc6C,QAAQjG,UAAU6G,aAC1FF,gBAAgBpE,GAAGuE,uBAAaC,OAAOC,KAAMtD,gBAC7CiD,gBAAgBpE,GAAGuE,uBAAaC,OAAOE,WAAW,KAC9C5G,wBAAyB,KAE7BsG,gBAAgBpE,GAAGuE,uBAAaC,OAAOG,SAAS,KAC5C7G,wBAAyB,KAzFRE,CAAAA,SACrBA,OAAO+C,iBAAiB,SAASC,UACvBwB,WAAaxB,EAAEE,OAAOuB,QAAQhF,sBAChC+E,aACAxB,EAAE4D,iBACqBpC,WAAWC,QAAQiB,QAAQjG,UAAUkG,gBAC1B9C,cAAc6C,QAAQjG,UAAUoH,aACvDC,aAoFnBC,CAAiB/G,QAhJMA,CAAAA,SACvBA,OAAO+C,iBAAiB,SAAUC,UACxBoC,aAAepC,EAAEE,OAAOuB,QAAQhF,2BAClC2F,aAAc,CACdpC,EAAE4D,uBACIhH,cAAgBwF,aAAaX,QAAQ,MAC3C1D,gBAAgBnB,cAAeI,aA2IvCgH,CAAmBhH"} \ No newline at end of file diff --git a/question/bank/columnsortorder/amd/src/actions.js b/question/bank/columnsortorder/amd/src/actions.js new file mode 100644 index 00000000000..197f3b36e5b --- /dev/null +++ b/question/bank/columnsortorder/amd/src/actions.js @@ -0,0 +1,134 @@ +// 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 . + +/** + * Common javascript for handling actions on the admin page and the user's view of the question bank. + * + * @module qbank_columnsortorder/actions + * @copyright 2023 onwards Catalyst IT Europe Ltd + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import SortableList from 'core/sortable_list'; +import $ from 'jquery'; +import * as repository from 'qbank_columnsortorder/repository'; +import Notification from "core/notification"; +import Fragment from "core/fragment"; +import Templates from "core/templates"; + +export const SELECTORS = { + columnList: '.qbank-column-list', + sortableColumn: '.qbank-sortable-column', + removeLink: '[data-action=remove]', + moveHandler: '[data-drag-type=move]', + addColumn: '.addcolumn', + addLink: '[data-action=add]', + actionLink: '.action-link', +}; + +/** + * Sets up sortable list in the column sort order page. + * + * @param {Element} listRoot Element containing the sortable list. + * @param {Boolean} vertical Is the list in vertical orientation, rather than horizonal? + * @param {Boolean} global Should changes be saved to global config, rather than user preferences? + * @return {jQuery} sortable column elements, for attaching additional event listeners. + */ +export const setupSortableLists = (listRoot, vertical = false, global = false) => { + const sortableList = new SortableList(listRoot, { + moveHandlerSelector: SELECTORS.moveHandler, + isHorizontal: !vertical, + }); + sortableList.getElementName = element => Promise.resolve(element.data('name')); + + const sortableColumns = $(SELECTORS.sortableColumn); + + sortableColumns.on(SortableList.EVENTS.DROP, () => { + repository.setColumnbankOrder(getColumnOrder(listRoot), global).catch(Notification.exception); + listRoot.querySelectorAll(SELECTORS.sortableColumn).forEach(item => item.classList.remove('active')); + }); + + sortableColumns.on(SortableList.EVENTS.DRAGSTART, (event) => { + event.currentTarget.classList.add('active'); + }); + + return sortableColumns; +}; + +/** + * Set up event handlers for action buttons. + * + * For each action, call the web service to update the appropriate setting or user preference, then call the fragment to + * refresh the view. + * + * @param {Element} uiRoot The root of the question bank UI. + * @param {Boolean} global Should changes be saved to global config, rather than user preferences? + */ +export const setupActionButtons = (uiRoot, global = false) => { + uiRoot.addEventListener('click', async(e) => { + const actionLink = e.target.closest(SELECTORS.actionLink); + if (!actionLink) { + return; + } + try { + e.preventDefault(); + const action = actionLink.dataset.action; + if (action === 'add' || action === 'remove') { + const hiddenColumns = []; + const addColumnList = document.querySelector(SELECTORS.addColumn); + if (addColumnList) { + addColumnList.querySelectorAll(SELECTORS.addLink).forEach(item => { + if (action === 'add' && item === actionLink) { + return; + } + hiddenColumns.push(item.dataset.column); + }); + } + if (action === 'remove') { + hiddenColumns.push(actionLink.dataset.column); + } + await repository.setHiddenColumns(hiddenColumns, global); + } else if (action === 'reset') { + await Promise.all([ + repository.setColumnbankOrder([], global), + repository.setHiddenColumns([], global), + repository.setColumnSize('', global), + ]); + } + const fragmentData = uiRoot.dataset; + // We have to use then() there, as loadFragment doesn't appear to work with await. + Fragment.loadFragment(fragmentData.component, fragmentData.callback, fragmentData.contextid) + .then((html, js) => { + return Templates.replaceNode(uiRoot, html, js); + }) + .catch(Notification.exception); + } catch (ex) { + await Notification.exception(ex); + } + }); +}; + +/** + * Gets the newly reordered columns to display in the question bank view. + * @param {Element} listRoot + * @returns {Array} + */ +export const getColumnOrder = listRoot => { + const columns = Array.from(listRoot.querySelectorAll('[data-columnid]')) + .map(column => column.dataset.columnid); + + return columns.filter((value, index) => columns.indexOf(value) === index); +}; diff --git a/question/bank/columnsortorder/amd/src/admin_actions.js b/question/bank/columnsortorder/amd/src/admin_actions.js new file mode 100644 index 00000000000..e1c9215483f --- /dev/null +++ b/question/bank/columnsortorder/amd/src/admin_actions.js @@ -0,0 +1,70 @@ +// 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 . + +/** + * Javascript for handling actions on the admin page + * + * @module qbank_columnsortorder/admin_actions + * @copyright 2023 onwards Catalyst IT Europe Ltd + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as actions from 'qbank_columnsortorder/actions'; +import * as repository from 'qbank_columnsortorder/repository'; +import Notification from "core/notification"; +import Pending from 'core/pending'; + +/** + * Event handler to save the custom column widths when a field is edited. + * + * @param {Element} listRoot The root element of the list of columns. + */ +const setupSaveWidths = listRoot => { + listRoot.addEventListener('change', async() => { + const pendingPromise = new Pending('saveWidths'); + const columns = listRoot.querySelectorAll(actions.SELECTORS.sortableColumn); + const widths = []; + columns.forEach(column => { + const widthInput = column.querySelector('.width-input'); + const valid = widthInput.checkValidity(); + widthInput.closest('.has-validation').classList.add('was-validated'); + if (!valid) { + return; + } + widths.push({ + column: column.dataset.columnid, + width: widthInput.value, + }); + }); + await repository.setColumnSize(JSON.stringify(widths), true).catch(Notification.exception); + pendingPromise.resolve(); + }); +}; + +/** + * Initialize module + * + * Set up event handlers for the action buttons, width fields and initialise column sorting. + * + * @param {String} id ID for the admin UI root element. + */ +export const init = id => { + const uiRoot = document.getElementById(id); + const listRoot = uiRoot.querySelector(actions.SELECTORS.columnList); + actions.setupSortableLists(listRoot, true, true); + actions.setupActionButtons(uiRoot, true); + setupSaveWidths(listRoot); +}; diff --git a/question/bank/columnsortorder/amd/src/repository.js b/question/bank/columnsortorder/amd/src/repository.js new file mode 100644 index 00000000000..501075451f0 --- /dev/null +++ b/question/bank/columnsortorder/amd/src/repository.js @@ -0,0 +1,68 @@ +// 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 . + +/** + * External function calls for qbank_columnsortorder + * + * @module qbank_columnsortorder/repository + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {call as fetchMany} from 'core/ajax'; + +/** + * Save the list of hidden columns + * + * @param {String[]} columns List of hidden column names + * @param {Boolean} global Set global config setting, rather than user preference + * @return {Promise} + */ +export const setHiddenColumns = (columns, global = false) => { + const responses = fetchMany([{ + methodname: 'qbank_columnsortorder_set_hidden_columns', + args: {columns: columns, global: global}, + }]); + return responses[0]; +}; + +/** + * Save the order of columns + * + * @param {String[]} columns List of column names in the desired order + * @param {Boolean} global Set global config setting, rather than user preference + * @return {Promise} + */ +export const setColumnbankOrder = (columns, global = false) => { + return fetchMany([{ + methodname: 'qbank_columnsortorder_set_columnbank_order', + args: {columns: columns, global: global}, + }])[0]; +}; + +/** + * Save the column widths + * + * @param {String} sizes JSON string encoding an array of objects with "column" and "width" properties. + * @param {Boolean} global Set global config setting, rather than user preference + * @return {Promise} + */ +export const setColumnSize = (sizes, global = false) => { + return fetchMany([{ + methodname: 'qbank_columnsortorder_set_column_size', + args: {sizes: sizes, global: global}, + }])[0]; +}; diff --git a/question/bank/columnsortorder/amd/src/user_actions.js b/question/bank/columnsortorder/amd/src/user_actions.js new file mode 100644 index 00000000000..3c8597f4c1d --- /dev/null +++ b/question/bank/columnsortorder/amd/src/user_actions.js @@ -0,0 +1,367 @@ +// 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 . + +/** + * Javascript for customising the user's view of the question bank + * + * @module qbank_columnsortorder/user_actions + * @copyright 2021 Catalyst IT Australia Pty Ltd + * @author Ghaly Marc-Alexandre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import * as actions from 'qbank_columnsortorder/actions'; +import * as repository from 'qbank_columnsortorder/repository'; +import {get_string as getString} from 'core/str'; +import ModalEvents from 'core/modal_events'; +import ModalFactory from 'core/modal_factory'; +import Notification from "core/notification"; +import SortableList from 'core/sortable_list'; +import Templates from "core/templates"; + + +const SELECTORS = { + uiRoot: '.questionbankwindow', + moveAction: '.menu-action[data-action=move]', + resizeAction: '.menu-action[data-action=resize]', + resizeHandle: '.qbank_columnsortorder-action-handle.resize', + handleContainer: '.handle-container', + headerContainer: '.header-container', + tableColumn: identifier => `td[data-columnid="${identifier.replace(/["\\]/g, '\\$&')}"]`, +}; + +/** To track mouse event on a table header */ +let currentHeader; + +/** Current mouse x postion, to track mouse event on a table header */ +let currentX; + +/** + * Flag to temporarily prevent move and resize handles from being shown or hidden. + * + * @type {boolean} + */ +let suspendShowHideHandles = false; + +/** + * Add handle containers for move and resize handles. + * + * @param {Element} uiRoot The root element of the quesiton bank UI. + * @return {Promise} Resolved after the containers have been added to each column header. + */ +const addHandleContainers = uiRoot => { + return new Promise((resolve) => { + const headerContainers = uiRoot.querySelectorAll(SELECTORS.headerContainer); + Templates.renderForPromise('qbank_columnsortorder/handle_container', {}) + .then(({html, js}) => { + headerContainers.forEach(container => { + Templates.prependNodeContents(container, html, js); + }); + resolve(); + return headerContainers; + }).catch(Notification.exception); + }); +}; + +/** + * Render move handles in each container. + * + * This takes a list of the move actions rendered in each column header, and creates a corresponding drag handle for each. + * + * @param {NodeList} moveActions Menu actions for moving columns. + */ +const setUpMoveHandles = moveActions => { + moveActions.forEach(moveAction => { + const header = moveAction.closest('th'); + header.classList.add('qbank-sortable-column'); + const handleContainer = header.querySelector(SELECTORS.handleContainer); + const context = { + action: "move", + dragtype: "move", + target: '', + title: moveAction.title, + pixicon: "i/dragdrop", + pixcomponent: "core", + popup: true + }; + return Templates.renderForPromise('qbank_columnsortorder/action_handle', context) + .then(({html, js}) => { + Templates.prependNodeContents(handleContainer, html, js); + return handleContainer; + }).catch(Notification.exception); + }); +}; + +/** + * Serialise the current column sizes. + * + * This finds the current width set in each column header's style property, and returns them encoded as a JSON string. + * + * @param {Element} uiRoot The root element of the quesiton bank UI. + * @return {String} JSON array containing a list of objects with column and width properties. + */ +const serialiseColumnSizes = (uiRoot) => { + const columnSizes = []; + const tableHeaders = uiRoot.querySelectorAll('th'); + tableHeaders.forEach(header => { + // Only get the width set via style attribute (set by move action). + const width = parseInt(header.style.width); + if (!width || isNaN(width)) { + return; + } + columnSizes.push({ + column: header.dataset.columnid, + width: width + }); + }); + return JSON.stringify(columnSizes); +}; + +/** + * Render resize handles in each container. + * + * This takes a list of the resize actions rendered in each column header, and creates a corresponding drag handle for each. + * It also initialises the event handlers for the drag handles and resize modal. + * + * @param {Element} uiRoot Question bank UI root element. + */ +const setUpResizeHandles = (uiRoot) => { + const resizeActions = uiRoot.querySelectorAll(SELECTORS.resizeAction); + resizeActions.forEach(resizeAction => { + const headerContainer = resizeAction.closest(SELECTORS.headerContainer); + const handleContainer = headerContainer.querySelector(SELECTORS.handleContainer); + const context = { + action: "resize", + target: '', + title: resizeAction.title, + pixicon: 'i/twoway', + pixcomponent: 'core', + popup: true + }; + return Templates.renderForPromise('qbank_columnsortorder/action_handle', context) + .then(({html, js}) => { + Templates.appendNodeContents(handleContainer, html, js); + return handleContainer; + }).catch(Notification.exception); + }); + + let moveTracker = false; + let currentResizeHandle = null; + // Start mouse event on headers. + uiRoot.addEventListener('mousedown', e => { + currentResizeHandle = e.target.closest(SELECTORS.resizeHandle); + // Return if it is not ' resize' button. + if (!currentResizeHandle) { + return; + } + // Save current position. + currentX = e.pageX; + // Find the header. + currentHeader = e.target.closest(actions.SELECTORS.sortableColumn); + moveTracker = false; + suspendShowHideHandles = true; + }); + + // Resize column as the mouse move. + document.addEventListener('mousemove', e => { + if (!currentHeader || !currentResizeHandle || currentX === 0) { + return; + } + + // Prevent text selection as the handle is dragged. + document.getSelection().removeAllRanges(); + + // Adjust the column width according the amount the handle was dragged. + const offset = e.pageX - currentX; + currentX = e.pageX; + const newWidth = currentHeader.offsetWidth + offset; + currentHeader.style.width = newWidth + 'px'; + moveTracker = true; + }); + + // Set new size when mouse is up. + document.addEventListener('mouseup', () => { + if (!currentHeader || !currentResizeHandle || currentX === 0) { + return; + } + if (moveTracker) { + // If the mouse moved, we are changing the size by drag, so save the change. + repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception); + } else { + // If the mouse didn't move, display a modal to change the size using a form. + showResizeModal(currentHeader, uiRoot); + } + currentHeader = null; + currentResizeHandle = null; + currentX = 0; + moveTracker = false; + suspendShowHideHandles = false; + }); +}; + +/** + * Event handler for resize actions in each column header. + * + * This will listen for a click on any resize action, and activate the corresponding resize modal. + * + * @param {Element} uiRoot Question bank UI root element. + */ +const setUpResizeActions = uiRoot => { + uiRoot.addEventListener('click', (e) => { + const resizeAction = e.target.closest(SELECTORS.resizeAction); + if (resizeAction) { + e.preventDefault(); + const currentHeader = resizeAction.closest('th'); + showResizeModal(currentHeader, uiRoot); + } + }); +}; + +/** + * Show a modal containing a number input for changing a column width without click-and-drag. + * + * @param {Element} currentHeader The header element that is being resized. + * @param {Element} uiRoot The question bank UI root element. + * @returns {Promise} + */ +const showResizeModal = async(currentHeader, uiRoot) => { + + const initialWidth = currentHeader.offsetWidth; + + const modal = await ModalFactory.create({ + title: getString('resizecolumn', 'qbank_columnsortorder', currentHeader.textContent), + type: ModalFactory.types.SAVE_CANCEL, + body: Templates.render('qbank_columnsortorder/resize_modal', {}) + }); + const root = modal.getRoot(); + root.on(ModalEvents.cancel, () => { + currentHeader.style.width = initialWidth + 'px'; + }); + root.on(ModalEvents.save, () => { + repository.setColumnSize(serialiseColumnSizes(uiRoot)).catch(Notification.exception); + }); + modal.show(); + + const body = await modal.bodyPromise; + const input = body.get(0).querySelector('input'); + input.value = initialWidth; + + input.addEventListener('change', e => { + const newWidth = e.target.value; + currentHeader.style.width = newWidth + 'px'; + }); +}; + +/** + * Event handler for move actions in each column header. + * + * This will listen for a click on any move action, pass the click to the corresponding move handle, causing its modal to be shown. + * + * @param {Element} uiRoot Question bank UI root element. + */ +const setUpMoveActions = uiRoot => { + uiRoot.addEventListener('click', e => { + const moveAction = e.target.closest(SELECTORS.moveAction); + if (moveAction) { + e.preventDefault(); + const sortableColumn = moveAction.closest(actions.SELECTORS.sortableColumn); + const moveHandle = sortableColumn.querySelector(actions.SELECTORS.moveHandler); + moveHandle.click(); + } + }); +}; + +/** + * Event handler for showing and hiding handles when the mouse is over a column header. + * + * Implementing this behaviour using the :hover CSS pseudoclass is not sufficient, as the mouse may move over the neighbouring + * header while dragging, leading to some odd behaviour. This allows us to suspend the show/hide behaviour while a handle is being + * dragged, and so keep the active handle visible until the drag is finished. + * + * @param {Element} uiRoot Question bank UI root element. + */ +const setupShowHideHandles = uiRoot => { + let shownHeader = null; + let tableHead = uiRoot.querySelector('thead'); + uiRoot.addEventListener('mouseover', e => { + if (suspendShowHideHandles) { + return; + } + const header = e.target.closest(actions.SELECTORS.sortableColumn); + if (!header && !shownHeader) { + return; + } + if (!header || header !== shownHeader) { + tableHead.querySelector('.show-handles')?.classList.remove('show-handles'); + shownHeader = header; + if (header) { + header.classList.add('show-handles'); + } + } + }); +}; + +/** + * Event handler for sortable list DROP event. + * + * Find all table cells corresponding to the column of the dropped header, and move them to the new position. + * + * @param {Event} event + */ +const reorderColumns = event => { + // Current header. + const header = event.target; + // Find the previous sibling of the header, which will be used when moving columns. + const insertAfter = header.previousElementSibling; + // Move columns. + const uiRoot = document.querySelector(SELECTORS.uiRoot); + const columns = uiRoot.querySelectorAll(SELECTORS.tableColumn(header.dataset.columnid)); + columns.forEach(column => { + const row = column.parentElement; + if (insertAfter) { + // Find the column to insert after. + const insertAfterColumn = row.querySelector(SELECTORS.tableColumn(insertAfter.dataset.columnid)); + // Insert the column. + insertAfterColumn.after(column); + } else { + // Insert as the first child (first column in the table). + row.insertBefore(column, row.firstChild); + } + }); +}; + +/** + * Initialize module + * + * Add containers for the drag handles to each column header, then render handles, enable show/hide behaviour, set up drag/drop + * column sorting, then enable the move and resize modals to be triggered from menu actions. + */ +export const init = async() => { + const uiRoot = document.querySelector('.questionbankwindow'); + await addHandleContainers(uiRoot); + setUpMoveHandles(uiRoot.querySelectorAll(SELECTORS.moveAction)); + setUpResizeHandles(uiRoot); + setupShowHideHandles(uiRoot); + const sortableColumns = actions.setupSortableLists(uiRoot.querySelector(actions.SELECTORS.columnList)); + sortableColumns.on(SortableList.EVENTS.DROP, reorderColumns); + sortableColumns.on(SortableList.EVENTS.DRAGSTART, () => { + suspendShowHideHandles = true; + }); + sortableColumns.on(SortableList.EVENTS.DRAGEND, () => { + suspendShowHideHandles = false; + }); + setUpMoveActions(uiRoot); + setUpResizeActions(uiRoot); +}; diff --git a/question/bank/columnsortorder/classes/column_manager.php b/question/bank/columnsortorder/classes/column_manager.php index f8f9289e851..27c3f522375 100644 --- a/question/bank/columnsortorder/classes/column_manager.php +++ b/question/bank/columnsortorder/classes/column_manager.php @@ -67,9 +67,22 @@ class column_manager extends column_manager_base { */ public function __construct(bool $globalsettings = false) { $this->columnorder = $this->setup_property('enabledcol', $globalsettings); + if (empty($this->columnorder)) { + $this->columnorder = [ + 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column', + 'qbank_viewquestiontype\question_type_column' . column_base::ID_SEPARATOR . 'question_type_column', + 'qbank_viewquestionname\question_name_idnumber_tags_column' . column_base::ID_SEPARATOR . + 'question_name_idnumber_tags_column', + 'core_question\local\bank\edit_menu_column' . column_base::ID_SEPARATOR . 'edit_menu_column', + 'qbank_editquestion\question_status_column' . column_base::ID_SEPARATOR . 'question_status_column', + 'qbank_history\version_number_column' . column_base::ID_SEPARATOR . 'version_number_column', + 'qbank_viewcreator\creator_name_column' . column_base::ID_SEPARATOR . 'creator_name_column', + 'qbank_comment\comment_count_column' . column_base::ID_SEPARATOR . 'comment_count_column', + ]; + } $this->hiddencolumns = $this->setup_property('hiddencols', $globalsettings); $this->colsize = $this->setup_property('colsize', $globalsettings, 'json'); - $this->disabledcolumns = $this->setup_property('disabledcol', $globalsettings); + $this->disabledcolumns = $this->setup_property('disabledcol', true); // No user preference for disabledcol. if ($this->columnorder) { $this->columnorder = array_flip($this->columnorder); @@ -157,9 +170,16 @@ class column_manager extends column_manager_base { $course = (object) ['id' => 0]; $context = context_system::instance(); $contexts = new question_edit_contexts($context); + $category = question_make_default_categories($contexts->all()); + $params = ['cat' => $category->id . ',' . $context->id]; // Dummy call to get the objects without error. - $questionbank = new preview_view($contexts, new moodle_url('/question/bank/columnsortorder/sortcolumns.php'), $course, - null); + $questionbank = new preview_view( + $contexts, + new moodle_url('/question/bank/columnsortorder/sortcolumns.php'), + $course, + null, + $params + ); return $questionbank; } @@ -174,12 +194,11 @@ class column_manager extends column_manager_base { if ($column->get_name() === 'checkbox') { continue; } - $classelements = explode('\\', $key); $columns[] = (object) [ 'class' => get_class($column), 'name' => $column->get_title(), - 'colname' => end($classelements), - 'id' => implode(self::ID_SEPARATOR, [$column::class, $column->get_column_name()]), + 'colname' => $column->get_column_name(), + 'id' => $column->get_column_id(), ]; } return $columns; @@ -194,9 +213,9 @@ class column_manager extends column_manager_base { $disabled = []; if ($this->disabledcolumns) { foreach (array_keys($this->disabledcolumns) as $column) { - [$classname, $columnname] = explode(self::ID_SEPARATOR, $column); + [$classname, $columnname] = explode(column_base::ID_SEPARATOR, $column, 2); $columnobject = $classname::from_column_name($this->get_questionbank(), $columnname); - $disabled[] = (object) [ + $disabled[$column] = (object) [ 'disabledname' => $columnobject->get_title(), ]; } @@ -278,7 +297,7 @@ class column_manager extends column_manager_base { /** * Orders columns in the question bank view according to config_plugins table 'qbank_columnsortorder' config. * - * @param array $ordertosort Unordered array of columns + * @param array $ordertosort Unordered array of columns, [columnname => class] * @return array $properorder|$ordertosort Returns array ordered if 'qbank_columnsortorder' config exists. */ public function get_sorted_columns($ordertosort): array { @@ -295,10 +314,13 @@ class column_manager extends column_manager_base { } $properorder = array_merge($columnorder, $ordertosort); // Always have the checkbox at first column position. - if (isset($properorder['checkbox_column'])) { - $checkboxfirstelement = $properorder['checkbox_column']; - unset($properorder['checkbox_column']); - $properorder = array_merge(['checkbox_column' => $checkboxfirstelement], $properorder); + $checkboxid = 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column'; + if (isset($properorder[$checkboxid])) { + $checkboxfirstelement = $properorder[$checkboxid]; + unset($properorder[$checkboxid]); + $properorder = array_merge([ + $checkboxid => $checkboxfirstelement + ], $properorder); } return $properorder; } @@ -306,7 +328,7 @@ class column_manager extends column_manager_base { } /** - * Given an array of columns, set the isvisible attribute according to $this->hiddencolumns. + * Given an array of columns, set the isvisible attribute according to $this->hiddencolumns and $this->disabledcolumns. * * @param column_base[] $columns * @return array @@ -316,7 +338,9 @@ class column_manager extends column_manager_base { if (!is_object($column)) { continue; } - $column->isvisible = !in_array(get_class($column), $this->hiddencolumns); + $columnid = $column->get_column_id(); + + $column->isvisible = !in_array($columnid, $this->hiddencolumns) && !array_key_exists($columnid, $this->disabledcolumns); } return $columns; } @@ -341,16 +365,17 @@ class column_manager extends column_manager_base { */ public function get_hidden_columns(): array { return array_reduce($this->hiddencolumns, function($result, $hiddencolumn) { - $result[$hiddencolumn] = (new $hiddencolumn($this->get_questionbank()))->get_title(); + [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $hiddencolumn, 2); + $result[$hiddencolumn] = $columnclass::from_column_name($this->get_questionbank(), $columnname)->get_title(); return $result; }, []); } public function get_column_width(column_base $column): string { $colsizemap = $this->get_colsize_map(); - $columnclass = get_class($column); - if (array_key_exists($columnclass, $colsizemap)) { - return $colsizemap[$columnclass] . 'px'; + $columnid = $column->get_column_id(); + if (array_key_exists($columnid, $colsizemap)) { + return $colsizemap[$columnid] . 'px'; } return parent::get_column_width($column); } diff --git a/question/bank/columnsortorder/classes/external/set_column_size.php b/question/bank/columnsortorder/classes/external/set_column_size.php new file mode 100644 index 00000000000..9ed40234a6b --- /dev/null +++ b/question/bank/columnsortorder/classes/external/set_column_size.php @@ -0,0 +1,79 @@ +. + +namespace qbank_columnsortorder\external; + +use context_system; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_value; +use qbank_columnsortorder\column_manager; + +/** + * External function for saving column sizes. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_column_size extends external_api { + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'sizes' => new external_value(PARAM_TEXT, 'Size for each column', VALUE_REQUIRED), + 'global' => new external_value(PARAM_BOOL, 'Set global config setting, rather than user preference', + VALUE_DEFAULT, false), + ]); + } + + /** + * Returns description of method result value. + */ + public static function execute_returns(): void { + } + + /** + * Set sizes for columns + * Save against user preference if component is specified + * + * @param string $sizes json string representing [column => size]. + * @param bool $global Set global config setting, rather than user preference + */ + public static function execute(string $sizes, bool $global = false): void { + [ + 'sizes' => $sizes, + 'global' => $global, + ] + = self::validate_parameters(self::execute_parameters(), + [ + 'sizes' => $sizes, + 'global' => $global, + ]); + + $context = context_system::instance(); + self::validate_context($context); + if ($global) { + require_capability('moodle/site:config', $context); + } + + column_manager::set_column_size($sizes, $global); + } +} diff --git a/question/bank/columnsortorder/classes/external/set_hidden_columns.php b/question/bank/columnsortorder/classes/external/set_hidden_columns.php new file mode 100644 index 00000000000..e6a29378856 --- /dev/null +++ b/question/bank/columnsortorder/classes/external/set_hidden_columns.php @@ -0,0 +1,83 @@ +. + +namespace qbank_columnsortorder\external; + +use context_system; +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_multiple_structure; +use core_external\external_value; +use qbank_columnsortorder\column_manager; + +/** + * External function for saving the list of hidden columns. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_hidden_columns extends external_api { + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'columns' => new external_multiple_structure( + new external_value(PARAM_TEXT, 'Plugin name for the hidden column', VALUE_REQUIRED) + ), + 'global' => new external_value(PARAM_BOOL, 'Set global config setting, rather than user preference', + VALUE_DEFAULT, false), + ]); + } + + /** + * Returns description of method result value. + * + */ + public static function execute_returns(): void { + } + + /** + * Set hidden columns + * Save against user preference if specified + * + * @param array $columns list of hidden columns. + * @param bool $global Set global config setting, rather than user preference + */ + public static function execute(array $columns, bool $global = false): void { + [ + 'columns' => $columns, + 'global' => $global, + ] + = self::validate_parameters(self::execute_parameters(), + [ + 'columns' => $columns, + 'global' => $global, + ]); + + $context = context_system::instance(); + self::validate_context($context); + if ($global) { + require_capability('moodle/site:config', $context); + } + + column_manager::set_hidden_columns($columns, $global); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/column_action_move.php b/question/bank/columnsortorder/classes/local/bank/column_action_move.php new file mode 100644 index 00000000000..33c3519741f --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/column_action_move.php @@ -0,0 +1,53 @@ +. + +namespace qbank_columnsortorder\local\bank; + +use core_question\local\bank\column_action_base; +use core_question\local\bank\column_base; + +/** + * Move a column + * + * This will add an action menu item which will be enhanced by javascript in user_actions.js to show the move column modal for the + * current column when clicked. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_action_move extends column_action_base { + + /** @var string Label for the Move action. */ + protected string $move; + + protected function init(): void { + $this->move = get_string('move'); + } + + public function get_action_menu_link(column_base $column): ?\action_menu_link { + return new \action_menu_link_secondary( + new \moodle_url('/question/edit.php'), + null, + $this->move, + [ + 'title' => get_string('movecolumn', 'qbank_columnsortorder', $column->get_title()), + 'data-action' => 'move', + 'data-column' => get_class($column), + ]); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/column_action_remove.php b/question/bank/columnsortorder/classes/local/bank/column_action_remove.php new file mode 100644 index 00000000000..97a92e320f3 --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/column_action_remove.php @@ -0,0 +1,78 @@ +. + +namespace qbank_columnsortorder\local\bank; + +use core_question\local\bank\column_action_base; +use core_question\local\bank\column_base; + +/** + * Remove a column + * + * This action will display a link that will set the current column as hidden, then redirect back the current page. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_action_remove extends column_action_base { + + /** @var bool True if we are changing global config, false for user preferences. */ + protected bool $global; + + /** @var string Label for the Remove action. */ + protected string $remove; + + protected function init(): void { + $this->global = false; + $this->remove = get_string('remove'); + } + + /** + * Set the $global property to indicate whether we are changing global config. + * + * This action is used on both the user and admin screens, so requires this additional method. + * + * @param bool $global + * @return void + */ + public function set_global(bool $global): void { + $this->global = $global; + } + + public function get_action_menu_link(column_base $column): ?\action_menu_link { + $actionurl = new \moodle_url('/question/bank/columnsortorder/actions.php', [ + 'column' => $column->get_column_id(), + 'action' => 'remove', + 'sesskey' => sesskey(), + 'returnurl' => $this->qbank->returnurl, + ]); + if ($this->global) { + $actionurl->param('global', $this->global); + } + return new \action_menu_link_secondary( + $actionurl, + null, + $this->remove, + [ + 'class' => 'action-link', + 'title' => get_string('removecolumn', 'qbank_columnsortorder', $column->get_title()), + 'data-action' => 'remove', + 'data-column' => $column->get_column_id(), + ]); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/column_action_resize.php b/question/bank/columnsortorder/classes/local/bank/column_action_resize.php new file mode 100644 index 00000000000..ad27307e6be --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/column_action_resize.php @@ -0,0 +1,53 @@ +. + +namespace qbank_columnsortorder\local\bank; + +use core_question\local\bank\column_action_base; +use core_question\local\bank\column_base; + +/** + * Resize a column + * + * This will add an action menu item which will be enhanced by javascript in user_actions.js to show the resize column modal for the + * current column when clicked. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_action_resize extends column_action_base { + + /** @var string Label for the resize action. */ + protected string $resize; + + protected function init(): void { + $this->resize = get_string('resize', 'qbank_columnsortorder'); + } + + public function get_action_menu_link(column_base $column): ?\action_menu_link { + return new \action_menu_link_secondary( + new \moodle_url('/question/edit.php'), + null, + $this->resize, + [ + 'title' => get_string('resizecolumn', 'qbank_columnsortorder', $column->get_title()), + 'data-action' => 'resize', + 'data-column' => get_class($column), + ]); + } +} diff --git a/question/bank/columnsortorder/classes/local/bank/preview_view.php b/question/bank/columnsortorder/classes/local/bank/preview_view.php new file mode 100644 index 00000000000..377a3ac571b --- /dev/null +++ b/question/bank/columnsortorder/classes/local/bank/preview_view.php @@ -0,0 +1,100 @@ +. + +namespace qbank_columnsortorder\local\bank; + +use core_question\local\bank\view; +use qbank_columnsortorder\column_manager; + +/** + * Custom view for displaying a preview of the question bank + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class preview_view extends view { + + /** + * Use global settings for the column manager. + * + * @return void + */ + protected function init_column_manager(): void { + $this->columnmanager = new column_manager(true); + } + + /** + * Prints the table row with the preview data for each column. + * + * @param \stdClass $question + * @param int $rowcount + */ + public function print_table_row($question, $rowcount): void { + $rowclasses = implode(' ', $this->get_row_classes($question, $rowcount)); + $attributes = []; + if ($rowclasses) { + $attributes['class'] = $rowclasses; + } + echo \html_writer::start_tag('tr', $attributes); + foreach ($this->visiblecolumns as $column) { + $column->display_preview($question, $rowclasses); + } + echo \html_writer::end_tag('tr'); + foreach ($this->extrarows as $row) { + $row->display_preview($question, $rowclasses); + } + } + + /** + * Get a dummy question containing valid data for the default question fields. + * + * @return \stdClass + */ + protected function get_dummy_question(): \stdClass { + return (object)[ + 'id' => 1, + 'qtype' => 'truefalse', + 'createdby' => 2, + 'categoryid' => 1, + 'contextid' => 1, + 'status' => 'ready', + 'version' => 1, + 'versionid' => 1, + 'questionbankentryid' => 1, + 'name' => 'Lorem ipsum', + 'idnumber' => 123, + 'creatorfirstname' => 'Admin', + 'creatorlastname' => 'User', + 'timecreated' => 1691157311, + 'modifierfirstname' => 'Admin', + 'modifierlastname' => 'User', + 'timemodified' => 1691157311, + ]; + } + + /** + * Generate a preview of the question bank table with a single dummy question. + * + * @return string An HTML table containing the column headings and a single question row. + */ + public function get_preview(): string { + ob_start(); + $this->display_questions([$this->get_dummy_question()]); + return ob_get_clean(); + } +} diff --git a/question/bank/columnsortorder/classes/output/add_column.php b/question/bank/columnsortorder/classes/output/add_column.php new file mode 100644 index 00000000000..8dd02182c63 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/add_column.php @@ -0,0 +1,78 @@ +. + +namespace qbank_columnsortorder\output; + +use core_reportbuilder\local\models\column; +use qbank_columnsortorder\column_manager; +use moodle_url; +use renderer_base; + +/** + * Renderable for the "add column" dropdown list + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class add_column implements \renderable, \templatable { + + /** @var column_manager The column manager for getting the list of hidden columns. */ + protected column_manager $columnmanager; + + /** @var moodle_url The current page URL to redirect back to. */ + protected moodle_url $returnurl; + + /** @var bool True if we are changing global config, false for user preferences. */ + protected bool $global; + + /** + * Store arguments for generating template context. + * + * @param column_manager $columnmanager + * @param moodle_url $returnurl + * @param bool $global + */ + public function __construct(column_manager $columnmanager, moodle_url $returnurl, bool $global = false) { + $this->columnmanager = $columnmanager; + $this->returnurl = $returnurl; + $this->global = $global; + } + + public function export_for_template(renderer_base $output): array { + $hiddencolumns = []; + foreach ($this->columnmanager->get_hidden_columns() as $class => $name) { + $addurl = new moodle_url('/question/bank/columnsortorder/actions.php', [ + 'action' => 'add', + 'global' => $this->global, + 'column' => $class, + 'sesskey' => sesskey(), + 'returnurl' => $this->returnurl, + ]); + $hiddencolumns[] = [ + 'name' => $name, + 'addurl' => $addurl->out(false), + 'column' => $class, + 'addtext' => get_string('addcolumn', 'qbank_columnsortorder', $name), + ]; + } + return [ + 'hashiddencolumns' => !empty($hiddencolumns), + 'hiddencolumns' => $hiddencolumns, + ]; + } +} diff --git a/question/bank/columnsortorder/classes/output/column_sort_preview.php b/question/bank/columnsortorder/classes/output/column_sort_preview.php new file mode 100644 index 00000000000..dcc083e0c2d --- /dev/null +++ b/question/bank/columnsortorder/classes/output/column_sort_preview.php @@ -0,0 +1,55 @@ +. + +namespace qbank_columnsortorder\output; + +use moodle_url; +use templatable; +use renderable; +use qbank_columnsortorder\column_manager; + +/** + * Renderable for the question bank preview. + * + * This takes the HTML for a question bank preview, and displays in a page with a link to return to the admin screen. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_sort_preview implements templatable, renderable { + + /** @var string Rendered preview HTML. */ + protected string $preview; + + /** + * Store rendered preview for template context. + * + * @param string $preview + */ + public function __construct(string $preview) { + $this->preview = $preview; + } + + public function export_for_template(\renderer_base $output): array { + $context = [ + 'backurl' => new moodle_url('/question/bank/columnsortorder/sortcolumns.php'), + 'preview' => $this->preview, + ]; + return $context; + } +} diff --git a/question/bank/columnsortorder/classes/output/column_sort_ui.php b/question/bank/columnsortorder/classes/output/column_sort_ui.php new file mode 100644 index 00000000000..9e911160494 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/column_sort_ui.php @@ -0,0 +1,95 @@ +. + +namespace qbank_columnsortorder\output; + +use core_question\local\bank\column_base; +use qbank_columnsortorder\local\bank\column_action_remove; +use moodle_url; +use qbank_columnsortorder\column_manager; +use renderable; +use templatable; + +/** + * Renderable for the column sort admin UI. + * + * Displays a list of the currently enabled columns and allows them to be sorted, hidden, and resized. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class column_sort_ui implements templatable, renderable { + + /** + * @var int The minimum custom width for a column. + */ + const MIN_COLUMN_WIDTH = 10; + + public function export_for_template(\renderer_base $output): array { + $columnmanager = new column_manager(true); + $enabledcolumns = $columnmanager->get_columns(); + $disabledcolumns = $columnmanager->get_disabled_columns(); + $columnsizes = $columnmanager->get_colsize_map(); + $qbank = $columnmanager->get_questionbank(); + $returnurl = new moodle_url('/question/bank/columnsortorder/sortcolumns.php'); + $params = []; + $params['formaction'] = new moodle_url('/question/bank/columnsortorder/actions.php'); + $params['sesskey'] = sesskey(); + $params['disabled'] = $disabledcolumns; + $params['contextid'] = \context_system::instance()->id; + $params['minwidth'] = self::MIN_COLUMN_WIDTH; + foreach ($enabledcolumns as $column) { + if (in_array($column->id, $columnmanager->hiddencolumns) || array_key_exists($column->id, $disabledcolumns)) { + continue; + } + $name = $column->name; + $colname = get_string('qbankcolumnname', 'qbank_columnsortorder', $column->colname); + + $removeaction = new column_action_remove($qbank); + $removeaction->set_global(true); + $actionmenu = new \action_menu([ + $removeaction->get_action_menu_link($column->class::from_column_name($qbank, $column->colname)), + ]); + $params['names'][] = [ + 'name' => $name, + 'colname' => $colname, + 'class' => $column->class, + 'width' => $columnsizes[$column->id] ?? null, + 'widthlabel' => get_string('width', 'qbank_columnsortorder', $name), + 'actionmenu' => $actionmenu->export_for_template($output), + 'columnid' => $column->id, + 'escapedid' => str_replace('\\', '__', $column->id), + ]; + } + + $params['disabled'] = array_values($disabledcolumns); + $params['columnsdisabled'] = !empty($params['disabled']); + $addcolumn = new add_column($columnmanager, $returnurl); + $params['addcolumn'] = $addcolumn->export_for_template($output); + $resetcolums = new reset_columns($returnurl); + $params['resetcolumns'] = $resetcolums->export_for_template($output); + $params['extraclasses'] = 'pr-1'; + $urltoredirect = new moodle_url('/admin/settings.php', ['section' => 'manageqbanks']); + + $params['urltomanageqbanks'] = get_string('qbankgotomanageqbanks', 'qbank_columnsortorder', $urltoredirect->out()); + $params['previewurl'] = new moodle_url('/question/bank/columnsortorder/sortcolumns.php', [ + 'preview' => true + ]); + return $params; + } +} diff --git a/question/bank/columnsortorder/classes/output/fragment.php b/question/bank/columnsortorder/classes/output/fragment.php new file mode 100644 index 00000000000..33fc3653063 --- /dev/null +++ b/question/bank/columnsortorder/classes/output/fragment.php @@ -0,0 +1,37 @@ +. + +namespace qbank_columnsortorder\output; + +/** + * Output fragments for the column sort order interface + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class fragment { + /** + * Render the column sort UI with the current global config. + * + * @return string + */ + public static function column_sort_ui(): string { + global $OUTPUT; + return $OUTPUT->render(new \qbank_columnsortorder\output\column_sort_ui()); + } +} diff --git a/question/bank/columnsortorder/classes/output/reset_columns.php b/question/bank/columnsortorder/classes/output/reset_columns.php new file mode 100644 index 00000000000..601858b509b --- /dev/null +++ b/question/bank/columnsortorder/classes/output/reset_columns.php @@ -0,0 +1,61 @@ +. + +namespace qbank_columnsortorder\output; + +use renderer_base; + +/** + * Renderable for resetting customised column settings. + * + * This will display a link that resets all customised column settings and redirects back to the current page. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class reset_columns implements \renderable, \templatable { + + /** @var \moodle_url The current page URL to redirect back to. */ + protected \moodle_url $returnurl; + + /** @var bool True if we are changing global config, false for user preferences. */ + protected bool $global; + + /** + * Store data for generating the template context. + * + * @param \moodle_url $returnurl + * @param bool $global + */ + public function __construct(\moodle_url $returnurl, bool $global = false) { + $this->returnurl = $returnurl; + $this->global = $global; + } + + public function export_for_template(renderer_base $output): array { + $reseturl = new \moodle_url('/question/bank/columnsortorder/actions.php', [ + 'action' => 'reset', + 'global' => $this->global, + 'sesskey' => sesskey(), + 'returnurl' => $this->returnurl->out(), + ]); + return [ + 'reseturl' => $reseturl->out(false), + ]; + } +} diff --git a/question/bank/columnsortorder/classes/plugin_feature.php b/question/bank/columnsortorder/classes/plugin_feature.php new file mode 100644 index 00000000000..fbb49d6a131 --- /dev/null +++ b/question/bank/columnsortorder/classes/plugin_feature.php @@ -0,0 +1,64 @@ +. + +namespace qbank_columnsortorder; + +use core\context; +use core_question\local\bank\column_manager_base; +use core_question\local\bank\plugin_features_base; +use core_question\local\bank\view; +use qbank_columnsortorder\output\add_column; +use qbank_columnsortorder\output\reset_columns; + +/** + * Plugin features for qbank_columnsortorder + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugin_feature extends plugin_features_base { + /** + * Override the default column manager. + * + * This will set the column order, size and visibility based on the global settings defined on the admin screen, or on the + * current user's preference if they have set one. + * + * @return ?column_manager_base + */ + public function get_column_manager() : ?column_manager_base { + return new column_manager(); + } + + /** + * Return add and reset column controls. + * + * @param view $qbank The question bank view. + * @param context $context The current context, for permission checks. + * @param int $categoryid The current question category ID. + * @return \renderable[] + */ + public function get_question_bank_controls(view $qbank, context $context, int $categoryid) : array { + global $PAGE; + $PAGE->requires->js_call_amd('qbank_columnsortorder/user_actions', 'init'); + $returnurl = new \moodle_url($qbank->returnurl); + return [ + 200 => new add_column(new column_manager(), $returnurl), + 300 => new reset_columns($returnurl), + ]; + } +} diff --git a/question/bank/columnsortorder/classes/tests/external_function_testcase.php b/question/bank/columnsortorder/classes/tests/external_function_testcase.php new file mode 100644 index 00000000000..17a759cdd04 --- /dev/null +++ b/question/bank/columnsortorder/classes/tests/external_function_testcase.php @@ -0,0 +1,93 @@ +. + +/** + * Class defining resuable tests methods for external functions + * + * @package qbank_columnsortorder + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qbank_columnsortorder\tests; + +/** + * Class defining resuable tests methods for external functions + */ +abstract class external_function_testcase extends \advanced_testcase { + + /** + * @var string Fully-qualified external function class to test. + */ + protected $testclass; + + /** + * @var string The name of the setting used to store the data. + */ + protected $setting; + + /** + * @var bool Whether the data is stored as a comma-separated list. + */ + protected $csv = true; + + /** + * A function that returns the data to be passed to the external function. + * + * The data returned will depend on the testclass. + * + * @return mixed + */ + abstract protected function generate_test_data(): mixed; + + /** + * Test that execute() method sets the correct config setting. + */ + public function test_execute(): void { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $testdata = $this->generate_test_data(); + $this->testclass::execute($testdata, true); + + $currentconfig = get_config('qbank_columnsortorder', $this->setting); + if ($this->csv) { + $currentconfig = explode(',', $currentconfig); + } + + $this->assertEqualsCanonicalizing($testdata, $currentconfig); + } + + /** + * Test that execute() method sets user preference when a component is passed. + */ + public function test_execute_user(): void { + $this->resetAfterTest(true); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $testdata = $this->generate_test_data(); + $this->testclass::execute($testdata); + + $userpreference = get_user_preferences('qbank_columnsortorder_' . $this->setting); + if ($this->csv) { + $userpreference = explode(',', $userpreference); + } + + $this->assertEqualsCanonicalizing($testdata, $userpreference); + } +} diff --git a/question/bank/columnsortorder/lib.php b/question/bank/columnsortorder/lib.php new file mode 100644 index 00000000000..172780aa771 --- /dev/null +++ b/question/bank/columnsortorder/lib.php @@ -0,0 +1,36 @@ +. + +/** + * Standard callback functions for qbank_columnsortorder + * + * This file only exists for defining fragment callbacks. Do not include any other functions here. + * + * @package qbank_columnsortorder + * @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Render the column sort UI. {@see \qbank_columnsortorder\output\fragment::column_sort_ui()} + * + * @param array $args + * @return string + */ +function qbank_columnsortorder_output_fragment_column_sort_ui(array $args): string { + return \qbank_columnsortorder\output\fragment::column_sort_ui(); +} diff --git a/question/bank/columnsortorder/styles.css b/question/bank/columnsortorder/styles.css new file mode 100644 index 00000000000..667f4912d58 --- /dev/null +++ b/question/bank/columnsortorder/styles.css @@ -0,0 +1,43 @@ +#page-admin-question-bank-columnsortorder-sortcolumns .addcolumn { + display: inline-block; +} +.qbank-sortable-column { + background-color: white; + position: relative; +} +.jsenabled .qbank-column-list button.savewidths { + display: none; +} +.qbank-sortable-column .qbank_columnsortorder-action-handle { + display: none; +} +.qbank-sortable-column.show-handles .qbank_columnsortorder-action-handle { + display: block; +} +.qbank-sortable-column .handle-container { + pointer-events: none; /* Prevent the handle container blocking clicks to elements in the header */ + width: 100%; + position: absolute; + top: 40%; + z-index: 1; +} +.qbank_columnsortorder-action-handle { + pointer-events: auto; /* Ensure the handles themselves receive clicks */ +} +.qbank_columnsortorder-action-handle.move { + margin-left: -21px; + width: 16px; +} +.qbank_columnsortorder-action-handle.resize { + cursor: col-resize; + position: absolute; + top: 0; + right: 5px; +} +.qbank_columnsortorder-action-handle.resize img { + /* Prevent the resize handle icon being dragged across the page */ + pointer-events: none; +} +.qbank_columnsortorder-action-handle .icon { + margin-right: 0; +} diff --git a/question/bank/columnsortorder/templates/action_handle.mustache b/question/bank/columnsortorder/templates/action_handle.mustache new file mode 100644 index 00000000000..1aabe979ea1 --- /dev/null +++ b/question/bank/columnsortorder/templates/action_handle.mustache @@ -0,0 +1,46 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/action_handle + + Render a single action handle for customising a column. + + Example context (json): + { + "action": "move", + "target": "", + "title": "Move column A", + "pixicon": "i/dragdrop", + "pixcomponent": "core", + "popup": true + } +}} + + {{#pixicon}} + {{#pix}}{{pixicon}}, {{pixcomponent}}{{/pix}} + {{/pixicon}} + {{#icon}} + + {{/icon}} + {{#text}} + {{text}} + {{/text}} + diff --git a/question/bank/columnsortorder/templates/add_column.mustache b/question/bank/columnsortorder/templates/add_column.mustache new file mode 100644 index 00000000000..aa2104abb3a --- /dev/null +++ b/question/bank/columnsortorder/templates/add_column.mustache @@ -0,0 +1,57 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/add_column + + Add column widget + + Displays a list of currently hidden columns, with links to add them back to the question bank. + + Context variables required for this template: + * hiddencolumns - list of currently hidden columns + + Example context (json): + { + "hashiddencolumns": true, + "hiddencolumns": [ + { + "name": "Column A", + "class": "class_name_A", + "addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_A" + }, + { + "name": "Column B", + "class": "class_name_B", + "addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_B" + } + ] + } +}} +{{#hashiddencolumns}} + +{{/hashiddencolumns}} diff --git a/question/bank/columnsortorder/templates/column_sort_preview.mustache b/question/bank/columnsortorder/templates/column_sort_preview.mustache new file mode 100644 index 00000000000..f0d43999cc2 --- /dev/null +++ b/question/bank/columnsortorder/templates/column_sort_preview.mustache @@ -0,0 +1,37 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/column_sort_preview + + Display a preview of the question bank, with a button to return to the customisation UI. + + Context variables required for this template: + * backurl - URL of customisation UI page. + * preview - Rendered HTML for a question bank table with the current settings. + + Example context (json): + { + "backurl": "https://example.com/question/bank/columnsortorder/sortcolumns.php", + "preview": "
" + } +}} +
+ + Back + + {{{preview}}} +
diff --git a/question/bank/columnsortorder/templates/column_sort_ui.mustache b/question/bank/columnsortorder/templates/column_sort_ui.mustache new file mode 100644 index 00000000000..62e6fa88d3d --- /dev/null +++ b/question/bank/columnsortorder/templates/column_sort_ui.mustache @@ -0,0 +1,181 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/column_sort_ui + + Display a list of enable columns with customisation controls, and a list of columns from disabled plugins, with a link to the + question bank plugin management screen. + + Context variables required for this template: + * contextid - Current context id. + * formaction - The URL of the actions controller to submit the form to. + * sesskey - The current sesskey. + * previewurl - The URL of the question bank preview page. + * addcolumn - The context for the qbank_columnsortorder/add_column template. + * resetcolumns - The context for the qbank_columnsortorder/reset_columns template. + * names - A list of the currently active columns + * name - The display name of the column + * tiptitle - The tooltip text for the move handle. + * colname - The unique class name for the column from the plugin. + * escapedclass - The class name with \ replaced by __. + * widthlabel - The label text of for this column's width field. + * minwidth - The minimum value for the width field. + * width - The current value for the width field. + * actionmenu - The context for core/action_menu, a list of actions for the column. + * columnsdisabled - Are the any columns defined by disabled plugins? + * disabled - A list of disabled column names. + * disabledname - The disabled column's name. + * urltomanageqbanks - Link to the page for managing qbank plugins. + + Example context (json): + { + "contextid": 1, + "formaction": "https://example.com/question/bank/columnsortorder/actions.php", + "sesskey": "12345abcde", + "previewurl": "https://example.com/question/bank/columnsortorder/sortcolumns.php?preview=1", + "addcolumn": { + "hashiddencolumns": true, + "hiddencolumns": [ + { + "name": "Column A", + "class": "class_name_A", + "addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_A" + }, + { + "name": "Column B", + "class": "class_name_B", + "addurl": "/question/bank/columnsortorder/actions.php?action=add&column=class_name_B" + } + ] + }, + "resetcolumns": { + "reseturl": "https://example.com/question/bank/columnsortorder/actions.php?action=reset" + }, + "names": [ + { + "name": "Column A", + "tiptitle": "Move Column A", + "columnid": "qbank_example\\col_name_A-col_name_A", + "escapedid": "qbank_example__col_name_A-col_name_A", + "widthlabel": "Width of Column A", + "minwidth": "10", + "width": "" + }, + { + "name": "Column B", + "tiptitle": "Move Column B", + "columnid": "qbank_example\\col_name_B-col_name_B", + "escapedid": "qbank_example__col_name_B-col_name_B", + "minwidth": "10", + "width": "200" + } + ], + "columnsdisabled": true, + "disabled": [ + { + "disabledname": "disabled_1" + }, + { + "disabledname": "disabled_2" + } + ], + "urltomanageqbanks": "Manage qbank plugins" + } +}} +
+

+ {{#str}}qbanksortdescription, qbank_columnsortorder{{/str}} +

+
+ {{! + }}
+
+ + {{#str}}preview{{/str}} + + {{#addcolumn}} + {{>qbank_columnsortorder/add_column}} + {{/addcolumn}} +
+ {{#resetcolumns}} + {{>qbank_columnsortorder/reset_columns}} + {{/resetcolumns}} +
+ + + + + + + + + + + + {{#names}} + + + + + + + + {{/names}} + +
{{#str}}name{{/str}}{{#str}}plugin{{/str}}{{#str}}columnwidth, qbank_columnsortorder{{/str}}{{#str}}action{{/str}}
{{>core/drag_handle}}{{name}}{{colname}} +
+ + + +
+ {{#str}}invalidwidth, qbank_columnsortorder{{/str}} +
+
+
{{#actionmenu}}{{>core/action_menu}}{{/actionmenu}}
+
+
+ {{#columnsdisabled}} + {{#str}}qbankcolumnsdisabled, qbank_columnsortorder{{/str}} +
+ {{/columnsdisabled}} + {{#disabled}} +
+ {{disabledname}} +
+ {{/disabled}} +
+
+ {{{urltomanageqbanks}}} +
+ {{#js}} + require(['qbank_columnsortorder/admin_actions'], function(AdminTable) { + AdminTable.init("qbank_columnsortorder-{{uniqid}}"); + }); + {{/js}} +
diff --git a/question/bank/columnsortorder/templates/handle_container.mustache b/question/bank/columnsortorder/templates/handle_container.mustache new file mode 100644 index 00000000000..6657328e58e --- /dev/null +++ b/question/bank/columnsortorder/templates/handle_container.mustache @@ -0,0 +1,27 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/handle_container + + Container for move/resize handles. + + Example context (json): + { + } +}} +
+
diff --git a/question/bank/columnsortorder/templates/reset_columns.mustache b/question/bank/columnsortorder/templates/reset_columns.mustache new file mode 100644 index 00000000000..f6cc991829c --- /dev/null +++ b/question/bank/columnsortorder/templates/reset_columns.mustache @@ -0,0 +1,33 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/reset_columns + + Button to reset question bank column settings to defaults. + + Context variables required for this template: + + Example context (json): + { + "reseturl": "https://example.com/question/bank/columnsortorder/actions.php?action=reset" + } +}} + + + {{#str}}resetcolumns, qbank_columnsortorder{{/str}} + + diff --git a/question/bank/columnsortorder/templates/resize_modal.mustache b/question/bank/columnsortorder/templates/resize_modal.mustache new file mode 100644 index 00000000000..88cace87bf6 --- /dev/null +++ b/question/bank/columnsortorder/templates/resize_modal.mustache @@ -0,0 +1,29 @@ +{{! + 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 . +}} +{{! + @template qbank_columnsortorder/resize_modal + + This template renders the content of the resize modal, a number input to set the column width in pixels. + + Example context (json): + { + } +}} + diff --git a/question/bank/columnsortorder/tests/behat/admin_settings.feature b/question/bank/columnsortorder/tests/behat/admin_settings.feature new file mode 100644 index 00000000000..3235ce533dc --- /dev/null +++ b/question/bank/columnsortorder/tests/behat/admin_settings.feature @@ -0,0 +1,101 @@ +@qbank @qbank_columnsortorder @javascript +Feature: Set default question bank column order and size + In order to set sensible defaults for the question bank interface + As an admin + I want to hide, reorder, and resize columns + + Scenario: Admin can reorder question bank columns + Given I change the window size to "large" + And I log in as "admin" + When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration + And I drag "Created by" "qbank_columnsortorder > column move handle" and I drop it in "T" "qbank_columnsortorder > column move handle" + Then "Created by" "table_row" should appear before "T" "table_row" + And I reload the page + And "Created by" "table_row" should appear before "T" "table_row" + And I follow "Preview" + And "Created by" "qbank_columnsortorder > column header" should appear before "T" "qbank_columnsortorder > column header" + + Scenario: Disabling a question bank plugin removes its columns + Given I log in as "admin" + When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration + And I should see "Created by" + And I click on "Manage question bank plugins" "link" + And I click on "Disable" "link" in the "View creator" "table_row" + And I click on "Column sort order" "link" + Then "Currently disabled question bank plugins:" "text" should appear before "Created by" "text" + And I click on "Manage question bank plugins" "link" + And I click on "Enable" "link" in the "View creator" "table_row" + And I click on "Column sort order" "link" + Then I should not see "Currently disabled question bank plugins:" + And I should see "Created by" + + Scenario: Admin can hide a column in site administration page + Given I log in as "admin" + And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration + And "Created by" "table_row" should exist + When I click on "Actions menu" "link" in the "Created by" "table_row" + And I choose "Remove" in the open action menu + Then "Created by" "table_row" should not exist + And I reload the page + And "Created by" "table_row" should not exist + And I follow "Preview" + And "Created by" "qbank_columnsortorder > column header" should not exist + + Scenario: Admin can show a column in site administration page + Given the following config values are set as admin: + | config | value | plugin | + | hiddencols | qbank_viewcreator\creator_name_column-creator_name_column | qbank_columnsortorder | + And I log in as "admin" + And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration + And "Created by" "table_row" should not exist + When I press "Add columns" + And I follow "Created by" + Then "Created by" "table_row" should exist + And I reload the page + And "Created by" "table_row" should exist + And I follow "Preview" + And "Created by" "qbank_columnsortorder > column header" should exist + + Scenario: Admin can resize a column in site administration page + Given I log in as "admin" + And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration + And the field "Width of 'Question' in pixels" matches value "" + When I set the field "Width of 'Question' in pixels" to "400" + And I reload the page + Then the field "Width of 'Question' in pixels" matches value "400" + And I follow "Preview" + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px" + + Scenario: Custom fields can be reordered, resized, hidden and shown + Given I change the window size to "large" + And I log in as "admin" + And I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration + And I press "Add a new category" + And I click on "Add a new custom field" "link" + And I follow "Checkbox" + And I set the following fields to these values: + | Name | checkboxcustomcolumn | + | Short name | chckcust | + And I press "Save changes" + And I should see "checkboxcustomcolumn" + And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration + And "checkboxcustomcolumn" "table_row" should appear after "Comments" "table_row" + When I drag "checkboxcustomcolumn" "qbank_columnsortorder > column move handle" and I drop it in "Comments" "qbank_columnsortorder > column move handle" + And I set the field "Width of 'checkboxcustomcolumn' in pixels" to "200" + And I follow "Preview" + And "checkboxcustomcolumn" "qbank_columnsortorder > column header" should appear before "Comments" "qbank_columnsortorder > column header" + And the "style" attribute of "checkboxcustomcolumn" "qbank_columnsortorder > column header" should contain "width: 200px" + And I follow "Back" + And "checkboxcustomcolumn" "table_row" should appear before "Comments" "table_row" + And the field "Width of 'checkboxcustomcolumn' in pixels" matches value "200" + And I click on "Actions menu" "link" in the "checkboxcustomcolumn" "table_row" + And I choose "Remove" in the open action menu + And "checkboxcustomcolumn" "table_row" should not exist + And I follow "Preview" + And "checkboxcustomcolumn" "qbank_columnsortorder > column header" should not exist + And I follow "Back" + And I press "Add columns" + And I follow "checkboxcustomcolumn" + And "checkboxcustomcolumn" "table_row" should exist + And I follow "Preview" + And "checkboxcustomcolumn" "qbank_columnsortorder > column header" should exist diff --git a/question/bank/columnsortorder/tests/behat/behat_qbank_columnsortorder.php b/question/bank/columnsortorder/tests/behat/behat_qbank_columnsortorder.php new file mode 100644 index 00000000000..c3561c7191f --- /dev/null +++ b/question/bank/columnsortorder/tests/behat/behat_qbank_columnsortorder.php @@ -0,0 +1,44 @@ +. + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); + +/** + * Steps definitions related with the drag and drop header. + * @package qbank_columnsortorder + * @category test + * @copyright 2022 Catalyst IT Australia Pty Ltd + * @author Nathan Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_qbank_columnsortorder extends behat_base { + + public static function get_exact_named_selectors(): array { + return [ + new behat_component_named_selector('column header', [ + "//th[contains(@data-name, %locator%)]", + ]), + new behat_component_named_selector('column move handle', [ + "//*[self::th or self::tr][contains(@data-name, %locator%)]//span[contains(@data-drag-type, 'move')]", + ]), + new behat_component_named_selector('column resize handle', [ + "//th[contains(@data-name, %locator%)]//span[contains(@data-action, 'resize')]", + ]), + ]; + } +} diff --git a/question/bank/columnsortorder/tests/behat/columnsortorder.feature b/question/bank/columnsortorder/tests/behat/columnsortorder.feature deleted file mode 100644 index cba9965b00d..00000000000 --- a/question/bank/columnsortorder/tests/behat/columnsortorder.feature +++ /dev/null @@ -1,92 +0,0 @@ -@qbank @qbank_columnsortorder @javascript -Feature: An plugin column can be reordered and displayed in the question bank view. - In order to reorganise the question bank view columns - As an admin - I need to be able to reorder them - - Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | teacher1 | Teacher | 1 | teacher1@example.com | - And the following "courses" exist: - | fullname | shortname | format | - | Course 1 | C1 | weeks | - And the following "activity" exists: - | activity | quiz | - | course | C1 | - | name | Test quiz Q001 | - And the following "course enrolments" exist: - | user | course | role | - | teacher1 | C1 | editingteacher | - And the following "question category" exist: - | contextlevel | reference | name | - | Activity module | Test quiz Q001 | Question category 1 | - And the following "questions" exist: - | questioncategory | qtype | name | user | questiontext | idnumber | - | Question category 1 | essay | Test question to be seen | teacher1 | Write about whatever you want | idnumber1 | - - Scenario: Teacher can see proper view - Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" - When I apply question bank filter "Category" with value "Question category 1" - And I should see "Test question to be seen" - Then I should see "Teacher 1" - - Scenario: Reordering question bank columns - Given I log in as "admin" - When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration - And I drag "Created by (creator_name_column)" "text" and I drop it in "T (question_type_column)" "text" - And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" - And I apply question bank filter "Category" with value "Question category 1" - Then ".creatorname" "css_element" should appear before ".qtype" "css_element" - - Scenario: Disabling and enabling column display is proper - Given I log in as "admin" - When I navigate to "Plugins > Question bank plugins > Column sort order" in site administration - And I should see "Created by (creator_name_column)" - And I click on "Manage question bank plugins" "link" - And I click on "Disable" "link" in the "View creator" "table_row" - And I click on "Column sort order" "link" - Then "Currently disabled question bank plugins:" "text" should appear before "Created by" "text" - And I should not see "Created by (creator_name_column)" - And I click on "Manage question bank plugins" "link" - And I click on "Enable" "link" in the "View creator" "table_row" - And I click on "Column sort order" "link" - Then I should not see "Currently disabled question bank plugins:" - And I should see "Created by (creator_name_column)" - - Scenario: Custom fields are reorderable - Given I log in as "admin" - When I navigate to "Plugins > Question bank plugins > Question custom fields" in site administration - And I press "Add a new category" - And I click on "Add a new custom field" "link" - And I follow "Checkbox" - And I set the following fields to these values: - | Name | checkboxcustomcolumn | - | Short name | chckcust | - And I press "Save changes" - Then I should see "checkboxcustomcolumn" - And I navigate to "Plugins > Question bank plugins > Column sort order" in site administration - And I should see "checkboxcustomcolumn" - And I change the window size to "large" - And I drag "checkboxcustomcolumn" "text" and I drop it in "T (question_type_column)" "text" - Then "checkboxcustomcolumn" "text" should appear before "T (question_type_column)" "text" - And I click on "Manage question bank plugins" "link" - And I click on "Disable" "link" in the "Question custom fields" "table_row" - And I click on "Column sort order" "link" - Then "Currently disabled question bank plugins:" "text" should appear before "chckcust" "text" - And I click on "Manage question bank plugins" "link" - And I click on "Enable" "link" in the "Question custom fields" "table_row" - And I click on "Column sort order" "link" - Then I should not see "Currently disabled question bank plugins:" - And I should see "checkboxcustomcolumn" - - Scenario: Reordering with disabled columns - When I log in as "admin" - And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration - And I click on "Disable" "link" in the "Question statistics" "table_row" - And I click on "Enable" "link" in the "Question statistics" "table_row" - And I click on "Disable" "link" in the "Question statistics" "table_row" - And I am on the "Course 1" "core_question > course question bank" page - Then I should see "Question bank" - And "Create a new question" "button" should exist - # Really, we are just checking the question bank displayed without errors. diff --git a/question/bank/columnsortorder/tests/behat/question_bank.feature b/question/bank/columnsortorder/tests/behat/question_bank.feature new file mode 100644 index 00000000000..d1efde4044b --- /dev/null +++ b/question/bank/columnsortorder/tests/behat/question_bank.feature @@ -0,0 +1,127 @@ +@qbank @qbank_columnsortorder @javascript +Feature: Set question bank column order and size + In order customise my view of the question bank + As a teacher + I want to hide, reorder, and resize columns + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | weeks | + And the following "activity" exists: + | activity | quiz | + | course | C1 | + | name | Test quiz Q001 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "question category" exist: + | contextlevel | reference | name | + | Activity module | Test quiz Q001 | Question category 1 | + And the following "questions" exist: + | questioncategory | qtype | name | user | questiontext | idnumber | + | Question category 1 | essay | Test question to be seen | teacher1 | Write about whatever you want | idnumber1 | + And the following config values are set as admin: + | config | value | plugin | + | hiddencols | qbank_usage\question_last_used_column-question_last_used_column | qbank_columnsortorder | + | enabledcol | qbank_comment\comment_count_column-comment_count_column,qbank_viewquestionname\question_name_idnumber_tags_column-question_name_idnumber_tags_column,core_question\local\bank\edit_menu_column-edit_menu_column,qbank_editquestion\question_status_column-question_status_column,qbank_history\version_number_column-version_number_column,qbank_statistics\columns\discrimination_index:discrimination_index,qbank_statistics\columns\facility_index:facility_index,qbank_statistics\columns\discriminative_efficiency:discriminative_efficiency,qbank_usage\question_usage_column-question_usage_column,qbank_usage\question_last_used_column-question_last_used_column,qbank_viewcreator\creator_name_column-creator_name_column,qbank_viewcreator\modifier_name_column-modifier_name_column,qbank_viewquestiontype\question_type_column-question_type_column | qbank_columnsortorder | + | colsize | [{"column":"qbank_comment\\comment_count_column-comment_count_column","width":"20"},{"column":"qbank_viewquestionname\\question_name_idnumber_tags_column-question_name_idnumber_tags_column","width":"300"},{"column":"qbank_editquestion\\question_status_column-question_status_column","width":"20"},{"column":"qbank_history\\version_number_column-version_number_column","width":"20"},{"column":"qbank_statistics\\columns\\discrimination_index:discrimination_index","width":"20"},{"column":"qbank_statistics\\columns\\facility_index:facility_index","width":"20"},{"column":"qbank_statistics\\columns\\discriminative_efficiency:discriminative_efficiency","width":"20"},{"column":"qbank_usage\\question_usage_column-question_usage_column","width":"20"},{"column":"qbank_viewcreator\\creator_name_column-creator_name_column","width":"200"},{"column":"qbank_viewcreator\\modifier_name_column-modifier_name_column","width":"200"},{"column":"qbank_viewquestiontype\\question_type_column-question_type_column","width":"20"},{"column":"core_question\\local\\bank\\edit_menu_column-edit_menu_column","width":"50"}] | qbank_columnsortorder | + + Scenario: The teacher sees the question bank with global settings initially + Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + When I apply question bank filter "Category" with value "Question category 1" + Then I should see "Test question to be seen" + And "Last used" "qbank_columnsortorder > column header" should not exist + And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header" + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px;" + + Scenario: User preference takes precedence over global defaults + Given the following "user preferences" exist: + | user | preference | value | + | teacher1 | qbank_columnsortorder_hiddencols | qbank_comment\comment_count_column-comment_count_column | + | teacher1 | qbank_columnsortorder_enabledcol | qbank_viewquestionname\question_name_idnumber_tags_column-question_name_idnumber_tags_column,qbank_usage\question_last_used_column-question_last_used_column,core_question\local\bank\edit_menu_column-edit_menu_column,qbank_editquestion\question_status_column-question_status_column,qbank_history\version_number_column-version_number_column,qbank_statistics\columns\discrimination_index:discrimination_index,qbank_statistics\columns\facility_index:facility_index,qbank_statistics\columns\discriminative_efficiency:discriminative_efficiency,qbank_usage\question_usage_column-question_usage_column,qbank_usage\question_last_used_column-question_last_used_column,qbank_viewcreator\creator_name_column-creator_name_column,qbank_viewcreator\modifier_name_column-modifier_name_column,qbank_viewquestiontype\question_type_column-question_type_column | + | teacher1 | qbank_columnsortorder_colsize | [{"column":"qbank_comment\\comment_count_column-comment_count_column","width":"20"},{"column":"qbank_viewquestionname\\question_name_idnumber_tags_column-question_name_idnumber_tags_column","width":"400"},{"column":"qbank_editquestion\\question_status_column-question_status_column","width":"20"},{"column":"qbank_history\\version_number_column-version_number_column","width":"20"},{"column":"qbank_statistics\\columns\\discrimination_index:discrimination_index","width":"20"},{"column":"qbank_statistics\\columns\\facility_index:facility_index","width":"20"},{"column":"qbank_statistics\\columns\\discriminative_efficiency:discriminative_efficiency","width":"20"},{"column":"qbank_usage\\question_usage_column-question_usage_column","width":"20"},{"column":"qbank_viewcreator\\creator_name_column-creator_name_column","width":"200"},{"column":"qbank_viewcreator\\modifier_name_column-modifier_name_column","width":"200"},{"column":"qbank_viewquestiontype\\question_type_column-question_type_column","width":"20"},{"column":"core_question\\local\\bank\\edit_menu_column-edit_menu_column","width":"50"}] | + And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + When I apply question bank filter "Category" with value "Question category 1" + Then "Comments" "qbank_columnsortorder > column header" should not exist + And "Question" "qbank_columnsortorder > column header" should appear before "Last used" "qbank_columnsortorder > column header" + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px;" + + Scenario: User can remove a column in the question bank + Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + And I apply question bank filter "Category" with value "Question category 1" + And "Comments" "qbank_columnsortorder > column header" should exist + And I click on "Actions menu" "link" in the "Comments" "qbank_columnsortorder > column header" + And I choose "Remove" in the open action menu + Then "Comments" "qbank_columnsortorder > column header" should not exist + And I reload the page + And "Comments" "qbank_columnsortorder > column header" should not exist + + Scenario: User can add a column in the question bank + Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + And I apply question bank filter "Category" with value "Question category 1" + And "Last used" "qbank_columnsortorder > column header" should not exist + When I press "Add columns" + And I follow "Last used" + Then "Last used" "qbank_columnsortorder > column header" should exist + And I reload the page + And "Last used" "qbank_columnsortorder > column header" should exist + + Scenario: User can resize a column in the question bank using modal dialog + Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + And I apply question bank filter "Category" with value "Question category 1" + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px" + When I click on "Actions menu" "link" in the "Question" "qbank_columnsortorder > column header" + And I choose "Resize" in the open action menu + And I set the field "Column width (pixels)" to "400" + And I press "Save changes" + Then the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px" + And I reload the page + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 400px" + + Scenario: User can resize a column in the question bank by dragging + Given I change the window size to "large" + And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + And I apply question bank filter "Category" with value "Question category 1" + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should contain "width: 300px" + When I hover "Question" "qbank_columnsortorder > column header" + And I drag "Question" "qbank_columnsortorder > column resize handle" and I drop it in "Status" "qbank_columnsortorder > column header" + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should not contain "width: 300px" + And I reload the page + And the "style" attribute of "Question" "qbank_columnsortorder > column header" should not contain "width: 300px" + + Scenario: User can move a column in the question bank using modal dialog + Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + And I apply question bank filter "Category" with value "Question category 1" + And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header" + When I click on "Actions menu" "link" in the "Comments" "qbank_columnsortorder > column header" + And I choose "Move" in the open action menu + And I follow "After \"Question\"" + Then "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header" + And I reload the page + And "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header" + + Scenario: User can move a column in the question bank by dragging + Given I change the window size to "large" + And I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "teacher1" + And I apply question bank filter "Category" with value "Question category 1" + And "Comments" "qbank_columnsortorder > column header" should appear before "Question" "qbank_columnsortorder > column header" + When I hover "Comments" "qbank_columnsortorder > column header" + And I drag "Comments" "qbank_columnsortorder > column move handle" and I drop it in "Status" "qbank_columnsortorder > column header" + Then "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header" + And I reload the page + And "Comments" "qbank_columnsortorder > column header" should appear after "Question" "qbank_columnsortorder > column header" + + Scenario: Reordering with disabled columns + When I log in as "admin" + And I navigate to "Plugins > Question bank plugins > Manage question bank plugins" in site administration + And I click on "Disable" "link" in the "Question statistics" "table_row" + And I click on "Enable" "link" in the "Question statistics" "table_row" + And I click on "Disable" "link" in the "Question statistics" "table_row" + And I am on the "Course 1" "core_question > course question bank" page + Then I should see "Question bank" + And "Create a new question" "button" should exist + # Really, we are just checking the question bank displayed without errors. diff --git a/question/bank/columnsortorder/tests/column_manager_test.php b/question/bank/columnsortorder/tests/column_manager_test.php index b7a5024ff39..ba1c42434fc 100644 --- a/question/bank/columnsortorder/tests/column_manager_test.php +++ b/question/bank/columnsortorder/tests/column_manager_test.php @@ -20,6 +20,7 @@ defined('MOODLE_INTERNAL') || die(); use advanced_testcase; use context_course; +use core_question\local\bank\column_base; use core_question\local\bank\question_edit_contexts; use core_question\local\bank\view; use moodle_url; @@ -71,9 +72,9 @@ class column_manager_test extends advanced_testcase { // Get current view columns. $this->columns = []; foreach ($this->questionbank->get_visiblecolumns() as $column) { - $this->columns[] = get_class($column); + $this->columns[] = $column->get_column_id(); } - $this->columnmanager = new column_manager(); + $this->columnmanager = new column_manager(true); $this->randomstring = random_string(); } @@ -114,7 +115,7 @@ class column_manager_test extends advanced_testcase { $data = $this->{$dataproperty}; $this->assertFalse(get_config('qbank_columnsortorder', $setting)); $this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting)); - column_manager::{$function}($data); + column_manager::{$function}($data, true); $expected = $csv ? implode(',', $data) : $data; $this->assertEquals($expected, get_config('qbank_columnsortorder', $setting)); $this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting)); @@ -134,7 +135,7 @@ class column_manager_test extends advanced_testcase { $data = $this->{$dataproperty}; $this->assertFalse(get_config('qbank_columnsortorder', $setting)); $this->assertEmpty(get_user_preferences('qbank_columnsortorder_' . $setting)); - column_manager::{$function}($data, 'qbank_columnsortorder'); + column_manager::{$function}($data); $expected = $csv ? implode(',', $data) : $data; $this->assertFalse(get_config('qbank_columnsortorder', $setting)); $this->assertEquals($expected, get_user_preferences('qbank_columnsortorder_' . $setting)); @@ -165,19 +166,17 @@ class column_manager_test extends advanced_testcase { shuffle($neworder); set_config('enabledcol', implode(',', $neworder), 'qbank_columnsortorder'); - $this->columnmanager = new column_manager(); + $this->columnmanager = new column_manager(true); $columnstosort = []; - foreach ($this->columns as $key => $column) { - $colname = explode('\\', $column); - $columnstosort[end($colname)] = $column; + foreach ($this->columns as $column) { + $columnstosort[$column] = $column; } $sortedcolumns = $this->columnmanager->get_sorted_columns($columnstosort); - $expectedorder = ['checkbox_column' => 0]; - foreach ($neworder as $key => $column) { - $colname = explode('\\', $column); - $expectedorder[end($colname)] = $column; + $expectedorder = ['core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column' => 0]; + foreach ($neworder as $columnid) { + $expectedorder[$columnid] = $columnid; } $this->assertSame($expectedorder, $sortedcolumns); } @@ -219,13 +218,14 @@ class column_manager_test extends advanced_testcase { public function test_plugin_enabled_disabled_observers(): void { $neworder = $this->columnmanager->get_sorted_columns($this->columns); shuffle($neworder); - set_columnbank_order::execute($neworder); + $this->columnmanager::set_column_order($neworder, true); // Get the list of enabled columns, excluding core columns (we can't disable those). $currentconfig = get_config('qbank_columnsortorder', 'enabledcol'); $currentconfig = array_filter(explode(',', $currentconfig), fn($class) => !str_starts_with($class, 'core')); // Pick a column at random and get its plugin name. - $class = $currentconfig[array_rand($currentconfig, 1)]; - $randomplugintodisable = explode('\\', $class)[0]; + $randomcolumnid = $currentconfig[array_rand($currentconfig, 1)]; + [$randomcolumnclass] = explode(column_base::ID_SEPARATOR, $randomcolumnid, 2); + [$randomplugintodisable] = explode('\\', $randomcolumnclass); $olddisabledconfig = get_config('qbank_columnsortorder', 'disabledcol'); \core\event\qbank_plugin_disabled::create_for_plugin($randomplugintodisable)->trigger(); $newdisabledconfig = get_config('qbank_columnsortorder', 'disabledcol'); @@ -260,7 +260,7 @@ class column_manager_test extends advanced_testcase { set_config('disabledcol', implode(',', $disabledcols), 'qbank_columnsortorder'); // Enable one of the disabled plugins. - $this->columnmanager = new column_manager(); + $this->columnmanager = new column_manager(true); $this->columnmanager->enable_columns($randomplugin1); // The enabledcol setting should now contain all columns except the remaining disabled plugin. $expectedenabled = array_filter($this->columns, fn($column) => !str_starts_with($column, $randomplugin2)); @@ -291,11 +291,12 @@ class column_manager_test extends advanced_testcase { set_config('disabledcol', implode(',', $disabledcols), 'qbank_columnsortorder'); - $this->columnmanager = new column_manager(); + $this->columnmanager = new column_manager(true); $expecteddisablednames = []; - foreach ($disabledcols as $disabledcol) { - $columnobject = new $disabledcol($this->questionbank); - $expecteddisablednames[] = (object) [ + foreach ($disabledcols as $disabledcolid) { + [$columnclass, $columnname] = explode(column_base::ID_SEPARATOR, $disabledcolid, 2); + $columnobject = $columnclass::from_column_name($this->questionbank, $columnname); + $expecteddisablednames[$disabledcolid] = (object) [ 'disabledname' => $columnobject->get_title(), ]; } diff --git a/question/bank/columnsortorder/tests/external/set_column_size_test.php b/question/bank/columnsortorder/tests/external/set_column_size_test.php new file mode 100644 index 00000000000..298e57ab882 --- /dev/null +++ b/question/bank/columnsortorder/tests/external/set_column_size_test.php @@ -0,0 +1,68 @@ +. + +namespace qbank_columnsortorder\external; + +use qbank_columnsortorder\column_manager; +use qbank_columnsortorder\tests\external_function_testcase; + +// phpcs:disable moodle.PHPUnit.TestCaseNames.Missing +// This class inherits its test methods from the parent class. + +/** + * Unit tests for qbank_columnsortorder external API. + * + * @package qbank_columnsortorder + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \qbank_columnsortorder\external\set_column_size + */ +class set_column_size_test extends external_function_testcase { + + /** + * @var string Fully-qualified external function class to test. + */ + protected $testclass = '\qbank_columnsortorder\external\set_column_size'; + + /** + * @var string The name of the setting used to store the data. + */ + protected $setting = 'colsize'; + + /** + * @var bool Whether the data is stored as a comma-separated list. + */ + protected $csv = false; + + /** + * Generate a list of random column sizes. + * + * @return array + */ + protected function generate_test_data(): string { + $columnsortorder = new column_manager(); + $questionlistcolumns = $columnsortorder->get_columns(); + $columnsizes = []; + foreach ($questionlistcolumns as $columnnobject) { + $columnsizes[] = (object)[ + 'column' => $columnnobject->name, + 'width' => rand(1, 100) . 'px' + ]; + } + return json_encode($columnsizes); + } +} diff --git a/question/bank/columnsortorder/tests/external/set_columnbank_order_test.php b/question/bank/columnsortorder/tests/external/set_columnbank_order_test.php new file mode 100644 index 00000000000..c22fe3edd33 --- /dev/null +++ b/question/bank/columnsortorder/tests/external/set_columnbank_order_test.php @@ -0,0 +1,61 @@ +. + +namespace qbank_columnsortorder\external; + +use qbank_columnsortorder\column_manager; +use qbank_columnsortorder\tests\external_function_testcase; + +// phpcs:disable moodle.PHPUnit.TestCaseNames.Missing +// This class inherits its test methods from the parent class. + +/** + * Unit tests for qbank_columnsortorder external API. + * + * @package qbank_columnsortorder + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \qbank_columnsortorder\external\set_columnbank_order + */ +class set_columnbank_order_test extends external_function_testcase { + + /** + * @var string Fully-qualified external function class to test. + */ + protected $testclass = '\qbank_columnsortorder\external\set_columnbank_order'; + + /** + * @var string The name of the setting used to store the data. + */ + protected $setting = 'enabledcol'; + + /** + * Return a randomly-ordered list of columns. + * + * @return array + */ + protected function generate_test_data(): array { + $columnsortorder = new column_manager(); + $questionlistcolumns = $columnsortorder->get_columns(); + $columnclasses = []; + foreach ($questionlistcolumns as $columnnobject) { + $columnclasses[] = $columnnobject->class; + } + shuffle($columnclasses); + return $columnclasses; + } +} diff --git a/question/bank/columnsortorder/tests/external/set_hidden_columns_test.php b/question/bank/columnsortorder/tests/external/set_hidden_columns_test.php new file mode 100644 index 00000000000..7047151fa09 --- /dev/null +++ b/question/bank/columnsortorder/tests/external/set_hidden_columns_test.php @@ -0,0 +1,57 @@ +. + +namespace qbank_columnsortorder\external; + +use qbank_columnsortorder\column_manager; +use qbank_columnsortorder\tests\external_function_testcase; + +// phpcs:disable moodle.PHPUnit.TestCaseNames.Missing +// This class inherits its test methods from the parent class. + +/** + * Unit tests for qbank_columnsortorder external API. + * + * @package qbank_columnsortorder + * @copyright 2023 Catalyst IT Europe Ltd. + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \qbank_columnsortorder\external\set_hidden_columns + */ +class set_hidden_columns_test extends external_function_testcase { + + /** + * @var string Fully-qualified external function class to test. + */ + protected $testclass = '\qbank_columnsortorder\external\set_hidden_columns'; + + /** + * @var string The name of the setting used to store the data. + */ + protected $setting = 'hiddencols'; + + /** + * Return a random list of hidden column names. + * + * @return array + */ + protected function generate_test_data(): array { + $columnsortorder = new column_manager(); + $questionlistcolumns = $columnsortorder->get_columns(); + $hiddencolumns = array_slice($questionlistcolumns, rand(0, count($questionlistcolumns) - 1)); + return array_map(fn($column) => $column->name, $hiddencolumns); + } +} diff --git a/question/bank/columnsortorder/version.php b/question/bank/columnsortorder/version.php index db804351502..83861d9a9a3 100644 --- a/question/bank/columnsortorder/version.php +++ b/question/bank/columnsortorder/version.php @@ -26,6 +26,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qbank_columnsortorder'; -$plugin->version = 2023052200; +$plugin->version = 2023052201; $plugin->requires = 2023041800; $plugin->maturity = MATURITY_STABLE; diff --git a/question/bank/comment/classes/comment_count_column.php b/question/bank/comment/classes/comment_count_column.php index 3995cf3367b..6fc542f5d3c 100644 --- a/question/bank/comment/classes/comment_count_column.php +++ b/question/bank/comment/classes/comment_count_column.php @@ -110,4 +110,7 @@ class comment_count_column extends column_base { return ['pr-3']; } + public function get_default_width(): int { + return 150; + } } diff --git a/question/bank/statistics/tests/behat/statistics_values.feature b/question/bank/statistics/tests/behat/statistics_values.feature index d14f3cd370e..4832e44a604 100644 --- a/question/bank/statistics/tests/behat/statistics_values.feature +++ b/question/bank/statistics/tests/behat/statistics_values.feature @@ -210,10 +210,8 @@ Feature: Show statistics in question bank And I run the scheduled task "\quiz_statistics\task\recalculate" # Confirm the "Needs checking?" column matches the expected values based on students' answers When I am on the "Quiz 3" "mod_quiz > question bank" page logged in as "admin" - Then the following should exist in the "categoryquestions" table: - | Question | Needs checking? | - | MCA | Likely | - | MCB | Very likely | - | MCC | Unlikely | - | MCD | Likely | - | MCE | Very likely | + Then "Likely" "text" should exist in the "MCA" "table_row" + And "Very likely" "text" should exist in the "MCB" "table_row" + And "Unlikely" "text" should exist in the "MCC" "table_row" + And "Likely" "text" should exist in the "MCD" "table_row" + And "Very likely" "text" should exist in the "MCE" "table_row" diff --git a/question/bank/viewquestionname/classes/viewquestionname_column_helper.php b/question/bank/viewquestionname/classes/viewquestionname_column_helper.php index 6e3027aa4bb..96d1a171ba5 100644 --- a/question/bank/viewquestionname/classes/viewquestionname_column_helper.php +++ b/question/bank/viewquestionname/classes/viewquestionname_column_helper.php @@ -72,4 +72,7 @@ class viewquestionname_column_helper extends column_base { return 'q.name'; } + public function get_default_width(): int { + return 250; + } } diff --git a/question/bank/viewquestiontype/classes/question_type_column.php b/question/bank/viewquestiontype/classes/question_type_column.php index bffc58f2b19..ed81ea66c69 100644 --- a/question/bank/viewquestiontype/classes/question_type_column.php +++ b/question/bank/viewquestiontype/classes/question_type_column.php @@ -52,4 +52,7 @@ class question_type_column extends column_base { return 'q.qtype'; } + public function get_default_width(): int { + return 45; + } } diff --git a/question/classes/local/bank/checkbox_column.php b/question/classes/local/bank/checkbox_column.php index 7d66e8d7ab3..1b022f118be 100644 --- a/question/classes/local/bank/checkbox_column.php +++ b/question/classes/local/bank/checkbox_column.php @@ -96,4 +96,7 @@ class checkbox_column extends column_base { return ['q.id']; } + public function get_default_width(): int { + return 25; + } } diff --git a/question/classes/local/bank/column_base.php b/question/classes/local/bank/column_base.php index 73c449d4322..cc122865de3 100644 --- a/question/classes/local/bank/column_base.php +++ b/question/classes/local/bank/column_base.php @@ -34,7 +34,7 @@ namespace core_question\local\bank; abstract class column_base extends view_component { /** - * @const string A separator for joining column attributes together into a unique ID string. + * @var string A separator for joining column attributes together into a unique ID string. */ const ID_SEPARATOR = '-'; @@ -101,7 +101,8 @@ abstract class column_base extends view_component { /** * Output the column header cell. * - * @params column_action_base[] A list of column actions to include in the header. + * @param column_action_base[] $columnactions A list of column actions to include in the header. + * @param string $width A CSS width property value. */ public function display_header(array $columnactions = [], string $width = ''): void { global $PAGE; @@ -253,7 +254,7 @@ abstract class column_base extends view_component { $tag = 'td'; $attr = [ 'class' => $this->get_classes(), - 'data-pluginname' => get_class($this), + 'data-columnid' => $this->get_column_id(), ]; if ($this->isheading) { $tag = 'th'; @@ -319,6 +320,15 @@ abstract class column_base extends view_component { return []; } + /** + * Return the default column width in pixels. + * + * @return int + */ + public function get_default_width(): int { + return 120; + } + /** * Output the contents of this column. * @param object $question the row from the $question table, augmented with extra information. diff --git a/question/classes/local/bank/column_manager_base.php b/question/classes/local/bank/column_manager_base.php index 6841219e697..d0b595e0c17 100644 --- a/question/classes/local/bank/column_manager_base.php +++ b/question/classes/local/bank/column_manager_base.php @@ -69,6 +69,6 @@ class column_manager_base { * @return string CSS width property value. */ public function get_column_width(column_base $column): string { - return ''; + return $column->get_default_width() . 'px'; } } diff --git a/question/classes/local/bank/view.php b/question/classes/local/bank/view.php index c004c2415ca..4fa1a4c8607 100644 --- a/question/classes/local/bank/view.php +++ b/question/classes/local/bank/view.php @@ -388,14 +388,7 @@ class view { $newpluginclasscolumns = []; $corequestionbankcolumns = [ 'core_question\local\bank\checkbox_column' . column_base::ID_SEPARATOR . 'checkbox_column', - 'qbank_viewquestiontype\question_type_column' . column_base::ID_SEPARATOR . 'question_type_column', - 'qbank_viewquestionname\question_name_idnumber_tags_column' . column_base::ID_SEPARATOR . - 'question_name_idnumber_tags_column', 'core_question\local\bank\edit_menu_column' . column_base::ID_SEPARATOR . 'edit_menu_column', - 'qbank_editquestion\question_status_column' . column_base::ID_SEPARATOR . 'question_status_column', - 'qbank_history\version_number_column' . column_base::ID_SEPARATOR . 'version_number_column', - 'qbank_viewcreator\creator_name_column' . column_base::ID_SEPARATOR . 'creator_name_column', - 'qbank_comment\comment_count_column' . column_base::ID_SEPARATOR . 'comment_count_column', ]; if (question_get_display_preference('qbshowtext', 0, PARAM_INT, new \moodle_url(''))) { @@ -1417,7 +1410,7 @@ class view { // Start of the table. echo \html_writer::start_tag('table', [ 'id' => 'categoryquestions', - 'class' => 'question-bank-table generaltable table-responsive', + 'class' => 'question-bank-table generaltable', 'data-defaultsort' => json_encode($this->sort), ]); diff --git a/question/tests/behat/behat_core_question.php b/question/tests/behat/behat_core_question.php index 8bf8eb90c03..03be434dcef 100644 --- a/question/tests/behat/behat_core_question.php +++ b/question/tests/behat/behat_core_question.php @@ -174,12 +174,24 @@ class behat_core_question extends behat_question_base { * @param string $questionname the question name. */ public function i_action_the_question($action, $questionname) { - $this->execute('behat_action_menu::i_choose_in_the_named_menu_in_container', [ - $action, - get_string('edit', 'core'), - $questionname, - 'table_row', - ]); + if ($this->running_javascript()) { + // This method isn't allowed unless Javascript is running. + $this->execute('behat_action_menu::i_open_the_action_menu_in', [ + $questionname, + 'table_row', + ]); + $this->execute('behat_action_menu::i_choose_in_the_open_action_menu', [ + $action + ]); + } else { + // This method doesn't open the menu correctly when Javascript is running. + $this->execute('behat_action_menu::i_choose_in_the_named_menu_in_container', [ + $action, + get_string('edit', 'core'), + $questionname, + 'table_row', + ]); + } } /**