From eb05ad534a5c986fc36edadc6279ccd1d6e72dbb Mon Sep 17 00:00:00 2001
From: Cory Hughart <cory@blackbird.digital>
Date: Fri, 28 Feb 2020 14:08:30 -0500
Subject: [PATCH] initial implementation based off work by @jtsternberg

---
 src/wp-admin/includes/admin-filters.php       |   3 +
 .../includes/class-wp-posts-list-table.php    | 105 +++++++++++++++++-
 src/wp-includes/class-wp-taxonomy.php         |   9 ++
 src/wp-includes/taxonomy.php                  |   2 +
 4 files changed, 118 insertions(+), 1 deletion(-)

diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php
index 4e66b28bb8..cefc948ff0 100644
--- a/src/wp-admin/includes/admin-filters.php
+++ b/src/wp-admin/includes/admin-filters.php
@@ -148,3 +148,6 @@ add_action( 'post_updated', array( 'WP_Privacy_Policy_Content', '_policy_page_up
 
 // Append '(Draft)' to draft page titles in the privacy page dropdown.
 add_filter( 'list_pages', '_wp_privacy_settings_filter_draft_page_titles', 10, 2 );
+
+// Sortable taxonomy columns
+add_filter( 'posts_clauses', array( 'WP_Posts_List_Table', '_peform_sortable_taxonomy_columns_query' ), 10, 2 );
diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php
index 3a8dd65936..6744a9c6e0 100644
--- a/src/wp-admin/includes/class-wp-posts-list-table.php
+++ b/src/wp-admin/includes/class-wp-posts-list-table.php
@@ -116,6 +116,92 @@ class WP_Posts_List_Table extends WP_List_Table {
 		}
 	}
 
+
+	/**
+	 * Performs column sorting for taxonomies registered with 'show_column_sortable'.
+	 *
+	 * @since  5.4.0
+	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 *
+	 * @param array    $clauses  The list of clauses for the query.
+	 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
+	 * @return string Modified clauses
+	 */
+	public static function _peform_sortable_taxonomy_columns_query( $clauses, $wp_query ) {
+		global $wpdb;
+
+		if ( ! isset( $wp_query->query['orderby'] ) ) {
+			return $clauses;
+		}
+
+		$taxonomies = self::get_sortable_taxonomy_columns( $wp_query->query['post_type'] );
+
+		if ( empty( $taxonomies ) || ! is_array( $taxonomies ) ) {
+			return $clauses;
+		}
+
+		$sortable_taxonomies = array();
+		foreach ( $taxonomies as $taxonomy ) {
+			if ( 'category' == $taxonomy )
+				$column_key = 'categories';
+			elseif ( 'post_tag' == $taxonomy )
+				$column_key = 'tags';
+			else
+				$column_key = 'taxonomy-' . $taxonomy;
+
+			$sortable_taxonomies[ $taxonomy ] = $column_key;
+		}
+
+		$key = array_search( $wp_query->query['orderby'], $sortable_taxonomies, true );
+
+		if ( false === $key ) {
+			return $clauses;
+		}
+
+		$clauses['join'] .= "
+			LEFT OUTER JOIN {$wpdb->term_relationships} ON {$wpdb->posts}.ID={$wpdb->term_relationships}.object_id
+			LEFT OUTER JOIN {$wpdb->term_taxonomy} USING (term_taxonomy_id)
+			LEFT OUTER JOIN {$wpdb->terms} USING (term_id)
+		";
+		$clauses['where'] = $wpdb->prepare( " AND taxonomy = '%s'", $key );
+		$clauses['groupby'] = "object_id";
+		$clauses['orderby'] = "GROUP_CONCAT({$wpdb->terms}.name ORDER BY name ASC) ";
+		$clauses['orderby'] .= ( 'ASC' == strtoupper( $wp_query->get('order') ) ) ? 'ASC' : 'DESC';
+
+		return $clauses;
+	}
+
+	/**
+	 * Get a list of all taxonomies registered as sortable.
+	 * Passes through the "manage_taxonomies_for_{$post_type}_sortable_columns" filter.
+	 *
+	 * @since  5.4.0
+	 *
+	 * @param  string $post_type Post type to retrieve Taxonomies
+	 * @return array Array of sortable taxonomies
+	 */
+	public static function get_sortable_taxonomy_columns( $post_type ) {
+		$taxonomies = get_object_taxonomies( $post_type, 'objects' );
+		$taxonomies = wp_filter_object_list( $taxonomies, array( 'show_admin_column' => true, 'show_column_sortable' => true ), 'and', 'name' );
+
+		/**
+		 * Filter the sortable taxonomy columns in the Posts list table.
+		 *
+		 * The dynamic portion of the hook name, `$post_type`, refers to the post
+		 * type slug.
+		 *
+		 * @since 5.4.0
+		 *
+		 * @param array  $taxonomies Array of taxonomies to enable sortable columns for.
+		 * @param string $post_type  The post type.
+		 */
+		$taxonomies = apply_filters( "manage_taxonomies_for_{$post_type}_sortable_columns", $taxonomies );
+		$taxonomies = array_filter( $taxonomies, 'taxonomy_exists' );
+
+		return $taxonomies;
+	}
+
 	/**
 	 * Sets whether the table layout should be hierarchical or not.
 	 *
@@ -689,12 +775,29 @@ class WP_Posts_List_Table extends WP_List_Table {
 	 * @return array
 	 */
 	protected function get_sortable_columns() {
-		return array(
+		$sortables = array(
 			'title'    => 'title',
 			'parent'   => 'parent',
 			'comments' => 'comment_count',
 			'date'     => array( 'date', true ),
 		);
+
+		$taxonomies = self::get_sortable_taxonomy_columns( $this->screen->post_type );
+
+		if ( ! empty( $taxonomies ) && is_array( $taxonomies ) ) {
+			foreach ( $taxonomies as $taxonomy ) {
+				if ( 'category' == $taxonomy )
+					$column_key = 'categories';
+				elseif ( 'post_tag' == $taxonomy )
+					$column_key = 'tags';
+				else
+					$column_key = 'taxonomy-' . $taxonomy;
+
+				$sortables[ $column_key ] = $column_key;
+			}
+		}
+
+		return $sortables;
 	}
 
 	/**
diff --git a/src/wp-includes/class-wp-taxonomy.php b/src/wp-includes/class-wp-taxonomy.php
index 7cb35e9044..bdbfe21803 100644
--- a/src/wp-includes/class-wp-taxonomy.php
+++ b/src/wp-includes/class-wp-taxonomy.php
@@ -124,6 +124,14 @@ final class WP_Taxonomy {
 	 */
 	public $show_admin_column = false;
 
+	/**
+	 * Whether the admin column for the taxonomy on its post type listing screens is sortable.
+	 *
+	 * @since 5.4.0
+	 * @var bool
+	 */
+	public $show_column_sortable = false;
+
 	/**
 	 * The callback function for the meta box display.
 	 *
@@ -269,6 +277,7 @@ final class WP_Taxonomy {
 			'show_tagcloud'         => null,
 			'show_in_quick_edit'    => null,
 			'show_admin_column'     => false,
+			'show_column_sortable'  => false,
 			'meta_box_cb'           => null,
 			'meta_box_sanitize_cb'  => null,
 			'capabilities'          => array(),
diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php
index 370cb46ad5..38851ab3ec 100644
--- a/src/wp-includes/taxonomy.php
+++ b/src/wp-includes/taxonomy.php
@@ -67,6 +67,7 @@ function create_initial_taxonomies() {
 			'public'                => true,
 			'show_ui'               => true,
 			'show_admin_column'     => true,
+			'show_column_sortable'  => true,
 			'_builtin'              => true,
 			'capabilities'          => array(
 				'manage_terms' => 'manage_categories',
@@ -90,6 +91,7 @@ function create_initial_taxonomies() {
 			'public'                => true,
 			'show_ui'               => true,
 			'show_admin_column'     => true,
+			'show_column_sortable'  => true,
 			'_builtin'              => true,
 			'capabilities'          => array(
 				'manage_terms' => 'manage_post_tags',
-- 
2.23.0

From 118ec8b76643c3a606730af7e3574d85f9e836c3 Mon Sep 17 00:00:00 2001
From: Cory Hughart <cory@blackbird.digital>
Date: Fri, 28 Feb 2020 15:33:25 -0500
Subject: [PATCH] fix posts with no taxonomy assigned not appearing

---
 .../includes/class-wp-posts-list-table.php    | 21 ++++++++++++-------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php
index 6744a9c6e0..dafe5ce7d2 100644
--- a/src/wp-admin/includes/class-wp-posts-list-table.php
+++ b/src/wp-admin/includes/class-wp-posts-list-table.php
@@ -159,15 +159,20 @@ class WP_Posts_List_Table extends WP_List_Table {
 			return $clauses;
 		}
 
-		$clauses['join'] .= "
-			LEFT OUTER JOIN {$wpdb->term_relationships} ON {$wpdb->posts}.ID={$wpdb->term_relationships}.object_id
-			LEFT OUTER JOIN {$wpdb->term_taxonomy} USING (term_taxonomy_id)
-			LEFT OUTER JOIN {$wpdb->terms} USING (term_id)
-		";
-		$clauses['where'] = $wpdb->prepare( " AND taxonomy = '%s'", $key );
-		$clauses['groupby'] = "object_id";
-		$clauses['orderby'] = "GROUP_CONCAT({$wpdb->terms}.name ORDER BY name ASC) ";
+		$clauses['join'] .= $wpdb->prepare( "
+			LEFT OUTER JOIN (
+				SELECT object_id, taxonomy, name
+				FROM {$wpdb->term_relationships}
+				LEFT OUTER JOIN {$wpdb->term_taxonomy} USING (term_taxonomy_id)
+				LEFT OUTER JOIN {$wpdb->terms} USING (term_id)
+				WHERE taxonomy = '%s'
+			) tax
+			ON {$wpdb->posts}.ID=tax.object_id
+		", $key );
+		$clauses['groupby'] = "{$wpdb->posts}.ID"; // term_taxonomy_id
+		$clauses['orderby'] = "GROUP_CONCAT(tax.name ORDER BY tax.name ASC) ";
 		$clauses['orderby'] .= ( 'ASC' == strtoupper( $wp_query->get('order') ) ) ? 'ASC' : 'DESC';
+		$clauses['orderby'] .= ", post_title ASC";
 
 		return $clauses;
 	}
-- 
2.23.0

From dda775caa04e794e79866403ed5512432d4175dd Mon Sep 17 00:00:00 2001
From: Cory Hughart <cory@blackbird.digital>
Date: Fri, 28 Feb 2020 18:06:58 -0500
Subject: [PATCH] fix typo and remove comment

---
 src/wp-admin/includes/admin-filters.php             | 2 +-
 src/wp-admin/includes/class-wp-posts-list-table.php | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php
index cefc948ff0..019df48f70 100644
--- a/src/wp-admin/includes/admin-filters.php
+++ b/src/wp-admin/includes/admin-filters.php
@@ -150,4 +150,4 @@ add_action( 'post_updated', array( 'WP_Privacy_Policy_Content', '_policy_page_up
 add_filter( 'list_pages', '_wp_privacy_settings_filter_draft_page_titles', 10, 2 );
 
 // Sortable taxonomy columns
-add_filter( 'posts_clauses', array( 'WP_Posts_List_Table', '_peform_sortable_taxonomy_columns_query' ), 10, 2 );
+add_filter( 'posts_clauses', array( 'WP_Posts_List_Table', '_perform_sortable_taxonomy_columns_query' ), 10, 2 );
diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php
index dafe5ce7d2..97204eed3c 100644
--- a/src/wp-admin/includes/class-wp-posts-list-table.php
+++ b/src/wp-admin/includes/class-wp-posts-list-table.php
@@ -128,7 +128,7 @@ class WP_Posts_List_Table extends WP_List_Table {
 	 * @param WP_Query $wp_query The WP_Query instance (passed by reference).
 	 * @return string Modified clauses
 	 */
-	public static function _peform_sortable_taxonomy_columns_query( $clauses, $wp_query ) {
+	public static function _perform_sortable_taxonomy_columns_query( $clauses, $wp_query ) {
 		global $wpdb;
 
 		if ( ! isset( $wp_query->query['orderby'] ) ) {
@@ -169,7 +169,7 @@ class WP_Posts_List_Table extends WP_List_Table {
 			) tax
 			ON {$wpdb->posts}.ID=tax.object_id
 		", $key );
-		$clauses['groupby'] = "{$wpdb->posts}.ID"; // term_taxonomy_id
+		$clauses['groupby'] = "{$wpdb->posts}.ID";
 		$clauses['orderby'] = "GROUP_CONCAT(tax.name ORDER BY tax.name ASC) ";
 		$clauses['orderby'] .= ( 'ASC' == strtoupper( $wp_query->get('order') ) ) ? 'ASC' : 'DESC';
 		$clauses['orderby'] .= ", post_title ASC";
-- 
2.23.0

