From 102b5bc59a48258e6b1b04eb1ea27bf948686ede Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Fri, 30 Jan 2026 14:56:43 -0600 Subject: [PATCH 01/13] Taxonomy: Add UI to Category page to indicate default category. Shows the default category first in the list and adds a "Default" label next to its name in the categories admin screen. See https://core.trac.wordpress.org/ticket/26268 --- .../includes/class-wp-terms-list-table.php | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 7537a499974d5..71c207454cd23 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -242,6 +242,15 @@ public function display_rows_or_placeholder() { return; } + // Handle custom display of default category by showing it first in the list. + $default_category = false; + if ( 'category' === $taxonomy ) { + $default_category = get_term( get_option( 'default_category' ), 'category' ); + if ( ! $default_category || is_wp_error( $default_category ) ) { + $default_category = false; + } + } + if ( is_taxonomy_hierarchical( $taxonomy ) && ! isset( $this->callback_args['orderby'] ) ) { if ( ! empty( $this->callback_args['search'] ) ) {// Ignore children on searches. $children = array(); @@ -249,13 +258,24 @@ public function display_rows_or_placeholder() { $children = _get_term_hierarchy( $taxonomy ); } + if ( $default_category ) { + $this->single_row( $default_category ); + } + /* * Some funky recursion to get the job done (paging & parents mainly) is contained within. * Skip it for non-hierarchical taxonomies for performance sake. */ $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count ); } else { + if ( $default_category ) { + $this->single_row( $default_category ); + } + foreach ( $this->items as $term ) { + if ( $default_category && $default_category->term_id === $term->term_id ) { + continue; + } $this->single_row( $term ); } } @@ -275,6 +295,12 @@ private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$coun $end = $start + $per_page; + // Handle display of default category by showing it first in the list, capture default category id. + $default_category_id = false; + if ( 'category' === $taxonomy ) { + $default_category_id = (int) get_option( 'default_category' ); + } + foreach ( $terms as $key => $term ) { if ( $count >= $end ) { @@ -285,6 +311,11 @@ private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$coun continue; } + // Skip duplicating display of default category. + if ( $default_category_id && $default_category_id === $term->term_id ) { + continue; + } + // If the page starts in a subtree, print the parents. if ( $count === $start && $term->parent > 0 && empty( $_REQUEST['s'] ) ) { $my_parents = array(); @@ -383,6 +414,12 @@ public function column_cb( $item ) { public function column_name( $tag ) { $taxonomy = $this->screen->taxonomy; + $default_term = get_option( 'default_' . $taxonomy ); + $default_term_label = ''; + if ( $tag->term_id == $default_term ) { + $default_term_label = ' — ' . __( 'Default' ) . ''; + } + $pad = str_repeat( '— ', max( 0, $this->level ) ); /** @@ -422,8 +459,9 @@ public function column_name( $tag ) { } $output = sprintf( - '%s
', - $name + '%s%s
', + $name, + $default_term_label ); /** This filter is documented in wp-admin/includes/class-wp-terms-list-table.php */ From 4dc573efaa532aea6023f7bb6bca5a271e0c02af Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Mon, 2 Feb 2026 12:12:38 -0600 Subject: [PATCH 02/13] Taxonomy: Fix pagination and optimize default category handling. - Only display pinned default category on first page to maintain correct per-page count - Pass default_category_id as parameter to _rows() to avoid repeated get_option() calls during recursion --- .../includes/class-wp-terms-list-table.php | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 71c207454cd23..a5f3e6daaf4af 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -243,11 +243,14 @@ public function display_rows_or_placeholder() { } // Handle custom display of default category by showing it first in the list. - $default_category = false; + $default_category = false; + $default_category_id = 0; if ( 'category' === $taxonomy ) { - $default_category = get_term( get_option( 'default_category' ), 'category' ); + $default_category_id = (int) get_option( 'default_category' ); + $default_category = get_term( $default_category_id, 'category' ); if ( ! $default_category || is_wp_error( $default_category ) ) { - $default_category = false; + $default_category = false; + $default_category_id = 0; } } @@ -258,7 +261,8 @@ public function display_rows_or_placeholder() { $children = _get_term_hierarchy( $taxonomy ); } - if ( $default_category ) { + // Only show pinned default category on the first page. + if ( $default_category && 0 === $offset ) { $this->single_row( $default_category ); } @@ -266,14 +270,15 @@ public function display_rows_or_placeholder() { * Some funky recursion to get the job done (paging & parents mainly) is contained within. * Skip it for non-hierarchical taxonomies for performance sake. */ - $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count ); + $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count, 0, 0, $default_category_id ); } else { - if ( $default_category ) { + // Only show pinned default category on the first page. + if ( $default_category && 0 === $offset ) { $this->single_row( $default_category ); } foreach ( $this->items as $term ) { - if ( $default_category && $default_category->term_id === $term->term_id ) { + if ( $default_category_id && $default_category_id === $term->term_id ) { continue; } $this->single_row( $term ); @@ -290,17 +295,12 @@ public function display_rows_or_placeholder() { * @param int $count * @param int $parent_term * @param int $level + * @param int $default_category_id */ - private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$count, $parent_term = 0, $level = 0 ) { + private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$count, $parent_term = 0, $level = 0, $default_category_id = 0 ) { $end = $start + $per_page; - // Handle display of default category by showing it first in the list, capture default category id. - $default_category_id = false; - if ( 'category' === $taxonomy ) { - $default_category_id = (int) get_option( 'default_category' ); - } - foreach ( $terms as $key => $term ) { if ( $count >= $end ) { @@ -355,7 +355,7 @@ private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$coun unset( $terms[ $key ] ); if ( isset( $children[ $term->term_id ] ) && empty( $_REQUEST['s'] ) ) { - $this->_rows( $taxonomy, $terms, $children, $start, $per_page, $count, $term->term_id, $level + 1 ); + $this->_rows( $taxonomy, $terms, $children, $start, $per_page, $count, $term->term_id, $level + 1, $default_category_id ); } } } From b7e0e1a8d0fbd9008239d63839a9efbb18fefadd Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Mon, 2 Feb 2026 12:24:01 -0600 Subject: [PATCH 03/13] Taxonomy: Add "Change Default" row action for default category. - Add "Change Default" link to row actions for the default category - Add id attribute to Writing Settings default category row for fragment linking --- src/wp-admin/includes/class-wp-terms-list-table.php | 8 ++++++++ src/wp-admin/options-writing.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index a5f3e6daaf4af..fab353abdd938 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -570,6 +570,14 @@ protected function handle_row_actions( $item, $column_name, $primary ) { ); } + if ( 'category' === $taxonomy && $tag->term_id === (int) get_option( 'default_category' ) ) { + $actions['change-default'] = sprintf( + '%s', + admin_url( 'options-writing.php#default-category-row' ), + __( 'Change Default' ) + ); + } + /** * Filters the action links displayed for each term in the Tags list table. * diff --git a/src/wp-admin/options-writing.php b/src/wp-admin/options-writing.php index 6f85b54679327..dd0b64e1c64fe 100644 --- a/src/wp-admin/options-writing.php +++ b/src/wp-admin/options-writing.php @@ -78,7 +78,7 @@ - + Date: Mon, 2 Feb 2026 12:42:27 -0600 Subject: [PATCH 04/13] fix: Taxonomy: Fix Yoda condition in default category check. --- src/wp-admin/includes/class-wp-terms-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index fab353abdd938..5a73fb80232e4 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -570,7 +570,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { ); } - if ( 'category' === $taxonomy && $tag->term_id === (int) get_option( 'default_category' ) ) { + if ( 'category' === $taxonomy && (int) get_option( 'default_category' ) === $tag->term_id ) { $actions['change-default'] = sprintf( '%s', admin_url( 'options-writing.php#default-category-row' ), From c985c34a80f09af1045e414b271b254b85ed056f Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Mon, 2 Feb 2026 14:11:43 -0600 Subject: [PATCH 05/13] Taxonomy: Add helpful links in default category help text. Update the help text below the Categories list table to include links for renaming the default category and choosing a different default. --- src/wp-admin/edit-tags.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wp-admin/edit-tags.php b/src/wp-admin/edit-tags.php index 6efdeb9b0527d..6c5b7158c08af 100644 --- a/src/wp-admin/edit-tags.php +++ b/src/wp-admin/edit-tags.php @@ -627,11 +627,14 @@

