Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
102b5bc
Taxonomy: Add UI to Category page to indicate default category.
kraftbj Jan 30, 2026
f0421a5
Merge remote-tracking branch 'origin/trunk' into trac-26268-default-c…
kraftbj Feb 2, 2026
4dc573e
Taxonomy: Fix pagination and optimize default category handling.
kraftbj Feb 2, 2026
b7e0e1a
Taxonomy: Add "Change Default" row action for default category.
kraftbj Feb 2, 2026
eadf8ac
fix: Taxonomy: Fix Yoda condition in default category check.
kraftbj Feb 2, 2026
c985c34
Taxonomy: Add helpful links in default category help text.
kraftbj Feb 2, 2026
52e5831
Apply suggestions from code review
kraftbj Feb 3, 2026
fa26a3a
Merge branch 'trunk' into trac-26268-default-category-ui
kraftbj Feb 3, 2026
1ec2f96
fix: Taxonomy: Fix null dereference for hierarchical taxonomies.
kraftbj Feb 3, 2026
ef7c23a
Taxonomy: Use strict comparison and improve docblock for default term…
kraftbj Feb 3, 2026
7b3573f
Taxonomy: Remove pinning of default category to top of list.
kraftbj Feb 3, 2026
8c2c0d3
Merge branch 'trunk' into trac-26268-default-category-ui
kraftbj May 22, 2026
9f1cf14
Taxonomy: Address PR feedback on default category UI.
kraftbj May 22, 2026
35bd4f4
Taxonomy: Polish default category UI per code review.
kraftbj May 22, 2026
7d91014
Merge remote-tracking branch 'origin/trunk' into trac-26268-default-c…
kraftbj Nov 11, 2026
1cbec4e
Taxonomy: Use Yoda conditions in the default term label check.
kraftbj Nov 11, 2026
4b93869
Tests: Isolate the taxonomy check in the non-category Change Default …
kraftbj Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/wp-admin/edit-tags.php
Original file line number Diff line number Diff line change
Expand Up @@ -627,12 +627,40 @@
<div class="form-wrap edit-term-notes">
<p>
<?php
$default_category_id = (int) get_option( 'default_category' );
printf(
/* translators: %s: 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 */
'<strong>' . apply_filters( 'the_category', get_cat_name( get_option( 'default_category' ) ), '', '' ) . '</strong>'
'<strong>' . apply_filters( 'the_category', get_cat_name( $default_category_id ), '', '' ) . '</strong>'
);

$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 <a href="%1$s">renamed</a> or you can choose a <a href="%2$s">different default category</a>.' ),
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 <a href="%s">renamed</a>.' ),
esc_url( $default_edit_link )
);
} elseif ( $can_change_default ) {
echo ' ';
printf(
/* translators: %s: URL to Writing Settings. */
__( 'You can choose a <a href="%s">different default category</a>.' ),
esc_url( admin_url( 'options-writing.php#default_category' ) )
);
}
?>
</p>
<?php if ( current_user_can( 'import' ) ) : ?>
Expand Down
25 changes: 23 additions & 2 deletions src/wp-admin/includes/class-wp-terms-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,13 @@ public function column_cb( $item ) {
public function column_name( $tag ) {
$taxonomy = $this->screen->taxonomy;

$default_term_label = '';
if ( (int) get_option( 'default_' . $taxonomy ) === $tag->term_id
|| (int) get_option( 'default_term_' . $taxonomy ) === $tag->term_id
) {
$default_term_label = ' &mdash; <span class="taxonomy-default-label">' . __( 'Default' ) . '</span>';
}

$pad = str_repeat( '&#8212; ', max( 0, $this->level ) );

/**
Expand Down Expand Up @@ -420,8 +427,9 @@ public function column_name( $tag ) {
}

$output = sprintf(
'<strong>%s</strong><br />',
$name
'<strong>%s%s</strong><br />',
$name,
$default_term_label
);

/** This filter is documented in wp-admin/includes/class-wp-terms-list-table.php */
Expand Down Expand Up @@ -530,6 +538,19 @@ protected function handle_row_actions( $item, $column_name, $primary ) {
);
}

if ( 'category' === $taxonomy
&& (int) get_option( 'default_category' ) === $tag->term_id
&& current_user_can( 'manage_options' )
) {
$actions['change-default'] = sprintf(
'<a href="%s" aria-label="%s">%s</a>',
esc_url( admin_url( 'options-writing.php#default_category' ) ),
/* translators: %s: Taxonomy term name. */
esc_attr( sprintf( __( 'Change the default category from &#8220;%s&#8221;' ), $tag->name ) ),
__( 'Change Default' )
);
}

/**
* Filters the action links displayed for each term in the Tags list table.
*
Expand Down
151 changes: 151 additions & 0 deletions tests/phpunit/tests/admin/wpTermsListTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -87,4 +98,144 @@ 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' ) );
$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.' );
}

/**
* @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.' );
}
}
Loading