From dd67b5d40fb438a9197f8c95e404b2833ea0ef55 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 9 Apr 2026 17:01:44 +0530 Subject: [PATCH 1/2] Taxonomy: Update item count dynamically after adding or deleting a term.Fixes #50082. --- src/js/_enqueues/admin/tags.js | 52 ++++++++++---- src/wp-admin/includes/ajax-actions.php | 8 ++- tests/phpunit/tests/ajax/wpAjaxAddTag.php | 35 +++++++++ tests/phpunit/tests/ajax/wpAjaxDeleteTag.php | 76 ++++++++++++++++++++ 4 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 tests/phpunit/tests/ajax/wpAjaxDeleteTag.php diff --git a/src/js/_enqueues/admin/tags.js b/src/js/_enqueues/admin/tags.js index ff7761adb8d3e..fac5fed159683 100644 --- a/src/js/_enqueues/admin/tags.js +++ b/src/js/_enqueues/admin/tags.js @@ -13,6 +13,24 @@ jQuery( function($) { var addingTerm = false; + /** + * Updates the displaying-num span with the new total item count. + * + * @since 6.8.0 + * + * @param {number} total The new total number of items. + * @return {void} + */ + function updateDisplayingNum( total ) { + $( '.displaying-num' ).text( + wp.i18n.sprintf( + /* translators: %s: Number of items. */ + wp.i18n._n( '%s item', '%s items', total ), + total.toLocaleString() + ) + ); + } + /** * Adds an event handler to the delete term link on the term overview page. * @@ -45,10 +63,11 @@ jQuery( function($) { * * @return {void} */ - $.post(ajaxurl, data, function(r){ + $.post( ajaxurl, data, function( r ) { var message; - if ( '1' == r ) { - $('#ajax-response').empty(); + + if ( r && r.success ) { + $( '#ajax-response' ).empty(); let nextFocus = tr.next( 'tr' ).find( 'a.row-title' ); let prevFocus = tr.prev( 'tr' ).find( 'a.row-title' ); // If there is neither a next row or a previous row, focus the tag input field. @@ -60,7 +79,7 @@ jQuery( function($) { } } - tr.fadeOut('normal', function(){ tr.remove(); }); + tr.fadeOut( 'normal', function() { tr.remove(); } ); /** * Removes the term from the parent box and the tag cloud. @@ -69,23 +88,25 @@ jQuery( function($) { * This term ID is then used to select the relevant HTML elements: * The parent box and the tag cloud. */ - $('select#parent option[value="' + data.match(/tag_ID=(\d+)/)[1] + '"]').remove(); - $('a.tag-link-' + data.match(/tag_ID=(\d+)/)[1]).remove(); + $( 'select#parent option[value="' + data.match( /tag_ID=(\d+)/ )[1] + '"]' ).remove(); + $( 'a.tag-link-' + data.match( /tag_ID=(\d+)/ )[1] ).remove(); nextFocus.trigger( 'focus' ); + updateDisplayingNum( r.data.total ); message = wp.i18n.__( 'The selected tag has been deleted.' ); - - } else if ( '-1' == r ) { + + } else if ( '-1' === r ) { message = wp.i18n.__( 'Sorry, you are not allowed to do that.' ); - $('#ajax-response').empty().append('

' + message + '

'); + $( '#ajax-response' ).empty().append( '

' + message + '

' ); resetRowAfterFailure( tr ); } else { message = wp.i18n.__( 'An error occurred while processing your request. Please try again later.' ); - $('#ajax-response').empty().append('

' + message + '

'); + $( '#ajax-response' ).empty().append( '

' + message + '