renamed or you can choose a different default category.' ), /** This filter is documented in wp-includes/category-template.php */ - '' . apply_filters( 'the_category', get_cat_name( get_option( 'default_category' ) ), '', '' ) . '' + '' . apply_filters( 'the_category', get_cat_name( $default_category_id ), '', '' ) . '', + esc_url( get_edit_term_link( $default_category_id, 'category' ) ), + esc_url( admin_url( 'options-writing.php#default-category-row' ) ) ); ?>

From 52e5831c2adf50ad811cd8715b8bc496e3ca515a Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Tue, 3 Feb 2026 09:56:16 -0600 Subject: [PATCH 06/13] Apply suggestions from code review Co-authored-by: Weston Ruter --- src/wp-admin/edit-tags.php | 2 +- .../includes/class-wp-terms-list-table.php | 17 +++++++---------- src/wp-admin/options-writing.php | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/wp-admin/edit-tags.php b/src/wp-admin/edit-tags.php index 6c5b7158c08af..c8aa12a651021 100644 --- a/src/wp-admin/edit-tags.php +++ b/src/wp-admin/edit-tags.php @@ -634,7 +634,7 @@ /** This filter is documented in wp-includes/category-template.php */ '' . apply_filters( 'the_category', get_cat_name( $default_category_id ), '', '' ) . '', esc_url( get_edit_term_link( $default_category_id, 'category' ) ), - esc_url( admin_url( 'options-writing.php#default-category-row' ) ) + esc_url( admin_url( 'options-writing.php#default_category' ) ) ); ?>

diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 5a73fb80232e4..f42e11ea76298 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -243,14 +243,11 @@ public function display_rows_or_placeholder() { } // Handle custom display of default category by showing it first in the list. - $default_category = false; - $default_category_id = 0; + $default_category = null; if ( 'category' === $taxonomy ) { - $default_category_id = (int) get_option( 'default_category' ); - $default_category = get_term( $default_category_id, 'category' ); - if ( ! $default_category || is_wp_error( $default_category ) ) { - $default_category = false; - $default_category_id = 0; + $default_category = get_term( (int) get_option( 'default_category' ), 'category' ); + if ( ! ( $default_category instanceof WP_Term ) ) { + $default_category = null; } } @@ -270,7 +267,7 @@ public function display_rows_or_placeholder() { * Some funky recursion to get the job done (paging & parents mainly) is contained within. * Skip it for non-hierarchical taxonomies for performance sake. */ - $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count, 0, 0, $default_category_id ); + $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count, 0, 0, $default_category->term_id ); } else { // Only show pinned default category on the first page. if ( $default_category && 0 === $offset ) { @@ -278,7 +275,7 @@ public function display_rows_or_placeholder() { } foreach ( $this->items as $term ) { - if ( $default_category_id && $default_category_id === $term->term_id ) { + if ( $default_category && $default_category->term_id === $term->term_id ) { continue; } $this->single_row( $term ); @@ -573,7 +570,7 @@ protected function handle_row_actions( $item, $column_name, $primary ) { if ( 'category' === $taxonomy && (int) get_option( 'default_category' ) === $tag->term_id ) { $actions['change-default'] = sprintf( '%s', - admin_url( 'options-writing.php#default-category-row' ), + admin_url( 'options-writing.php#default_category' ), __( 'Change Default' ) ); } diff --git a/src/wp-admin/options-writing.php b/src/wp-admin/options-writing.php index dd0b64e1c64fe..6f85b54679327 100644 --- a/src/wp-admin/options-writing.php +++ b/src/wp-admin/options-writing.php @@ -78,7 +78,7 @@ - + Date: Tue, 3 Feb 2026 10:09:32 -0600 Subject: [PATCH 07/13] fix: Taxonomy: Fix null dereference for hierarchical taxonomies. --- src/wp-admin/includes/class-wp-terms-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index f42e11ea76298..8a2cd638b11f1 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -267,7 +267,7 @@ public function display_rows_or_placeholder() { * Some funky recursion to get the job done (paging & parents mainly) is contained within. * Skip it for non-hierarchical taxonomies for performance sake. */ - $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count, 0, 0, $default_category->term_id ); + $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count, 0, 0, $default_category ? $default_category->term_id : 0 ); } else { // Only show pinned default category on the first page. if ( $default_category && 0 === $offset ) { From ef7c23a5a8cd1b720554221a084cc9b35583e4b7 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Tue, 3 Feb 2026 16:47:27 -0600 Subject: [PATCH 08/13] Taxonomy: Use strict comparison and improve docblock for default term check. Use strict comparison with integer casting for consistency with other default term checks in the same file. Also add a description to the $default_category_id parameter in the _rows() method docblock. --- src/wp-admin/includes/class-wp-terms-list-table.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 8a2cd638b11f1..6f0253f579719 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -292,7 +292,7 @@ public function display_rows_or_placeholder() { * @param int $count * @param int $parent_term * @param int $level - * @param int $default_category_id + * @param int $default_category_id Optional. Term ID of the default category to skip displaying, or 0 if none. Default 0. */ private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$count, $parent_term = 0, $level = 0, $default_category_id = 0 ) { @@ -413,7 +413,7 @@ public function column_name( $tag ) { $default_term = get_option( 'default_' . $taxonomy ); $default_term_label = ''; - if ( $tag->term_id == $default_term ) { + if ( $tag->term_id === (int) $default_term ) { $default_term_label = ' — ' . __( 'Default' ) . ''; } From 7b3573f60f630eb842052c1b2d18e644d7b0e626 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Tue, 3 Feb 2026 16:49:22 -0600 Subject: [PATCH 09/13] Taxonomy: Remove pinning of default category to top of list. Keep the default category in its natural position within the hierarchy rather than pinning it to the top. This avoids complexity with child categories appearing detached from their parent when the default category has subcategories. The default category is still clearly indicated via the "Default" label and "Change Default" row action. The explanatory text below the categories table also describes the default category behavior. --- .../includes/class-wp-terms-list-table.php | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 6f0253f579719..3c70221ee3831 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -242,15 +242,6 @@ public function display_rows_or_placeholder() { return; } - // Handle custom display of default category by showing it first in the list. - $default_category = null; - if ( 'category' === $taxonomy ) { - $default_category = get_term( (int) get_option( 'default_category' ), 'category' ); - if ( ! ( $default_category instanceof WP_Term ) ) { - $default_category = null; - } - } - if ( is_taxonomy_hierarchical( $taxonomy ) && ! isset( $this->callback_args['orderby'] ) ) { if ( ! empty( $this->callback_args['search'] ) ) {// Ignore children on searches. $children = array(); @@ -258,26 +249,13 @@ public function display_rows_or_placeholder() { $children = _get_term_hierarchy( $taxonomy ); } - // Only show pinned default category on the first page. - if ( $default_category && 0 === $offset ) { - $this->single_row( $default_category ); - } - /* * Some funky recursion to get the job done (paging & parents mainly) is contained within. * Skip it for non-hierarchical taxonomies for performance sake. */ - $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count, 0, 0, $default_category ? $default_category->term_id : 0 ); + $this->_rows( $taxonomy, $this->items, $children, $offset, $number, $count ); } else { - // Only show pinned default category on the first page. - if ( $default_category && 0 === $offset ) { - $this->single_row( $default_category ); - } - foreach ( $this->items as $term ) { - if ( $default_category && $default_category->term_id === $term->term_id ) { - continue; - } $this->single_row( $term ); } } @@ -292,9 +270,8 @@ public function display_rows_or_placeholder() { * @param int $count * @param int $parent_term * @param int $level - * @param int $default_category_id Optional. Term ID of the default category to skip displaying, or 0 if none. Default 0. */ - private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$count, $parent_term = 0, $level = 0, $default_category_id = 0 ) { + private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$count, $parent_term = 0, $level = 0 ) { $end = $start + $per_page; @@ -308,11 +285,6 @@ private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$coun continue; } - // Skip duplicating display of default category. - if ( $default_category_id && $default_category_id === $term->term_id ) { - continue; - } - // If the page starts in a subtree, print the parents. if ( $count === $start && $term->parent > 0 && empty( $_REQUEST['s'] ) ) { $my_parents = array(); @@ -352,7 +324,7 @@ private function _rows( $taxonomy, $terms, &$children, $start, $per_page, &$coun unset( $terms[ $key ] ); if ( isset( $children[ $term->term_id ] ) && empty( $_REQUEST['s'] ) ) { - $this->_rows( $taxonomy, $terms, $children, $start, $per_page, $count, $term->term_id, $level + 1, $default_category_id ); + $this->_rows( $taxonomy, $terms, $children, $start, $per_page, $count, $term->term_id, $level + 1 ); } } } From 9f1cf1454a68f736190fce3d2fb60e75697ba91a Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Fri, 22 May 2026 15:48:29 -0500 Subject: [PATCH 10/13] Taxonomy: Address PR feedback on default category UI. - Restrict "Change Default" row action to users with `manage_options` - Include `default_term_{$taxonomy}` option in the Default label check for taxonomies registered with a `default_term` arg - Split the edit-tags.php help text by capability so users without `edit_term` or `manage_options` no longer see broken links --- src/wp-admin/edit-tags.php | 37 ++++++++++++++++--- .../includes/class-wp-terms-list-table.php | 10 +++-- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/edit-tags.php b/src/wp-admin/edit-tags.php index c8aa12a651021..cb2d72c62a21a 100644 --- a/src/wp-admin/edit-tags.php +++ b/src/wp-admin/edit-tags.php @@ -627,15 +627,40 @@

