From f8e7bbe776df0897014bb57d0a7a813c1b3967f1 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Mon, 24 Jun 2024 17:44:03 +0100 Subject: [PATCH] MDL-82283 admin: js filtering of the plugins on plugins overview page --- admin/amd/build/plugins_overview.min.js | 11 +++ admin/amd/build/plugins_overview.min.js.map | 1 + admin/amd/src/plugins_overview.js | 82 +++++++++++++++++++ admin/plugins.php | 11 +-- admin/renderer.php | 90 ++++++++++----------- lang/en/plugin.php | 2 + theme/boost/scss/moodle/admin.scss | 7 ++ theme/boost/style/moodle.css | 6 ++ theme/classic/style/moodle.css | 6 ++ 9 files changed, 160 insertions(+), 56 deletions(-) create mode 100644 admin/amd/build/plugins_overview.min.js create mode 100644 admin/amd/build/plugins_overview.min.js.map create mode 100644 admin/amd/src/plugins_overview.js diff --git a/admin/amd/build/plugins_overview.min.js b/admin/amd/build/plugins_overview.min.js new file mode 100644 index 00000000000..d0516941435 --- /dev/null +++ b/admin/amd/build/plugins_overview.min.js @@ -0,0 +1,11 @@ +define("core_admin/plugins_overview",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=function(){const filters=document.querySelectorAll(SELECTORS_PLUGIN_FILTERS),pluginRows=document.querySelectorAll(SELECTORS_PLUGIN_ROWS),pluginTypeRows=document.querySelectorAll(SELECTORS_PLUGIN_TYPE_ROWS),filterPlugins=target=>{const filterBy=target.getAttribute("data-filterby"),headerVisibility={};for(const row of pluginRows){const type=[...row.classList].find((s=>s.startsWith("type-"))),visible="all"===filterBy||row.classList.contains(filterBy);row.style.display=visible?null:"none",visible&&type&&(headerVisibility[type]=!0)}for(const row of pluginTypeRows){const type=[...row.classList].find((s=>s.startsWith("type-")));if(type){const visible="all"===filterBy||headerVisibility[type];row.style.display=visible?null:"none"}}filters.forEach((el=>el.classList.remove("active"))),target.classList.add("active")};if(filters.forEach((target=>target.addEventListener("click",(e=>{e.preventDefault(),window.history.replaceState({},null,e.target.href),filterPlugins(target)})))),window.location.hash.length>1){const anchor=window.location.hash.substring(1),target=[...filters].find((t=>t.getAttribute("data-filterby")===anchor));target&&filterPlugins(target)}}; +/** + * Allows to filter the plugin list on plugins overview page + * + * @module core_admin/plugins_overview + * @copyright 2024 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const SELECTORS_PLUGIN_FILTERS="#plugins-overview-panel [data-filterby]",SELECTORS_PLUGIN_ROWS="table#plugins-control-panel tbody tr:not(.plugintypeheader)",SELECTORS_PLUGIN_TYPE_ROWS="table#plugins-control-panel tbody tr.plugintypeheader"})); + +//# sourceMappingURL=plugins_overview.min.js.map \ No newline at end of file diff --git a/admin/amd/build/plugins_overview.min.js.map b/admin/amd/build/plugins_overview.min.js.map new file mode 100644 index 00000000000..b844f80c7e8 --- /dev/null +++ b/admin/amd/build/plugins_overview.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"plugins_overview.min.js","sources":["../src/plugins_overview.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 * Allows to filter the plugin list on plugins overview page\n *\n * @module core_admin/plugins_overview\n * @copyright 2024 Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n PLUGIN_FILTERS: '#plugins-overview-panel [data-filterby]',\n PLUGIN_ROWS: 'table#plugins-control-panel tbody tr:not(.plugintypeheader)',\n PLUGIN_TYPE_ROWS: 'table#plugins-control-panel tbody tr.plugintypeheader',\n};\n\n/**\n * Initialise filters for the \"Plugins overview\" page\n */\nexport function init() {\n const filters = document.querySelectorAll(SELECTORS.PLUGIN_FILTERS);\n const pluginRows = document.querySelectorAll(SELECTORS.PLUGIN_ROWS);\n const pluginTypeRows = document.querySelectorAll(SELECTORS.PLUGIN_TYPE_ROWS);\n\n const filterPlugins = (target) => {\n const filterBy = target.getAttribute('data-filterby');\n const headerVisibility = {};\n\n // Hide all plugin rows in the plugin table that do not match the filter and show all others.\n for (const row of pluginRows) {\n const type = [...row.classList].find(s => s.startsWith('type-'));\n const visible = filterBy === 'all' ? true : row.classList.contains(filterBy);\n row.style.display = visible ? null : 'none';\n if (visible && type) {\n headerVisibility[type] = true;\n }\n }\n\n // Hide all the plugin type headers that do not have any visible plugins and show all others.\n for (const row of pluginTypeRows) {\n const type = [...row.classList].find(s => s.startsWith('type-'));\n if (type) {\n const visible = filterBy === 'all' || headerVisibility[type];\n row.style.display = visible ? null : 'none';\n }\n }\n\n // Toggle 'active' class for the selected filter.\n filters.forEach(el => el.classList.remove('active'));\n target.classList.add('active');\n };\n\n // Add event listeners for the links changing plugins filters.\n filters\n .forEach(target => target.addEventListener('click', (e) => {\n e.preventDefault();\n window.history.replaceState({}, null, e.target.href);\n filterPlugins(target);\n }));\n\n // Pre-filter plugins based on the current url anchor.\n if (window.location.hash.length > 1) {\n const anchor = window.location.hash.substring(1);\n const target = [...filters].find(t => t.getAttribute('data-filterby') === anchor);\n if (target) {\n filterPlugins(target);\n }\n }\n}\n"],"names":["filters","document","querySelectorAll","SELECTORS","pluginRows","pluginTypeRows","filterPlugins","target","filterBy","getAttribute","headerVisibility","row","type","classList","find","s","startsWith","visible","contains","style","display","forEach","el","remove","add","addEventListener","e","preventDefault","window","history","replaceState","href","location","hash","length","anchor","substring","t"],"mappings":"4JAiCUA,QAAUC,SAASC,iBAAiBC,0BACpCC,WAAaH,SAASC,iBAAiBC,uBACvCE,eAAiBJ,SAASC,iBAAiBC,4BAE3CG,cAAiBC,eACbC,SAAWD,OAAOE,aAAa,iBAC/BC,iBAAmB,OAGpB,MAAMC,OAAOP,WAAY,OACpBQ,KAAO,IAAID,IAAIE,WAAWC,MAAKC,GAAKA,EAAEC,WAAW,WACjDC,QAAuB,QAAbT,UAA4BG,IAAIE,UAAUK,SAASV,UACnEG,IAAIQ,MAAMC,QAAUH,QAAU,KAAO,OACjCA,SAAWL,OACXF,iBAAiBE,OAAQ,OAK5B,MAAMD,OAAON,eAAgB,OACxBO,KAAO,IAAID,IAAIE,WAAWC,MAAKC,GAAKA,EAAEC,WAAW,cACnDJ,KAAM,OACAK,QAAuB,QAAbT,UAAsBE,iBAAiBE,MACvDD,IAAIQ,MAAMC,QAAUH,QAAU,KAAO,QAK7CjB,QAAQqB,SAAQC,IAAMA,GAAGT,UAAUU,OAAO,YAC1ChB,OAAOM,UAAUW,IAAI,cAIzBxB,QACCqB,SAAQd,QAAUA,OAAOkB,iBAAiB,SAAUC,IACjDA,EAAEC,iBACFC,OAAOC,QAAQC,aAAa,GAAI,KAAMJ,EAAEnB,OAAOwB,MAC/CzB,cAAcC,aAIdqB,OAAOI,SAASC,KAAKC,OAAS,EAAG,OAC3BC,OAASP,OAAOI,SAASC,KAAKG,UAAU,GACxC7B,OAAS,IAAIP,SAASc,MAAKuB,GAAKA,EAAE5B,aAAa,mBAAqB0B,SACtE5B,QACAD,cAAcC;;;;;;;;MAvDpBJ,yBACc,0CADdA,sBAEW,8DAFXA,2BAGgB"} \ No newline at end of file diff --git a/admin/amd/src/plugins_overview.js b/admin/amd/src/plugins_overview.js new file mode 100644 index 00000000000..d5126f2c8ad --- /dev/null +++ b/admin/amd/src/plugins_overview.js @@ -0,0 +1,82 @@ +// 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 . + +/** + * Allows to filter the plugin list on plugins overview page + * + * @module core_admin/plugins_overview + * @copyright 2024 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +const SELECTORS = { + PLUGIN_FILTERS: '#plugins-overview-panel [data-filterby]', + PLUGIN_ROWS: 'table#plugins-control-panel tbody tr:not(.plugintypeheader)', + PLUGIN_TYPE_ROWS: 'table#plugins-control-panel tbody tr.plugintypeheader', +}; + +/** + * Initialise filters for the "Plugins overview" page + */ +export function init() { + const filters = document.querySelectorAll(SELECTORS.PLUGIN_FILTERS); + const pluginRows = document.querySelectorAll(SELECTORS.PLUGIN_ROWS); + const pluginTypeRows = document.querySelectorAll(SELECTORS.PLUGIN_TYPE_ROWS); + + const filterPlugins = (target) => { + const filterBy = target.getAttribute('data-filterby'); + const headerVisibility = {}; + + // Hide all plugin rows in the plugin table that do not match the filter and show all others. + for (const row of pluginRows) { + const type = [...row.classList].find(s => s.startsWith('type-')); + const visible = filterBy === 'all' ? true : row.classList.contains(filterBy); + row.style.display = visible ? null : 'none'; + if (visible && type) { + headerVisibility[type] = true; + } + } + + // Hide all the plugin type headers that do not have any visible plugins and show all others. + for (const row of pluginTypeRows) { + const type = [...row.classList].find(s => s.startsWith('type-')); + if (type) { + const visible = filterBy === 'all' || headerVisibility[type]; + row.style.display = visible ? null : 'none'; + } + } + + // Toggle 'active' class for the selected filter. + filters.forEach(el => el.classList.remove('active')); + target.classList.add('active'); + }; + + // Add event listeners for the links changing plugins filters. + filters + .forEach(target => target.addEventListener('click', (e) => { + e.preventDefault(); + window.history.replaceState({}, null, e.target.href); + filterPlugins(target); + })); + + // Pre-filter plugins based on the current url anchor. + if (window.location.hash.length > 1) { + const anchor = window.location.hash.substring(1); + const target = [...filters].find(t => t.getAttribute('data-filterby') === anchor); + if (target) { + filterPlugins(target); + } + } +} diff --git a/admin/plugins.php b/admin/plugins.php index f4d3d0c5cd9..98a250be162 100644 --- a/admin/plugins.php +++ b/admin/plugins.php @@ -29,8 +29,6 @@ require_once($CFG->libdir . '/adminlib.php'); require_once($CFG->libdir . '/filelib.php'); $fetchupdates = optional_param('fetchupdates', false, PARAM_BOOL); // Check for available plugins updates. -$updatesonly = optional_param('updatesonly', false, PARAM_BOOL); // Show updateable plugins only. -$contribonly = optional_param('contribonly', false, PARAM_BOOL); // Show additional plugins only. $uninstall = optional_param('uninstall', '', PARAM_COMPONENT); // Uninstall the plugin. $delete = optional_param('delete', '', PARAM_COMPONENT); // Delete the plugin folder after it is uninstalled. $confirmed = optional_param('confirm', false, PARAM_BOOL); // Confirm the uninstall/delete action. @@ -47,8 +45,7 @@ require_admin(); $syscontext = context_system::instance(); // URL params we want to maintain on redirects. -$pageparams = array('updatesonly' => $updatesonly, 'contribonly' => $contribonly); -$pageurl = new moodle_url('/admin/plugins.php', $pageparams); +$pageurl = new moodle_url('/admin/plugins.php'); $pluginman = core_plugin_manager::instance(); @@ -57,7 +54,7 @@ $PAGE->set_primary_active_tab('siteadminnode'); if ($uninstall) { if (!$confirmed) { - admin_externalpage_setup('pluginsoverview', '', $pageparams); + admin_externalpage_setup('pluginsoverview'); } else { $PAGE->set_url($pageurl); $PAGE->set_context($syscontext); @@ -201,7 +198,7 @@ if ($installupdate and $installupdateversion) { } } -admin_externalpage_setup('pluginsoverview', '', $pageparams); +admin_externalpage_setup('pluginsoverview'); /** @var core_admin_renderer $output */ $output = $PAGE->get_renderer('core', 'admin'); @@ -214,4 +211,4 @@ if ($fetchupdates) { redirect($PAGE->url); } -echo $output->plugin_management_page($pluginman, $checker, $pageparams); +echo $output->plugin_management_page($pluginman, $checker); diff --git a/admin/renderer.php b/admin/renderer.php index 38a2fcc501d..ae2aeaba984 100644 --- a/admin/renderer.php +++ b/admin/renderer.php @@ -1673,8 +1673,9 @@ class core_admin_renderer extends plugin_renderer_base { public function plugins_overview_panel(core_plugin_manager $pluginman, array $options = array()) { $plugininfo = $pluginman->get_plugins(); + $this->page->requires->js_call_amd('core_admin/plugins_overview', 'init'); - $numtotal = $numextension = $numupdatable = $numinstallable = 0; + $numtotal = $numextension = $numupdatable = $numinstallable = $nummissing = $numnew = 0; foreach ($plugininfo as $type => $plugins) { foreach ($plugins as $name => $plugin) { @@ -1688,8 +1689,12 @@ class core_admin_renderer extends plugin_renderer_base { } } if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) { + $nummissing++; continue; } + if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_NEW) { + $numnew++; + } $numtotal++; if (!$plugin->is_standard()) { $numextension++; @@ -1698,36 +1703,50 @@ class core_admin_renderer extends plugin_renderer_base { } $infoall = html_writer::link( - new moodle_url($this->page->url, array('contribonly' => 0, 'updatesonly' => 0)), + $this->page->url, get_string('overviewall', 'core_plugin'), - array('title' => get_string('filterall', 'core_plugin')) + ['title' => get_string('filterall', 'core_plugin'), 'data-filterby' => 'all', 'class' => 'active'] ).' '.html_writer::span($numtotal, 'badge number number-all'); $infoext = html_writer::link( - new moodle_url($this->page->url, array('contribonly' => 1, 'updatesonly' => 0)), + new moodle_url($this->page->url, [], 'additional'), get_string('overviewext', 'core_plugin'), - array('title' => get_string('filtercontribonly', 'core_plugin')) + ['title' => get_string('filtercontribonly', 'core_plugin'), 'data-filterby' => 'additional'] ).' '.html_writer::span($numextension, 'badge number number-additional'); if ($numupdatable) { $infoupdatable = html_writer::link( - new moodle_url($this->page->url, array('contribonly' => 0, 'updatesonly' => 1)), + new moodle_url($this->page->url, [], 'updatable'), get_string('overviewupdatable', 'core_plugin'), - array('title' => get_string('filterupdatesonly', 'core_plugin')) + ['title' => get_string('filterupdatesonly', 'core_plugin'), 'data-filterby' => 'updatable'] ).' '.html_writer::span($numupdatable, 'badge bg-info text-white number number-updatable'); } else { // No updates, or the notifications disabled. $infoupdatable = ''; } - $out = html_writer::start_div('', array('id' => 'plugins-overview-panel')); - - if (!empty($options['updatesonly'])) { - $out .= $this->output->heading(get_string('overviewupdatable', 'core_plugin'), 3); - } else if (!empty($options['contribonly'])) { - $out .= $this->output->heading(get_string('overviewext', 'core_plugin'), 3); + if ($numnew) { + $infonew = html_writer::link( + new moodle_url($this->page->url, [], 'newplugin'), + get_string('status_new', 'plugin'), + ['title' => get_string('filternewpluginsonly', 'core_plugin'), 'data-filterby' => 'newplugin'] + ).' '.html_writer::span($numnew, 'badge bg-success text-white number number-newplugin'); + } else { + $infonew = ''; } + if ($nummissing) { + $infomissing = html_writer::link( + new moodle_url($this->page->url, [], 'missing'), + get_string('status_missing', 'plugin'), + ['title' => get_string('filtermissingonly', 'core_plugin'), 'data-filterby' => 'missing'] + ).' '.html_writer::span($nummissing, 'badge bg-danger text-white number number-missing'); + } else { + $infomissing = ''; + } + + $out = html_writer::start_div('', ['id' => 'plugins-overview-panel']); + if ($numinstallable) { $out .= $this->output->single_button( new moodle_url($this->page->url, array('installupdatex' => 1)), @@ -1738,8 +1757,10 @@ class core_admin_renderer extends plugin_renderer_base { } $out .= html_writer::div($infoall, 'info info-all'). - html_writer::div($infoext, 'info info-ext'). - html_writer::div($infoupdatable, 'info info-updatable'); + html_writer::div($infoext, 'info info-additional'). + html_writer::div($infoupdatable, 'info info-updatable'). + html_writer::div($infonew, 'info info-newplugin'). + html_writer::div($infomissing, 'info info-missing'); $out .= html_writer::end_div(); // End of #plugins-overview-panel block. @@ -1759,38 +1780,6 @@ class core_admin_renderer extends plugin_renderer_base { $plugininfo = $pluginman->get_plugins(); - // Filter the list of plugins according the options. - if (!empty($options['updatesonly'])) { - $updateable = array(); - foreach ($plugininfo as $plugintype => $pluginnames) { - foreach ($pluginnames as $pluginname => $pluginfo) { - $pluginavailableupdates = $pluginfo->available_updates(); - if (!empty($pluginavailableupdates)) { - foreach ($pluginavailableupdates as $pluginavailableupdate) { - $updateable[$plugintype][$pluginname] = $pluginfo; - } - } - } - } - $plugininfo = $updateable; - } - - if (!empty($options['contribonly'])) { - $contribs = array(); - foreach ($plugininfo as $plugintype => $pluginnames) { - foreach ($pluginnames as $pluginname => $pluginfo) { - if (!$pluginfo->is_standard()) { - $contribs[$plugintype][$pluginname] = $pluginfo; - } - } - } - $plugininfo = $contribs; - } - - if (empty($plugininfo)) { - return ''; - } - $table = new html_table(); $table->id = 'plugins-control-panel'; $table->head = array( @@ -1888,13 +1877,15 @@ class core_admin_renderer extends plugin_renderer_base { $row->attributes['class'] .= ' standard'; $source = ''; } else { - $row->attributes['class'] .= ' extension'; - $source = html_writer::div(get_string('sourceext', 'core_plugin'), 'source badge bg-info text-white'); + $row->attributes['class'] .= ' additional'; + $source = html_writer::div(get_string('sourceext', 'core_plugin'), 'source badge mr-1 bg-info text-white'); } if ($status === core_plugin_manager::PLUGIN_STATUS_MISSING) { + $row->attributes['class'] .= ' missing'; $msg = html_writer::div(get_string('status_missing', 'core_plugin'), 'statusmsg badge bg-danger text-white'); } else if ($status === core_plugin_manager::PLUGIN_STATUS_NEW) { + $row->attributes['class'] .= ' newplugin'; $msg = html_writer::div(get_string('status_new', 'core_plugin'), 'statusmsg badge bg-success text-white'); } else { $msg = ''; @@ -1910,6 +1901,7 @@ class core_admin_renderer extends plugin_renderer_base { $updateinfo = ''; if (is_array($plugin->available_updates())) { + $row->attributes['class'] .= ' updatable'; foreach ($plugin->available_updates() as $availableupdate) { $updateinfo .= $this->plugin_available_update_info($pluginman, $availableupdate); } diff --git a/lang/en/plugin.php b/lang/en/plugin.php index 656cd55c520..914b4f7d65c 100644 --- a/lang/en/plugin.php +++ b/lang/en/plugin.php @@ -53,6 +53,8 @@ $string['err_response_format_version'] = 'Unexpected version of the response for $string['err_response_http_code'] = 'Unable to fetch available updates data - unexpected HTTP response code.'; $string['filterall'] = 'Show all'; $string['filtercontribonly'] = 'Show additional plugins only'; +$string['filtermissingonly'] = 'Show missing from disk only'; +$string['filternewpluginsonly'] = 'Show only plugins to be installed'; $string['filterupdatesonly'] = 'Show updateable only'; $string['incompatibleversion'] = 'Incompatible Moodle version: {$a}'; $string['isenabled'] = 'Enabled?'; diff --git a/theme/boost/scss/moodle/admin.scss b/theme/boost/scss/moodle/admin.scss index 3f7fc109b3b..65cc85f109c 100644 --- a/theme/boost/scss/moodle/admin.scss +++ b/theme/boost/scss/moodle/admin.scss @@ -438,6 +438,13 @@ .info { display: inline-block; margin-right: 1em; + + [data-filterby].active { + font-weight: bold; + } + } + .updateavailableinstallall { + margin-right: 1em; } } diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 3446ea52216..58a38ffcc69 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -26876,6 +26876,12 @@ img.icon { display: inline-block; margin-right: 1em; } +#page-admin-plugins #plugins-overview-panel .info [data-filterby].active { + font-weight: bold; +} +#page-admin-plugins #plugins-overview-panel .updateavailableinstallall { + margin-right: 1em; +} #page-admin-plugins .checkforupdates { margin: 10px 0; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 4ab448104a5..38ba75af2ca 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -26876,6 +26876,12 @@ img.icon { display: inline-block; margin-right: 1em; } +#page-admin-plugins #plugins-overview-panel .info [data-filterby].active { + font-weight: bold; +} +#page-admin-plugins #plugins-overview-panel .updateavailableinstallall { + margin-right: 1em; +} #page-admin-plugins .checkforupdates { margin: 10px 0; }