' ); resetRowAfterFailure( tr ); } + wp.a11y.speak( message, 'assertive' ); - }); + } ); } return false; @@ -179,19 +200,22 @@ jQuery( function($) { $('.tags .no-items').remove(); - if ( form.find('select#parent') ) { + if ( form.find( 'select#parent' ) ) { // Parents field exists, Add new term to the list. term = res.responses[1].supplemental; // Create an indent for the Parent field. indent = ''; - for ( i = 0; i < res.responses[1].position; i++ ) + for ( i = 0; i < res.responses[1].position; i++ ) { indent += '   '; + } form.find( 'select#parent option:selected' ).after( '' ); } - $('input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):visible, textarea:visible', form).val(''); + updateDisplayingNum( res.responses[0].supplemental.total ); + + $( 'input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):visible, textarea:visible', form ).val( '' ); }); return false; diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 2af08fba70af9..80655c70ff2b2 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -805,9 +805,10 @@ function wp_ajax_delete_tag() { } if ( wp_delete_term( $tag_id, $taxonomy ) ) { - wp_die( 1 ); + $total = wp_count_terms( array( 'taxonomy' => $taxonomy ) ); + wp_send_json_success( array( 'total' => (int) $total ) ); } else { - wp_die( 0 ); + wp_send_json_error(); } } @@ -1153,6 +1154,8 @@ function wp_ajax_add_tag() { $message = $messages['_item'][1]; } + $total = wp_count_terms( array( 'taxonomy' => $taxonomy ) ); + $response->add( array( 'what' => 'taxonomy', @@ -1161,6 +1164,7 @@ function wp_ajax_add_tag() { 'parents' => $parents, 'noparents' => $no_parents, 'notice' => $message, + 'total' => (int) $total, ), ) ); diff --git a/tests/phpunit/tests/ajax/wpAjaxAddTag.php b/tests/phpunit/tests/ajax/wpAjaxAddTag.php index fbb65ff6aec84..85b695f8ade54 100755 --- a/tests/phpunit/tests/ajax/wpAjaxAddTag.php +++ b/tests/phpunit/tests/ajax/wpAjaxAddTag.php @@ -139,6 +139,41 @@ public function test_adding_existing_category_should_error() { $this->assertSame( $expected, (string) $this->get_xml_response_taxonomy()->wp_error ); } + /** + * Tests that the add tag AJAX response includes the total term count. + * + * @ticket 50082 + * + * @covers ::wp_ajax_add_tag + * @covers ::wp_count_terms + */ + public function test_add_tag_returns_total_count() { + $this->_setRole( 'administrator' ); + + wp_insert_term( 'existing-tag', 'post_tag' ); + + $_POST = array( + 'taxonomy' => 'post_tag', + 'post_type' => 'post', + 'screen' => 'edit-post_tag', + 'action' => 'add-tag', + 'tag-name' => 'new-tag', + '_wpnonce_add-tag' => wp_create_nonce( 'add-tag' ), + ); + + try { + $this->_handleAjax( 'add-tag' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + $xml = simplexml_load_string( $this->_last_response, 'SimpleXMLElement', LIBXML_NOCDATA ); + $total = (int) $xml->response->taxonomy->supplemental->total; + + // Two terms now exist: 'existing-tag' and 'new-tag'. + $this->assertSame( 2, $total ); + } + /** * Helper method to get the taxonomy's response or error. * diff --git a/tests/phpunit/tests/ajax/wpAjaxDeleteTag.php b/tests/phpunit/tests/ajax/wpAjaxDeleteTag.php new file mode 100644 index 0000000000000..6e87308f0fec5 --- /dev/null +++ b/tests/phpunit/tests/ajax/wpAjaxDeleteTag.php @@ -0,0 +1,76 @@ +_setRole( 'administrator' ); + + $term = wp_insert_term( 'tag-to-delete', 'post_tag' ); + wp_insert_term( 'tag-to-keep', 'post_tag' ); + + $_POST = array( + 'action' => 'delete-tag', + 'tag_ID' => $term['term_id'], + 'taxonomy' => 'post_tag', + '_wpnonce' => wp_create_nonce( 'delete-tag_' . $term['term_id'] ), + ); + + try { + $this->_handleAjax( 'delete-tag' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + $response = json_decode( $this->_last_response, true ); + + $this->assertTrue( $response['success'] ); + // One term remains after deletion. + $this->assertSame( 1, $response['data']['total'] ); + } + + /** + * Tests that deleting a tag without permission returns -1. + * + * @ticket 50082 + * + * @covers ::wp_ajax_delete_tag + */ + public function test_delete_tag_without_capability_should_error() { + $this->_setRole( 'subscriber' ); + + $term = self::factory()->term->create_and_get( + array( 'taxonomy' => 'post_tag' ) + ); + + $_POST = array( + 'action' => 'delete-tag', + 'tag_ID' => $term->term_id, + 'taxonomy' => 'post_tag', + '_wpnonce' => wp_create_nonce( 'delete-tag_' . $term->term_id ), + ); + + $this->expectException( 'WPAjaxDieStopException' ); + $this->expectExceptionMessage( '-1' ); + $this->_handleAjax( 'delete-tag' ); + } +} From fe69670095a5ed1ff78b9ac09320019143b4a3f4 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Apr 2026 12:30:19 +0530 Subject: [PATCH 2/2] Retrigger CI