renamed or you can choose a different default category.' ), + /* translators: %s: Default category name. */ + __( 'Deleting a category does not delete the posts in that category. Instead, posts that were only assigned to the deleted category are set to the default category %s. The default category cannot be deleted.' ), /** This filter is documented in wp-includes/category-template.php */ - '' . apply_filters( 'the_category', get_cat_name( $default_category_id ), '', '' ) . '', - esc_url( get_edit_term_link( $default_category_id, 'category' ) ), - esc_url( admin_url( 'options-writing.php#default_category' ) ) + '' . apply_filters( 'the_category', get_cat_name( $default_category_id ), '', '' ) . '' ); + + $default_edit_link = current_user_can( 'edit_term', $default_category_id ) ? get_edit_term_link( $default_category_id, 'category' ) : null; + $can_change_default = current_user_can( 'manage_options' ); + + if ( $default_edit_link && $can_change_default ) { + echo ' '; + printf( + /* translators: 1: URL to edit the default category, 2: URL to Writing Settings. */ + __( 'It can be renamed or you can choose a different default category.' ), + esc_url( $default_edit_link ), + esc_url( admin_url( 'options-writing.php#default_category' ) ) + ); + } elseif ( $default_edit_link ) { + echo ' '; + printf( + /* translators: %s: URL to edit the default category. */ + __( 'It can be renamed.' ), + esc_url( $default_edit_link ) + ); + } elseif ( $can_change_default ) { + echo ' '; + printf( + /* translators: %s: URL to Writing Settings. */ + __( 'You can choose a different default category.' ), + esc_url( admin_url( 'options-writing.php#default_category' ) ) + ); + } ?>

diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index c4b2d12710491..346e5c27ea69a 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -383,9 +383,10 @@ public function column_cb( $item ) { public function column_name( $tag ) { $taxonomy = $this->screen->taxonomy; - $default_term = get_option( 'default_' . $taxonomy ); $default_term_label = ''; - if ( $tag->term_id === (int) $default_term ) { + if ( $tag->term_id === (int) get_option( 'default_' . $taxonomy ) + || $tag->term_id === (int) get_option( 'default_term_' . $taxonomy ) + ) { $default_term_label = ' — ' . __( 'Default' ) . ''; } @@ -537,7 +538,10 @@ protected function handle_row_actions( $item, $column_name, $primary ) { ); } - if ( 'category' === $taxonomy && (int) get_option( 'default_category' ) === $tag->term_id ) { + if ( 'category' === $taxonomy + && (int) get_option( 'default_category' ) === $tag->term_id + && current_user_can( 'manage_options' ) + ) { $actions['change-default'] = sprintf( '%s', admin_url( 'options-writing.php#default_category' ), From 35bd4f4a61f5ccd7e39ae58c065ed412c51e3523 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Fri, 22 May 2026 16:08:00 -0500 Subject: [PATCH 11/13] Taxonomy: Polish default category UI per code review. - Escape the URL on the `change-default` row action with `esc_url()` to match the surrounding pattern in `handle_row_actions()`. - Add a per-term `aria-label` to the `change-default` action so it matches the edit/delete/view actions. - Add tests for the new row-action gating and for the dual `default_{taxonomy}` / `default_term_{taxonomy}` lookup in `column_name()`. --- .../includes/class-wp-terms-list-table.php | 6 +- .../phpunit/tests/admin/wpTermsListTable.php | 146 ++++++++++++++++++ 2 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 346e5c27ea69a..526b60c737626 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -543,8 +543,10 @@ protected function handle_row_actions( $item, $column_name, $primary ) { && current_user_can( 'manage_options' ) ) { $actions['change-default'] = sprintf( - '%s', - admin_url( 'options-writing.php#default_category' ), + '%s', + esc_url( admin_url( 'options-writing.php#default_category' ) ), + /* translators: %s: Taxonomy term name. */ + esc_attr( sprintf( __( 'Change the default category from “%s”' ), $tag->name ) ), __( 'Change Default' ) ); } diff --git a/tests/phpunit/tests/admin/wpTermsListTable.php b/tests/phpunit/tests/admin/wpTermsListTable.php index 9071dfd4055c5..b6f97b6fdb61c 100644 --- a/tests/phpunit/tests/admin/wpTermsListTable.php +++ b/tests/phpunit/tests/admin/wpTermsListTable.php @@ -38,6 +38,17 @@ public function set_up() { $this->terms_list_table = new WP_Terms_List_Table(); } + /** + * Builds a terms list table bound to a specific taxonomy screen so that + * `$this->screen->taxonomy` resolves correctly inside the table methods. + * + * @param string $taxonomy Taxonomy slug. + * @return WP_Terms_List_Table + */ + private function get_terms_list_table_for_taxonomy( $taxonomy ) { + return new WP_Terms_List_Table( array( 'screen' => 'edit-' . $taxonomy ) ); + } + /** * Call an inaccessible (private or protected) method. * @@ -87,4 +98,139 @@ public function test_handle_row_actions_as_admin() { $this->assertStringContainsString( 'Delete', $actions, 'Delete action should be displayed to the admin.' ); $this->assertStringContainsString( admin_url( 'term.php' ), $actions, 'Edit term link should be displayed to the admin.' ); } + + /** + * @covers WP_Terms_List_Table::handle_row_actions() + * + * @ticket 26268 + */ + public function test_change_default_action_shown_for_default_category_as_admin() { + wp_set_current_user( self::$admin_id ); + + $default_category = self::factory()->term->create_and_get( array( 'taxonomy' => self::CATEGORY_TAXONOMY ) ); + $previous_default = get_option( 'default_category' ); + update_option( 'default_category', $default_category->term_id ); + + $list_table = $this->get_terms_list_table_for_taxonomy( self::CATEGORY_TAXONOMY ); + $actions = $this->call_inaccessible_method( $list_table, 'handle_row_actions', array( $default_category, 'title', 'title' ) ); + + update_option( 'default_category', $previous_default ); + + $this->assertStringContainsString( 'change-default', $actions, 'Change Default action should be displayed for the default category.' ); + $this->assertStringContainsString( esc_url( admin_url( 'options-writing.php#default_category' ) ), $actions, 'Change Default link should target the Writing Settings field.' ); + } + + /** + * @covers WP_Terms_List_Table::handle_row_actions() + * + * @ticket 26268 + */ + public function test_change_default_action_hidden_for_default_category_as_author() { + wp_set_current_user( self::$author_id ); + + $default_category = self::factory()->term->create_and_get( array( 'taxonomy' => self::CATEGORY_TAXONOMY ) ); + $previous_default = get_option( 'default_category' ); + update_option( 'default_category', $default_category->term_id ); + + $list_table = $this->get_terms_list_table_for_taxonomy( self::CATEGORY_TAXONOMY ); + $actions = $this->call_inaccessible_method( $list_table, 'handle_row_actions', array( $default_category, 'title', 'title' ) ); + + update_option( 'default_category', $previous_default ); + + $this->assertStringNotContainsString( 'change-default', $actions, 'Change Default action should be hidden for users without manage_options.' ); + } + + /** + * @covers WP_Terms_List_Table::handle_row_actions() + * + * @ticket 26268 + */ + public function test_change_default_action_hidden_for_non_default_category() { + wp_set_current_user( self::$admin_id ); + + $default_category = self::factory()->term->create_and_get( array( 'taxonomy' => self::CATEGORY_TAXONOMY ) ); + $non_default_category = self::factory()->term->create_and_get( array( 'taxonomy' => self::CATEGORY_TAXONOMY ) ); + $previous_default = get_option( 'default_category' ); + update_option( 'default_category', $default_category->term_id ); + + $list_table = $this->get_terms_list_table_for_taxonomy( self::CATEGORY_TAXONOMY ); + $actions = $this->call_inaccessible_method( $list_table, 'handle_row_actions', array( $non_default_category, 'title', 'title' ) ); + + update_option( 'default_category', $previous_default ); + + $this->assertStringNotContainsString( 'change-default', $actions, 'Change Default action should only appear on the default category row.' ); + } + + /** + * @covers WP_Terms_List_Table::handle_row_actions() + * + * @ticket 26268 + */ + public function test_change_default_action_hidden_for_non_category_taxonomy() { + wp_set_current_user( self::$admin_id ); + + $tag = self::factory()->term->create_and_get( array( 'taxonomy' => 'post_tag' ) ); + + $list_table = $this->get_terms_list_table_for_taxonomy( 'post_tag' ); + $actions = $this->call_inaccessible_method( $list_table, 'handle_row_actions', array( $tag, 'title', 'title' ) ); + + $this->assertStringNotContainsString( 'change-default', $actions, 'Change Default action should not appear on non-category taxonomies.' ); + } + + /** + * @covers WP_Terms_List_Table::column_name() + * + * @ticket 26268 + */ + public function test_column_name_shows_default_label_for_default_category() { + wp_set_current_user( self::$admin_id ); + + $default_category = self::factory()->term->create_and_get( array( 'taxonomy' => self::CATEGORY_TAXONOMY ) ); + $previous_default = get_option( 'default_category' ); + update_option( 'default_category', $default_category->term_id ); + + $list_table = $this->get_terms_list_table_for_taxonomy( self::CATEGORY_TAXONOMY ); + $output = $list_table->column_name( $default_category ); + + update_option( 'default_category', $previous_default ); + + $this->assertStringContainsString( 'taxonomy-default-label', $output, 'Default label markup should be rendered for the default category.' ); + } + + /** + * @covers WP_Terms_List_Table::column_name() + * + * @ticket 26268 + */ + public function test_column_name_shows_default_label_for_default_term_custom_taxonomy() { + wp_set_current_user( self::$admin_id ); + + register_taxonomy( 'wptest_default_label_tax', 'post' ); + $default_term = self::factory()->term->create_and_get( array( 'taxonomy' => 'wptest_default_label_tax' ) ); + update_option( 'default_term_wptest_default_label_tax', $default_term->term_id ); + + $list_table = $this->get_terms_list_table_for_taxonomy( 'wptest_default_label_tax' ); + $output = $list_table->column_name( $default_term ); + + delete_option( 'default_term_wptest_default_label_tax' ); + unregister_taxonomy( 'wptest_default_label_tax' ); + + $this->assertStringContainsString( 'taxonomy-default-label', $output, 'Default label should appear for terms identified by default_term_{taxonomy}.' ); + } + + /** + * @covers WP_Terms_List_Table::column_name() + * + * @ticket 26268 + */ + public function test_column_name_omits_default_label_for_non_default_term() { + wp_set_current_user( self::$admin_id ); + + $non_default_category = self::factory()->term->create_and_get( array( 'taxonomy' => self::CATEGORY_TAXONOMY ) ); + + $list_table = $this->get_terms_list_table_for_taxonomy( self::CATEGORY_TAXONOMY ); + $output = $list_table->column_name( $non_default_category ); + + $this->assertStringNotContainsString( 'taxonomy-default-label', $output, 'Default label should not appear on non-default terms.' ); + } } From 1cbec4ee3a54173e5bd1cb23b6c530e1772b6ec9 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Wed, 11 Nov 2026 14:00:59 -0600 Subject: [PATCH 12/13] Taxonomy: Use Yoda conditions in the default term label check. --- src/wp-admin/includes/class-wp-terms-list-table.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 21e29866fe8b2..3d6c9c1c002c8 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -384,8 +384,8 @@ public function column_name( $tag ) { $taxonomy = $this->screen->taxonomy; $default_term_label = ''; - if ( $tag->term_id === (int) get_option( 'default_' . $taxonomy ) - || $tag->term_id === (int) get_option( 'default_term_' . $taxonomy ) + if ( (int) get_option( 'default_' . $taxonomy ) === $tag->term_id + || (int) get_option( 'default_term_' . $taxonomy ) === $tag->term_id ) { $default_term_label = ' — ' . __( 'Default' ) . ''; } From 4b93869664ad0b7565d3414e93b326934719aba5 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Wed, 11 Nov 2026 15:09:33 -0600 Subject: [PATCH 13/13] Tests: Isolate the taxonomy check in the non-category Change Default test. --- tests/phpunit/tests/admin/wpTermsListTable.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/admin/wpTermsListTable.php b/tests/phpunit/tests/admin/wpTermsListTable.php index b6f97b6fdb61c..15b0da42de1ce 100644 --- a/tests/phpunit/tests/admin/wpTermsListTable.php +++ b/tests/phpunit/tests/admin/wpTermsListTable.php @@ -169,11 +169,16 @@ public function test_change_default_action_hidden_for_non_default_category() { public function test_change_default_action_hidden_for_non_category_taxonomy() { wp_set_current_user( self::$admin_id ); - $tag = self::factory()->term->create_and_get( array( 'taxonomy' => 'post_tag' ) ); + $tag = self::factory()->term->create_and_get( array( 'taxonomy' => 'post_tag' ) ); + $previous_default = get_option( 'default_category' ); + // Match the term ID so only the taxonomy check can suppress the action. + update_option( 'default_category', $tag->term_id ); $list_table = $this->get_terms_list_table_for_taxonomy( 'post_tag' ); $actions = $this->call_inaccessible_method( $list_table, 'handle_row_actions', array( $tag, 'title', 'title' ) ); + update_option( 'default_category', $previous_default ); + $this->assertStringNotContainsString( 'change-default', $actions, 'Change Default action should not appear on non-category taxonomies.' ); }