From 46db25d734a7fda63cfd46bf88c6e4cc6208b686 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Thu, 22 Oct 2026 08:50:50 +0600 Subject: [PATCH 1/5] Plugins: Update custom plugin group counts after deleting a plugin. Groups added through the `plugins_list` filter now have their count lowered when a plugin in them is deleted, and their tab is removed once they are empty. See #65191. --- src/js/_enqueues/wp/updates.js | 28 ++++++++++++++++++++++ tests/qunit/wp-admin/js/updates.js | 37 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index 9f3b80b843d46..362250a05420c 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -1379,6 +1379,16 @@ * @type {Object} */ plugins = settings.plugins, + /** + * Plugin groups that Core handles on its own. + * + * @type {Array} + */ + coreGroups = [ + 'all', 'search', 'upgrade', 'active', 'inactive', + 'recently_activated', 'mustuse', 'dropins', 'paused', + 'auto-update-enabled', 'auto-update-disabled' + ], remainingCount; // Add a success message after deleting a plugin. @@ -1447,6 +1457,24 @@ } } + // Remove from custom groups added through the `plugins_list` filter. + _.each( _.keys( plugins ), function( group ) { + var $view; + + if ( -1 !== _.indexOf( coreGroups, group ) || ! _.isArray( plugins[ group ] ) || -1 === _.indexOf( plugins[ group ], response.plugin ) ) { + return; + } + + plugins[ group ] = _.without( plugins[ group ], response.plugin ); + $view = $views.find( '.' + $.escapeSelector( group ) ); + + if ( plugins[ group ].length ) { + $view.find( '.count' ).text( '(' + plugins[ group ].length + ')' ); + } else { + $view.remove(); + } + } ); + plugins.all = _.without( plugins.all, response.plugin ); if ( plugins.all.length ) { diff --git a/tests/qunit/wp-admin/js/updates.js b/tests/qunit/wp-admin/js/updates.js index 9d3948811abfd..1921de7be1e55 100644 --- a/tests/qunit/wp-admin/js/updates.js +++ b/tests/qunit/wp-admin/js/updates.js @@ -122,6 +122,43 @@ jQuery( function( $ ) { assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.action, 'delete-plugin' ); assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'jetpack' ); } ); + QUnit.test( 'Deleting a plugin should update the counts of custom plugin groups', function( assert ) { + var plugins = window._wpUpdatesSettings.plugins; + + $( '#qunit-fixture' ).append( + '' + + '' + + '
' + + '' + + '
' + ); + + window._wpUpdatesSettings.plugins = { + all: [ 'custom-a/custom-a.php', 'custom-b/custom-b.php', 'other/other.php' ], + demo_group: [ 'custom-a/custom-a.php', 'custom-b/custom-b.php' ], + single_group: [ 'custom-a/custom-a.php' ] + }; + + // Run the fade out callback right away. + sinon.stub( $.fn, 'fadeOut' ).callsFake( function( speed, callback ) { + return this.each( callback ); + } ); + + wp.updates.deletePluginSuccess( { slug: 'custom-a', plugin: 'custom-a/custom-a.php', pluginName: 'Custom A' } ); + + $.fn.fadeOut.restore(); + + assert.equal( $( '.subsubsub .all .count' ).text(), '(2)', 'The All count should go down.' ); + assert.equal( $( '.subsubsub .demo_group .count' ).text(), '(1)', 'The custom group count should go down.' ); + assert.strictEqual( $( '.subsubsub .single_group' ).length, 0, 'An empty custom group should be removed.' ); + assert.deepEqual( window._wpUpdatesSettings.plugins.demo_group, [ 'custom-b/custom-b.php' ], 'The deleted plugin should be removed from the custom group.' ); + + window._wpUpdatesSettings.plugins = plugins; + } ); // QUnit.test( 'A successful update changes the message?', function( assert ) {} ); // QUnit.test( 'A failed update changes the message?', function( assert ) {} ); From 6ba822dc9630966b458e1650c119112528045a0d Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Thu, 22 Oct 2026 09:12:17 +0600 Subject: [PATCH 2/5] Retrigger CI after the Build WordPress step timed out. From a79fca18744e540021cc8a3b732ca75437adcce5 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Thu, 22 Oct 2026 09:21:06 +0600 Subject: [PATCH 3/5] Plugins: Remove the separator left on the last plugin view after a view is removed. See #65191. --- src/js/_enqueues/wp/updates.js | 8 ++++++++ tests/qunit/wp-admin/js/updates.js | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index 362250a05420c..575fd2c246c9a 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -1389,6 +1389,7 @@ 'recently_activated', 'mustuse', 'dropins', 'paused', 'auto-update-enabled', 'auto-update-disabled' ], + lastViewNode, remainingCount; // Add a success message after deleting a plugin. @@ -1488,6 +1489,13 @@ } } + // When the last view was removed, remove the separator left on the new last view. + lastViewNode = $views.children( 'li' ).last().contents().last().get( 0 ); + + if ( lastViewNode && Node.TEXT_NODE === lastViewNode.nodeType ) { + lastViewNode.nodeValue = lastViewNode.nodeValue.replace( /\s*\|\s*$/, '' ); + } + if ( $itemsCount.length && $currentView.length ) { remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length; $itemsCount.text( diff --git a/tests/qunit/wp-admin/js/updates.js b/tests/qunit/wp-admin/js/updates.js index 1921de7be1e55..f1702af84388a 100644 --- a/tests/qunit/wp-admin/js/updates.js +++ b/tests/qunit/wp-admin/js/updates.js @@ -128,8 +128,8 @@ jQuery( function( $ ) { $( '#qunit-fixture' ).append( '' + '' + '
' + @@ -155,6 +155,8 @@ jQuery( function( $ ) { assert.equal( $( '.subsubsub .all .count' ).text(), '(2)', 'The All count should go down.' ); assert.equal( $( '.subsubsub .demo_group .count' ).text(), '(1)', 'The custom group count should go down.' ); assert.strictEqual( $( '.subsubsub .single_group' ).length, 0, 'An empty custom group should be removed.' ); + assert.strictEqual( $( '.subsubsub .all' ).text(), 'All (2) |', 'Separators between views should be kept.' ); + assert.strictEqual( $( '.subsubsub .demo_group' ).text(), 'Demo Group (1)', 'The new last view should not end with a separator.' ); assert.deepEqual( window._wpUpdatesSettings.plugins.demo_group, [ 'custom-b/custom-b.php' ], 'The deleted plugin should be removed from the custom group.' ); window._wpUpdatesSettings.plugins = plugins; From 84ed2e6323133fcc3a2782a70c936835b8039d54 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Thu, 22 Oct 2026 09:48:54 +0600 Subject: [PATCH 4/5] Plugins: Remove the trailing view separator the same way as `wp.updates.decrementCount()`. Follow-up to [40025]. See #65191. --- src/js/_enqueues/wp/updates.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index 575fd2c246c9a..986c6025aeb43 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -1389,7 +1389,6 @@ 'recently_activated', 'mustuse', 'dropins', 'paused', 'auto-update-enabled', 'auto-update-disabled' ], - lastViewNode, remainingCount; // Add a success message after deleting a plugin. @@ -1489,12 +1488,8 @@ } } - // When the last view was removed, remove the separator left on the new last view. - lastViewNode = $views.children( 'li' ).last().contents().last().get( 0 ); - - if ( lastViewNode && Node.TEXT_NODE === lastViewNode.nodeType ) { - lastViewNode.nodeValue = lastViewNode.nodeValue.replace( /\s*\|\s*$/, '' ); - } + // Remove the trailing separator left on the last view when the view after it was removed. + $views.find( 'li:last' ).html( function() { return $( this ).children(); } ); if ( $itemsCount.length && $currentView.length ) { remainingCount = plugins[ $currentView.parent( 'li' ).attr('class') ].length; From 23e9ea9ca401114f422a2b8a0244a700713a1604 Mon Sep 17 00:00:00 2001 From: Huzaifa Al Mesbah Date: Fri, 23 Oct 2026 13:51:16 +0600 Subject: [PATCH 5/5] Plugins: Only look up custom plugin groups among the view items. A group key such as `count` would otherwise also match the count of every view. See #65191. --- src/js/_enqueues/wp/updates.js | 2 +- tests/qunit/wp-admin/js/updates.js | 33 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index 986c6025aeb43..458da2de79f47 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -1466,7 +1466,7 @@ } plugins[ group ] = _.without( plugins[ group ], response.plugin ); - $view = $views.find( '.' + $.escapeSelector( group ) ); + $view = $views.children( 'li.' + $.escapeSelector( group ) ); if ( plugins[ group ].length ) { $view.find( '.count' ).text( '(' + plugins[ group ].length + ')' ); diff --git a/tests/qunit/wp-admin/js/updates.js b/tests/qunit/wp-admin/js/updates.js index f1702af84388a..500d74a63afed 100644 --- a/tests/qunit/wp-admin/js/updates.js +++ b/tests/qunit/wp-admin/js/updates.js @@ -161,6 +161,39 @@ jQuery( function( $ ) { window._wpUpdatesSettings.plugins = plugins; } ); + QUnit.test( 'Deleting a plugin should only remove the view of an emptied custom group', function( assert ) { + var plugins = window._wpUpdatesSettings.plugins; + + $( '#qunit-fixture' ).append( + '' + + '' + + '
' + + '' + + '
' + ); + + // A group key that matches the class of the count elements. + window._wpUpdatesSettings.plugins = { + all: [ 'custom-a/custom-a.php', 'other/other.php' ], + count: [ 'custom-a/custom-a.php' ] + }; + + sinon.stub( $.fn, 'fadeOut' ).callsFake( function( speed, callback ) { + return this.each( callback ); + } ); + + wp.updates.deletePluginSuccess( { slug: 'custom-a', plugin: 'custom-a/custom-a.php', pluginName: 'Custom A' } ); + + $.fn.fadeOut.restore(); + + assert.strictEqual( $( '.subsubsub > li.count' ).length, 0, 'The emptied group view should be removed.' ); + assert.strictEqual( $( '.subsubsub .all' ).text(), 'All (1)', 'The All view should keep its count.' ); + + window._wpUpdatesSettings.plugins = plugins; + } ); // QUnit.test( 'A successful update changes the message?', function( assert ) {} ); // QUnit.test( 'A failed update changes the message?', function( assert ) {} );