diff --git a/admin/amd/build/plugin_management_table.min.js b/admin/amd/build/plugin_management_table.min.js index dcb425a81a2..886eeb66128 100644 --- a/admin/amd/build/plugin_management_table.min.js +++ b/admin/amd/build/plugin_management_table.min.js @@ -1,3 +1,3 @@ -define("core_admin/plugin_management_table",["exports","core_table/dynamic","core_table/local/dynamic/selectors","core/ajax","core/pending","core/notification"],(function(_exports,_dynamic,Selectors,_ajax,_pending,_notification){var 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 _defineProperty(obj,key,value){return(key=function(arg){var key=function(input,hint){if("object"!=typeof input||null===input)return input;var prim=input[Symbol.toPrimitive];if(void 0!==prim){var res=prim.call(input,hint||"default");if("object"!=typeof res)return res;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===hint?String:Number)(input)}(arg,"string");return"symbol"==typeof key?key:String(key)}(key))in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Selectors=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}(Selectors),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};let watching=!1;return _exports.default=class{constructor(){_defineProperty(this,"clickHandlers",[]),this.addClickHandler(this.handleStateToggle),this.registerEventListeners()}static init(){watching||(watching=!0,new this)}addClickHandler(handler){this.clickHandlers.push(handler.bind(this))}registerEventListeners(){document.addEventListener("click",function(e){const tableRoot=this.getTableRoot(e);tableRoot&&this.clickHandlers.forEach((handler=>handler(tableRoot,e)))}.bind(this))}getTableRoot(e){const tableRoot=e.target.closest(Selectors.main.region);return tableRoot||!1}setPluginState(methodname,plugin,state){return(0,_ajax.call)([{methodname:methodname,args:{plugin:plugin,state:state}}])[0]}async handleStateToggle(tableRoot,e){const stateToggle=e.target.closest('[data-action="togglestate"][data-toggle-method]');if(stateToggle){e.preventDefault();const pendingPromise=new _pending.default("core_table/dynamic:togglestate");await this.setPluginState(stateToggle.dataset.toggleMethod,stateToggle.dataset.plugin,"1"===stateToggle.dataset.state?0:1);const[updatedRoot]=await Promise.all([(0,_dynamic.refreshTableContent)(tableRoot),(0,_notification.fetchNotifications)()]);updatedRoot.querySelector('[data-action="togglestate"][data-plugin="'.concat(stateToggle.dataset.plugin,'"]')).focus(),pendingPromise.resolve()}}},_exports.default})); +define("core_admin/plugin_management_table",["exports","core_table/dynamic","core_table/local/dynamic/selectors","core/ajax","core/pending","core/notification"],(function(_exports,_dynamic,Selectors,_ajax,_pending,_notification){var 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 _defineProperty(obj,key,value){return(key=function(arg){var key=function(input,hint){if("object"!=typeof input||null===input)return input;var prim=input[Symbol.toPrimitive];if(void 0!==prim){var res=prim.call(input,hint||"default");if("object"!=typeof res)return res;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===hint?String:Number)(input)}(arg,"string");return"symbol"==typeof key?key:String(key)}(key))in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Selectors=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}(Selectors),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};let watching=!1;return _exports.default=class{constructor(){_defineProperty(this,"clickHandlers",[]),this.addClickHandler(this.handleStateToggle),this.addClickHandler(this.handleMoveUpDown),this.registerEventListeners()}static init(){watching||(watching=!0,new this)}addClickHandler(handler){this.clickHandlers.push(handler.bind(this))}registerEventListeners(){document.addEventListener("click",function(e){const tableRoot=this.getTableRoot(e);tableRoot&&this.clickHandlers.forEach((handler=>handler(tableRoot,e)))}.bind(this))}getTableRoot(e){const tableRoot=e.target.closest(Selectors.main.region);return tableRoot||!1}setPluginState(methodname,plugin,state){return(0,_ajax.call)([{methodname:methodname,args:{plugin:plugin,state:state}}])[0]}setPluginOrder(methodname,plugin,direction){return(0,_ajax.call)([{methodname:methodname,args:{plugin:plugin,direction:direction}}])[0]}async handleStateToggle(tableRoot,e){const stateToggle=e.target.closest('[data-action="togglestate"][data-toggle-method]');if(stateToggle){e.preventDefault();const pendingPromise=new _pending.default("core_table/dynamic:togglestate");await this.setPluginState(stateToggle.dataset.toggleMethod,stateToggle.dataset.plugin,"1"===stateToggle.dataset.state?0:1);const[updatedRoot]=await Promise.all([(0,_dynamic.refreshTableContent)(tableRoot),(0,_notification.fetchNotifications)()]);updatedRoot.querySelector('[data-action="togglestate"][data-plugin="'.concat(stateToggle.dataset.plugin,'"]')).focus(),pendingPromise.resolve()}}async handleMoveUpDown(tableRoot,e){const actionLink=e.target.closest('[data-action="move"][data-method][data-direction]');if(!actionLink)return;e.preventDefault();const pendingPromise=new _pending.default("core_table/dynamic:processAction");await this.setPluginOrder(actionLink.dataset.method,actionLink.dataset.plugin,"up"===actionLink.dataset.direction?-1:1);const[updatedRoot]=await Promise.all([(0,_dynamic.refreshTableContent)(tableRoot),(0,_notification.fetchNotifications)()]),exactMatch=updatedRoot.querySelector('[data-action="move"][data-plugin="'.concat(actionLink.dataset.plugin,'"][data-direction="').concat(actionLink.dataset.direction,'"]'));var _updatedRoot$querySel;exactMatch?exactMatch.focus():null===(_updatedRoot$querySel=updatedRoot.querySelector('[data-action="move"][data-plugin="'.concat(actionLink.dataset.plugin,'"]')))||void 0===_updatedRoot$querySel||_updatedRoot$querySel.focus();pendingPromise.resolve()}},_exports.default})); //# sourceMappingURL=plugin_management_table.min.js.map \ No newline at end of file diff --git a/admin/amd/build/plugin_management_table.min.js.map b/admin/amd/build/plugin_management_table.min.js.map index 719b4853f71..d20ef08f086 100644 --- a/admin/amd/build/plugin_management_table.min.js.map +++ b/admin/amd/build/plugin_management_table.min.js.map @@ -1 +1 @@ -{"version":3,"file":"plugin_management_table.min.js","sources":["../src/plugin_management_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {refreshTableContent} from 'core_table/dynamic';\nimport * as Selectors from 'core_table/local/dynamic/selectors';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\nimport {fetchNotifications} from 'core/notification';\n\nlet watching = false;\n\nexport default class {\n /**\n * @property {function[]} clickHandlers a list of handlers to call on click.\n */\n clickHandlers = [];\n\n constructor() {\n this.addClickHandler(this.handleStateToggle);\n this.registerEventListeners();\n }\n\n /**\n * Initialise an instance of the class.\n *\n * This is just a way of making it easier to initialise an instance of the class from PHP.\n */\n static init() {\n if (watching) {\n return;\n }\n watching = true;\n new this();\n }\n\n /**\n * Add a click handler to the list of handlers.\n *\n * @param {Function} handler A handler to call on a click event\n */\n addClickHandler(handler) {\n this.clickHandlers.push(handler.bind(this));\n }\n\n /**\n * Register the event listeners for this instance.\n */\n registerEventListeners() {\n document.addEventListener('click', function(e) {\n const tableRoot = this.getTableRoot(e);\n\n if (!tableRoot) {\n return;\n }\n\n this.clickHandlers.forEach((handler) => handler(tableRoot, e));\n }.bind(this));\n }\n\n /**\n * Get the table root from an event.\n *\n * @param {Event} e\n * @returns {HTMLElement|bool}\n */\n getTableRoot(e) {\n const tableRoot = e.target.closest(Selectors.main.region);\n if (!tableRoot) {\n return false;\n }\n\n return tableRoot;\n }\n\n /**\n * Set the plugin state (enabled or disabled)\n *\n * @param {string} methodname The web service to call\n * @param {string} plugin The name of the plugin to set the state for\n * @param {number} state The state to set\n * @returns {Promise}\n */\n setPluginState(methodname, plugin, state) {\n return fetchMany([{\n methodname,\n args: {\n plugin,\n state,\n },\n }])[0];\n }\n\n /**\n * Handle state toggling.\n *\n * @param {HTMLElement} tableRoot\n * @param {Event} e\n */\n async handleStateToggle(tableRoot, e) {\n const stateToggle = e.target.closest('[data-action=\"togglestate\"][data-toggle-method]');\n if (stateToggle) {\n e.preventDefault();\n const pendingPromise = new Pending('core_table/dynamic:togglestate');\n\n await this.setPluginState(\n stateToggle.dataset.toggleMethod,\n stateToggle.dataset.plugin,\n stateToggle.dataset.state === '1' ? 0 : 1\n );\n\n const [updatedRoot] = await Promise.all([\n refreshTableContent(tableRoot),\n fetchNotifications(),\n ]);\n\n // Refocus on the link that as pressed in the first place.\n updatedRoot.querySelector(`[data-action=\"togglestate\"][data-plugin=\"${stateToggle.dataset.plugin}\"]`).focus();\n pendingPromise.resolve();\n }\n }\n}\n"],"names":["obj","_getRequireWildcardCache","nodeInterop","WeakMap","cacheBabelInterop","cacheNodeInterop","_defineProperty","key","value","arg","input","hint","prim","Symbol","toPrimitive","undefined","res","call","TypeError","String","Number","_toPrimitive","_toPropertyKey","Object","defineProperty","enumerable","configurable","writable","Selectors","__esModule","default","cache","has","get","newObj","hasPropertyDescriptor","getOwnPropertyDescriptor","prototype","hasOwnProperty","desc","set","_interopRequireWildcard","_pending","watching","_exports","constructor","this","addClickHandler","handleStateToggle","registerEventListeners","static","handler","clickHandlers","push","bind","document","addEventListener","e","tableRoot","getTableRoot","forEach","target","closest","main","region","setPluginState","methodname","plugin","state","fetchMany","args","async","stateToggle","preventDefault","pendingPromise","Pending","dataset","toggleMethod","updatedRoot","Promise","all","refreshTableContent","fetchNotifications","querySelector","concat","focus","resolve"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"plugin_management_table.min.js","sources":["../src/plugin_management_table.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\nimport {refreshTableContent} from 'core_table/dynamic';\nimport * as Selectors from 'core_table/local/dynamic/selectors';\nimport {call as fetchMany} from 'core/ajax';\nimport Pending from 'core/pending';\nimport {fetchNotifications} from 'core/notification';\n\nlet watching = false;\n\nexport default class {\n /**\n * @property {function[]} clickHandlers a list of handlers to call on click.\n */\n clickHandlers = [];\n\n constructor() {\n this.addClickHandler(this.handleStateToggle);\n this.addClickHandler(this.handleMoveUpDown);\n this.registerEventListeners();\n }\n\n /**\n * Initialise an instance of the class.\n *\n * This is just a way of making it easier to initialise an instance of the class from PHP.\n */\n static init() {\n if (watching) {\n return;\n }\n watching = true;\n new this();\n }\n\n /**\n * Add a click handler to the list of handlers.\n *\n * @param {Function} handler A handler to call on a click event\n */\n addClickHandler(handler) {\n this.clickHandlers.push(handler.bind(this));\n }\n\n /**\n * Register the event listeners for this instance.\n */\n registerEventListeners() {\n document.addEventListener('click', function(e) {\n const tableRoot = this.getTableRoot(e);\n\n if (!tableRoot) {\n return;\n }\n\n this.clickHandlers.forEach((handler) => handler(tableRoot, e));\n }.bind(this));\n }\n\n /**\n * Get the table root from an event.\n *\n * @param {Event} e\n * @returns {HTMLElement|bool}\n */\n getTableRoot(e) {\n const tableRoot = e.target.closest(Selectors.main.region);\n if (!tableRoot) {\n return false;\n }\n\n return tableRoot;\n }\n\n /**\n * Set the plugin state (enabled or disabled)\n *\n * @param {string} methodname The web service to call\n * @param {string} plugin The name of the plugin to set the state for\n * @param {number} state The state to set\n * @returns {Promise}\n */\n setPluginState(methodname, plugin, state) {\n return fetchMany([{\n methodname,\n args: {\n plugin,\n state,\n },\n }])[0];\n }\n\n setPluginOrder(methodname, plugin, direction) {\n return fetchMany([{\n methodname,\n args: {\n plugin,\n direction,\n },\n }])[0];\n }\n\n /**\n * Handle state toggling.\n *\n * @param {HTMLElement} tableRoot\n * @param {Event} e\n */\n async handleStateToggle(tableRoot, e) {\n const stateToggle = e.target.closest('[data-action=\"togglestate\"][data-toggle-method]');\n if (stateToggle) {\n e.preventDefault();\n const pendingPromise = new Pending('core_table/dynamic:togglestate');\n\n await this.setPluginState(\n stateToggle.dataset.toggleMethod,\n stateToggle.dataset.plugin,\n stateToggle.dataset.state === '1' ? 0 : 1\n );\n\n const [updatedRoot] = await Promise.all([\n refreshTableContent(tableRoot),\n fetchNotifications(),\n ]);\n\n // Refocus on the link that as pressed in the first place.\n updatedRoot.querySelector(`[data-action=\"togglestate\"][data-plugin=\"${stateToggle.dataset.plugin}\"]`).focus();\n pendingPromise.resolve();\n }\n }\n\n async handleMoveUpDown(tableRoot, e) {\n const actionLink = e.target.closest('[data-action=\"move\"][data-method][data-direction]');\n if (!actionLink) {\n return;\n }\n\n e.preventDefault();\n\n const pendingPromise = new Pending('core_table/dynamic:processAction');\n\n await this.setPluginOrder(\n actionLink.dataset.method,\n actionLink.dataset.plugin,\n actionLink.dataset.direction === 'up' ? -1 : 1,\n );\n\n const [updatedRoot] = await Promise.all([\n refreshTableContent(tableRoot),\n fetchNotifications(),\n ]);\n\n // Refocus on the link that as pressed in the first place.\n const exactMatch = updatedRoot.querySelector(\n `[data-action=\"move\"][data-plugin=\"${actionLink.dataset.plugin}\"][data-direction=\"${actionLink.dataset.direction}\"]`\n );\n if (exactMatch) {\n exactMatch.focus();\n } else {\n // The move link is not present anymore, so we need to focus on the other one.\n updatedRoot.querySelector(`[data-action=\"move\"][data-plugin=\"${actionLink.dataset.plugin}\"]`)?.focus();\n }\n\n pendingPromise.resolve();\n }\n}\n"],"names":["obj","_getRequireWildcardCache","nodeInterop","WeakMap","cacheBabelInterop","cacheNodeInterop","_defineProperty","key","value","arg","input","hint","prim","Symbol","toPrimitive","undefined","res","call","TypeError","String","Number","_toPrimitive","_toPropertyKey","Object","defineProperty","enumerable","configurable","writable","Selectors","__esModule","default","cache","has","get","newObj","hasPropertyDescriptor","getOwnPropertyDescriptor","prototype","hasOwnProperty","desc","set","_interopRequireWildcard","_pending","watching","_exports","constructor","this","addClickHandler","handleStateToggle","handleMoveUpDown","registerEventListeners","static","handler","clickHandlers","push","bind","document","addEventListener","e","tableRoot","getTableRoot","forEach","target","closest","main","region","setPluginState","methodname","plugin","state","fetchMany","args","setPluginOrder","direction","async","stateToggle","preventDefault","pendingPromise","Pending","dataset","toggleMethod","updatedRoot","Promise","all","refreshTableContent","fetchNotifications","querySelector","concat","focus","resolve","actionLink","method","exactMatch","_updatedRoot$querySel"],"mappings":"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"} \ No newline at end of file diff --git a/admin/amd/src/plugin_management_table.js b/admin/amd/src/plugin_management_table.js index 622f4e6e02d..6a6644b98bf 100644 --- a/admin/amd/src/plugin_management_table.js +++ b/admin/amd/src/plugin_management_table.js @@ -29,6 +29,7 @@ export default class { constructor() { this.addClickHandler(this.handleStateToggle); + this.addClickHandler(this.handleMoveUpDown); this.registerEventListeners(); } @@ -102,6 +103,16 @@ export default class { }])[0]; } + setPluginOrder(methodname, plugin, direction) { + return fetchMany([{ + methodname, + args: { + plugin, + direction, + }, + }])[0]; + } + /** * Handle state toggling. * @@ -130,4 +141,39 @@ export default class { pendingPromise.resolve(); } } + + async handleMoveUpDown(tableRoot, e) { + const actionLink = e.target.closest('[data-action="move"][data-method][data-direction]'); + if (!actionLink) { + return; + } + + e.preventDefault(); + + const pendingPromise = new Pending('core_table/dynamic:processAction'); + + await this.setPluginOrder( + actionLink.dataset.method, + actionLink.dataset.plugin, + actionLink.dataset.direction === 'up' ? -1 : 1, + ); + + const [updatedRoot] = await Promise.all([ + refreshTableContent(tableRoot), + fetchNotifications(), + ]); + + // Refocus on the link that as pressed in the first place. + const exactMatch = updatedRoot.querySelector( + `[data-action="move"][data-plugin="${actionLink.dataset.plugin}"][data-direction="${actionLink.dataset.direction}"]` + ); + if (exactMatch) { + exactMatch.focus(); + } else { + // The move link is not present anymore, so we need to focus on the other one. + updatedRoot.querySelector(`[data-action="move"][data-plugin="${actionLink.dataset.plugin}"]`)?.focus(); + } + + pendingPromise.resolve(); + } } diff --git a/admin/classes/external/set_plugin_order.php b/admin/classes/external/set_plugin_order.php new file mode 100644 index 00000000000..d3cf3cd30a8 --- /dev/null +++ b/admin/classes/external/set_plugin_order.php @@ -0,0 +1,84 @@ +. + +namespace core_admin\external; + +use core_external\external_api; +use core_external\external_function_parameters; +use core_external\external_single_structure; +use core_external\external_value; + +/** + * Web Service to control the order of a plugin. + * + * @package core_admin + * @category external + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_plugin_order 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([ + 'plugin' => new external_value(PARAM_PLUGIN, 'The name of the plugin', VALUE_REQUIRED), + 'direction' => new external_value(PARAM_INT, 'The direction to move', VALUE_REQUIRED), + ]); + } + + /** + * Set the plugin state. + * + * @param string $plugin The name of the plugin + * @param int $direction The direction to move the plugin + * @return array + */ + public static function execute( + string $plugin, + int $direction, + ): array { + [ + 'plugin' => $plugin, + 'direction' => $direction, + ] = self::validate_parameters(self::execute_parameters(), [ + 'plugin' => $plugin, + 'direction' => $direction, + ]); + + $context = \context_system::instance(); + self::validate_context($context); + require_capability('moodle/site:config', $context); + + [$plugintype, $pluginname] = explode('_', \core_component::normalize_componentname($plugin), 2); + + $manager = \core_plugin_manager::resolve_plugininfo_class($plugintype); + $manager::change_plugin_order($pluginname, $direction); + + return []; + } + + /** + * Describe the return structure of the external service. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([]); + } +} diff --git a/admin/classes/table/editor_management_table.php b/admin/classes/table/editor_management_table.php index a8f566cceca..556d55fc18e 100644 --- a/admin/classes/table/editor_management_table.php +++ b/admin/classes/table/editor_management_table.php @@ -40,10 +40,6 @@ class editor_management_table extends \core_admin\table\plugin_management_table return new moodle_url('/admin/editors.php', $params); } - protected function supports_ordering(): bool { - return true; - } - protected function order_plugins(array $plugins): array { global $CFG; diff --git a/admin/classes/table/plugin_management_table.php b/admin/classes/table/plugin_management_table.php index 712ef4e944d..b7d1f5a1aed 100644 --- a/admin/classes/table/plugin_management_table.php +++ b/admin/classes/table/plugin_management_table.php @@ -65,10 +65,6 @@ abstract class plugin_management_table extends flexible_table implements dynamic return $plugin->is_enabled(); })); - $this->enabledplugincount = count(array_filter($this->plugins, function ($plugin) { - return $plugin->is_enabled(); - })); - $this->setup_column_configuration(); $this->set_filterset(new plugin_management_table_filterset()); $this->setup(); @@ -80,8 +76,12 @@ abstract class plugin_management_table extends flexible_table implements dynamic * @return \core\plugininfo\base[] */ protected function get_sorted_plugins(): array { - $plugins = $this->pluginmanager->get_plugins_of_type($this->get_plugintype()); - return self::sort_plugins($plugins); + if ($this->plugininfoclass::plugintype_supports_ordering()) { + return $this->plugininfoclass::get_sorted_plugins(); + } else { + $plugins = $this->pluginmanager->get_plugins_of_type($this->get_plugintype()); + return self::sort_plugins($plugins); + } } /** @@ -176,7 +176,7 @@ abstract class plugin_management_table extends flexible_table implements dynamic * @return null|string */ protected function get_sortorder_service(): ?string { - return null; + return 'core_admin_set_plugin_order'; } /** @@ -348,7 +348,7 @@ abstract class plugin_management_table extends flexible_table implements dynamic $hasdown = false; } - if ($this->get_sortorder_service()) { + if ($this->supports_ordering()) { $dataattributes = [ 'data-method' => $this->get_sortorder_service(), 'data-action' => 'move', @@ -490,6 +490,6 @@ abstract class plugin_management_table extends flexible_table implements dynamic * @return bool */ protected function supports_ordering(): bool { - return false; + return $this->plugininfoclass::plugintype_supports_ordering(); } } diff --git a/admin/tests/external/set_plugin_order_test.php b/admin/tests/external/set_plugin_order_test.php new file mode 100644 index 00000000000..474889ed070 --- /dev/null +++ b/admin/tests/external/set_plugin_order_test.php @@ -0,0 +1,170 @@ +. + +declare(strict_types=1); + +namespace core_admin\external; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Unit tests to configure the plugin order. + * + * Note: Not all plugins can be ordered, so this test is limited to those which support it. + * + * @package core + * @covers \core_admin\external\set_plugin_state + * @copyright 2023 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class set_plugin_order_test extends \externallib_advanced_testcase { + /** + * Text execute method for editor plugins, which support ordering. + * + * @dataProvider execute_editor_provider + * @param string $initialstate The initial state of the plugintype + * @param string $plugin The name of the plugin + * @param int $direction + * @param array $neworder + * @param string $newstate + */ + public function test_execute_editors( + string $initialstate, + string $plugin, + int $direction, + array $neworder, + string $newstate, + ): void { + global $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $CFG->texteditors = $initialstate; + + set_plugin_order::execute($plugin, $direction); + + $this->assertSame( + $neworder, + array_keys(\core\plugininfo\editor::get_sorted_plugins()), + ); + $this->assertSame($newstate, $CFG->texteditors); + } + + /** + * Data provider for base tests of the execute method. + * + * @return array + */ + public function execute_editor_provider(): array { + return [ + [ + 'initialstate' => 'textarea,tiny', + 'pluginname' => 'editor_textarea', + 1, // DOWN. + 'expected' => [ + 'tiny', + 'textarea', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'tiny,textarea', + ], + [ + 'initialstate' => 'textarea,tiny', + 'pluginname' => 'editor_textarea', + -1, // UP. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + [ + 'initialstate' => 'textarea,tiny', + 'pluginname' => 'editor_tiny', + 1, // DOWN. + // Tiny is already at the bottom of the list of enabled plugins. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + [ + 'initialstate' => 'textarea,tiny', + 'pluginname' => 'editor_atto', + 1, // DOWN. + // Atto is not enabled. Disabled editors are listed lexically after enabled editors. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + ]; + } + + /** + * Text execute method for plugins which do not support ordering. + * + * @dataProvider execute_non_orderable_provider + * @param string $plugin + */ + public function test_execute_editors_non_orderable(string $plugin): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $this->assertIsArray(set_plugin_order::execute($plugin, 1)); + } + + public function execute_non_orderable_provider(): array { + return [ + // Activities do not support ordering. + ['mod_assign'], + // Nor to blocks. + ['block_login'], + ]; + } + + /** + * Test execute method with no login. + */ + public function test_execute_no_login(): void { + $this->expectException(\require_login_exception::class); + set_plugin_order::execute('editor_tiny', 1); + } + + /** + * Test execute method with no login. + */ + public function test_execute_no_capability(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + $this->expectException(\required_capability_exception::class); + set_plugin_order::execute('editor_tiny', 1); + } +} diff --git a/lib/classes/plugininfo/base.php b/lib/classes/plugininfo/base.php index ba9cc56d42c..b7ce65ff9bf 100644 --- a/lib/classes/plugininfo/base.php +++ b/lib/classes/plugininfo/base.php @@ -23,10 +23,10 @@ */ namespace core\plugininfo; -use core_component, core_plugin_manager, moodle_url, coding_exception; - -defined('MOODLE_INTERNAL') || die(); - +use coding_exception; +use core_component; +use core_plugin_manager; +use moodle_url; /** * Base class providing access to the information about a plugin @@ -71,6 +71,12 @@ abstract class base { /** @var array|null array of {@link \core\update\info} for this plugin */ protected $availableupdates; + /** @var int Move a plugin up in the plugin order */ + public const MOVE_UP = -1; + + /** @var int Move a plugin down in the plugin order */ + public const MOVE_DOWN = 1; + /** * Whether this plugintype supports its plugins being disabled. * @@ -665,4 +671,39 @@ abstract class base { 'return' => $return, )); } + + /** + * Whether this plugintype supports ordering of plugins using native functionality. + * + * Please note that plugintypes which pre-date this native functionality may still support ordering + * but will not use the built-in functionality. + * + * @return bool + */ + public static function plugintype_supports_ordering(): bool { + return false; + } + + /** + * Finds all enabled plugins, the result may include missing plugins. + * + * @param bool $enabledonly Show all plugins, or only those which are enabled + * @return array|null of sorted plugins $pluginname => $pluginname, null means unknown + */ + public static function get_sorted_plugins(bool $enabledonly = false): ?array { + return null; + } + + /** + * Change the order of the plugin relative to other plugins in the plugintype. + * + * When possible, the change will be stored into the config_log table, to let admins check when/who has modified it. + * + * @param string $pluginname The plugin name to enable/disable. + * @param int $direction The direction to move the plugin. Negative numbers mean up, Positive mean down. + * @return bool Whether $pluginname has been updated or not. + */ + public static function change_plugin_order(string $pluginname, int $direction): bool { + return false; + } } diff --git a/lib/classes/plugininfo/editor.php b/lib/classes/plugininfo/editor.php index 2d19b0c8d09..d0547a91fab 100644 --- a/lib/classes/plugininfo/editor.php +++ b/lib/classes/plugininfo/editor.php @@ -23,10 +23,9 @@ */ namespace core\plugininfo; -use moodle_url, part_of_admin_tree, admin_settingpage; - -defined('MOODLE_INTERNAL') || die(); - +use admin_settingpage; +use moodle_url; +use part_of_admin_tree; /** * Class for HTML editors @@ -138,4 +137,76 @@ class editor extends base { public static function get_manage_url() { return new moodle_url('/admin/settings.php', array('section'=>'manageeditors')); } + + public static function plugintype_supports_ordering(): bool { + return true; + } + + public static function get_sorted_plugins(bool $enabledonly = false): ?array { + global $CFG; + + $pluginmanager = \core_plugin_manager::instance(); + $plugins = $pluginmanager->get_plugins_of_type('editor'); + + // The Editor list is stored in an ordered string. + $activeeditors = explode(',', $CFG->texteditors); + + $sortedplugins = []; + foreach ($activeeditors as $editor) { + if (isset($plugins[$editor])) { + $sortedplugins[$editor] = $plugins[$editor]; + unset($plugins[$editor]); + } + } + + if ($enabledonly) { + return $sortedplugins; + } + + // Sort the rest of the plugins lexically. + uasort($plugins, function ($a, $b) { + return strnatcasecmp($a->name, $b->name); + }); + + return array_merge( + $sortedplugins, + $plugins, + ); + } + + public static function change_plugin_order(string $pluginname, int $direction): bool { + $activeeditors = array_keys(self::get_sorted_plugins(true)); + $key = array_search($pluginname, $activeeditors); + + if ($key === false) { + return false; + } + + if ($direction === self::MOVE_DOWN) { + // Move down the list. + if ($key < (count($activeeditors) - 1)) { + $fsave = $activeeditors[$key]; + $activeeditors[$key] = $activeeditors[$key + 1]; + $activeeditors[$key + 1] = $fsave; + add_to_config_log('editor_position', $key, $key + 1, $pluginname); + set_config('texteditors', implode(',', $activeeditors)); + \core_plugin_manager::reset_caches(); + + return true; + } + } else if ($direction === self::MOVE_UP) { + if ($key >= 1) { + $fsave = $activeeditors[$key]; + $activeeditors[$key] = $activeeditors[$key - 1]; + $activeeditors[$key - 1] = $fsave; + add_to_config_log('editor_position', $key, $key - 1, $pluginname); + set_config('texteditors', implode(',', $activeeditors)); + \core_plugin_manager::reset_caches(); + + return true; + } + } + + return false; + } } diff --git a/lib/db/services.php b/lib/db/services.php index 4773429f4fa..df01c242b0e 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2959,6 +2959,12 @@ $functions = array( 'type' => 'write', 'ajax' => true, ], + 'core_admin_set_plugin_order' => [ + 'classname' => 'core_admin\external\set_plugin_order', + 'description' => 'Set the order of a plugin', + 'type' => 'write', + 'ajax' => true, + ], ); $services = array( diff --git a/lib/tests/plugininfo/base_test.php b/lib/tests/plugininfo/base_test.php index 72f7c86f897..6a04b96aec6 100644 --- a/lib/tests/plugininfo/base_test.php +++ b/lib/tests/plugininfo/base_test.php @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -declare(strict_types = 1); +declare(strict_types=1); namespace core\plugininfo; @@ -272,4 +272,49 @@ class base_test extends \advanced_testcase { ], ]; } + + /** + * Ensure that plugintype_supports_ordering() returns true. + * @covers ::plugintype_supports_ordering + */ + public function test_plugintype_supports_ordering(): void { + $this->assertFalse(base::plugintype_supports_ordering()); + } + + /** + * Ensure that the base implementation is used for plugins not supporting ordering. + * + * @dataProvider plugins_not_supporting_ordering + * @param string $plugin + * @coversNothing + * + * Note: This test cannot declare coverage because it covers the various plugin implementations. + */ + public function test_get_sorted_plugins( + string $plugin, + ): void { + [$plugintype, $pluginname] = explode('_', $plugin, 2); + $classname = \core_plugin_manager::resolve_plugininfo_class($plugintype); + + $this->assertFalse($classname::plugintype_supports_ordering()); + + $this->assertNull($classname::get_sorted_plugins()); + $this->assertNull($classname::get_sorted_plugins(true)); + $this->assertNull($classname::get_sorted_plugins(false)); + + $this->assertFalse($classname::change_plugin_order($pluginname, base::MOVE_UP)); + $this->assertFalse($classname::change_plugin_order($pluginname, base::MOVE_DOWN)); + } + + /** + * Data provider for plugins_not_supporting_ordering. + * + * @return string[] + */ + public function plugins_not_supporting_ordering(): array { + return [ + ['mod_assign'], + ['block_login'], + ]; + } } diff --git a/lib/tests/plugininfo/editor_test.php b/lib/tests/plugininfo/editor_test.php index a3d2b863ad5..11072692e82 100644 --- a/lib/tests/plugininfo/editor_test.php +++ b/lib/tests/plugininfo/editor_test.php @@ -65,4 +65,239 @@ class editor_test extends advanced_testcase { $this->assertCount(1, $plugins); $this->assertArrayHasKey('textarea', $plugins); } + + /** + * Ensure that plugintype_supports_ordering() returns true. + */ + public function test_plugintype_supports_ordering(): void { + $this->assertTrue(editor::plugintype_supports_ordering()); + } + + /** + * Ensure that get_sorted_plugins() returns the correct list of plugins. + * + * @dataProvider get_sorted_plugins_provider + * @param string $texteditors The $CFG->texteditors value to use as a base + * @param bool $enabledonly + * @param array $expected The expected order + */ + public function test_get_sorted_plugins( + string $texteditors, + bool $enabledonly, + array $expected, + ): void { + global $CFG; + $this->resetAfterTest(true); + + $CFG->texteditors = $texteditors; + $this->assertSame( + $expected, + array_keys(editor::get_sorted_plugins($enabledonly)), + ); + } + + /** + * Data provider for the get_sorted_plugins tests. + * + * @return array + */ + public function get_sorted_plugins_provider(): array { + return [ + [ + 'texteditors' => 'textarea,tiny', + 'enabledonly' => true, + 'expected' => [ + 'textarea', + 'tiny', + ], + ], + [ + 'texteditors' => 'tiny,textarea', + 'enabledonly' => true, + 'expected' => [ + 'tiny', + 'textarea', + ], + ], + [ + 'texteditors' => 'tiny', + 'enabledonly' => true, + 'expected' => [ + 'tiny', + ], + ], + 'Phantom values are removed from the list' => [ + 'texteditors' => 'fakeeditor', + 'enabledonly' => true, + 'expected' => [ + ], + ], + [ + 'texteditors' => 'textarea,tiny', + 'enabledonly' => false, + 'expected' => [ + 'textarea', + 'tiny', + + // Disabled editors are listed alphabetically at the end. + 'atto', + 'tinymce', + ], + ], + [ + 'texteditors' => 'tiny', + 'enabledonly' => false, + 'expected' => [ + 'tiny', + + // Disabled editors are listed alphabetically at the end. + 'atto', + 'textarea', + 'tinymce', + ], + ], + ]; + } + + /** + * Ensure that change_plugin_order() changes the order of the plugins. + * + * @dataProvider change_plugin_order_provider + * @param string $texteditors + * @param string $pluginname + * @param int $direction + * @param array $neworder + * @param string $newtexteditors + */ + public function test_change_plugin_order( + string $texteditors, + string $pluginname, + int $direction, + array $neworder, + string $newtexteditors, + ): void { + global $CFG; + $this->resetAfterTest(true); + + $CFG->texteditors = $texteditors; + editor::change_plugin_order($pluginname, $direction); + + $this->assertSame( + $neworder, + array_keys(editor::get_sorted_plugins()), + ); + $this->assertSame($newtexteditors, $CFG->texteditors); + } + + /** + * Data provider fro the change_plugin_order() tests. + * + * @return array + */ + public function change_plugin_order_provider(): array { + return [ + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'textarea', + 'direction' => base::MOVE_DOWN, + 'expected' => [ + 'tiny', + 'textarea', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'tiny,textarea', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'tiny', + 'direction' => base::MOVE_DOWN, + // Tiny is already at the bottom of the enabled plugins. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'atto', + 'direction' => base::MOVE_DOWN, + // Atto is not enabled. No change expected. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'tiny', + 'direction' => base::MOVE_UP, + 'expected' => [ + 'tiny', + 'textarea', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'tiny,textarea', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'tiny', + 'direction' => base::MOVE_UP, + // Tiny is already at the top of the enabled plugins. + 'expected' => [ + 'tiny', + 'textarea', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'tiny,textarea', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'atto', + 'direction' => base::MOVE_UP, + // Atto is not enabled. No change expected. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'tinymce', + 'direction' => base::MOVE_UP, + // TinyMCE is not enabled. No change expected. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + [ + 'texteditors' => 'textarea,tiny', + 'pluginname' => 'fakeeditor', + 'direction' => base::MOVE_UP, + // The fakeeditor plugin does not exist. No change expected. + 'expected' => [ + 'textarea', + 'tiny', + 'atto', + 'tinymce', + ], + 'newtexteditors' => 'textarea,tiny', + ], + ]; + } } diff --git a/version.php b/version.php index 467637cedb4..c1d7010490f 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023031000.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023031000.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.2dev (Build: 20230310)'; // Human-friendly version name