7

WP_Posts_List_Table::extra_tablenav() applies restrict_manage_posts, which can be used by a plugin to output extra dropdowns (or other markup) that allow filtering the posts in the table by arbitrary criteria.

Similarly, WP_(Users|Comments)_List_Table have a resttric_manage_(users|comment) action for similar purposes.

However, as far as I can tell, WP_Terms_List_Table does not. Is this correct?

I'm writing a plugin that relies heavily on termmeta on various custom taxonomies. My plugin adds custom columns to edit-tags.php?taxonomy=my_custom_tax that display the value(s) of the various termmeta for each term. It would greatly enhance the UX of my plugin if users were able to filter the term list by the distinct meta_value's for my meta_key's.

Before I open a Trac ticket that proposes adding a WP_Terms_List_Table::extra_tablenav() method and restrict_manage_terms filter so that it would be possible to do what I want to do, I wanted to see if anyone here on WPSE knows:

  1. is there a viable work around (that doesn't involve something like hijacking edit-tags.php?taxonomy=my_custom_tax to redirect to a custom page that uses a custom subclass of WP_Terms_List_Table)?
  2. is there a (good) reason WP_Terms_List_Table doesn't currently support this functionality?
    1. I realize for core there is no (good) reason to filter by any of the standard WP_Term_List_Table columns, but adding custom columns to it is fairly common and I can't believe I'm the only one who's ever wanted to filter by the values in custom columns.

Edit

I should note that I when displaying the meta_value in the custom column, I am outputting it as a link which effectively filters the term list by that value. The the reason the UX would be enchanced if users could choose from a dropdown (like in WP_Posts_List_Table) is that not all values of the meta_value are always visible on any given page of terms displayed in the list table. What I do to use the links to filter by a value that is not currently displayed on the current page is click on a random link, then edit the resulting URL in the browser's address field to have the meta_value I want to filter by. Not only is that a pain, but not all user's are savy enough to realize they can do this.

2 Answers 2

4

I've been searching around for the same answer to your question and running into the exact same issues, and really appreciate your thorough research. I'm definitely +1 here for proposing this addition!

To attempt to answer 2) - I don't think there's a good reason to exclude it. I think WordPress's taxonomy terms are woefully lacking in features compared to the post and user entities (seeing as how term meta came much later, after post and user meta), and would like to see that change.

To answer 1) - I don't know how much better this is than your solution, but the workaround I've come up with involves filtering the taxonomy list via the get_terms_args filter, and then manually inserting a select dropdown + filter button in client-side JS. Here's a sample of my setup (modified to not use my specific classes or constants), which is filtering by a certain term meta field:

PHP Logic

// @see https://developer.wordpress.org/reference/hooks/get_terms_args/
add_filter( 'get_terms_args', 'taxonomy_filter', 10, 2 );

function taxonomy_filter( $args, $taxonomies ) {
    global $pagenow;
    if ( 'edit-tags.php' !== $pagenow || ! in_array( 'taxonomy-name', $taxonomies, true ) ) { return $args; }

    // Sort by most recently added terms, instead of alphabetically
    $args['orderby'] = 'term_id';
    $args['order'] = 'desc';

    // Filter by term meta
    $meta_key = ( isset( $_GET['meta_key'] ) ) ? sanitize_text_field( $_GET['meta_key'] ) : null;
    $meta_value = ( isset( $_GET['meta_value'] ) ) ? sanitize_text_field( $_GET['meta_value'] ) : null;

    if ( 'meta-filter' === $meta_key && $meta_value ) {
        $args['meta_key'] = $meta_key;
        $args['meta_value'] = $meta_value;
    }

    // Note: for more complex filtering, use the $args['meta_query'] array.

    return $args;
}

At this point, you should be able to see this working simply by going to (e.g.) yoursite.com/wp-admin/edit-tags.php?taxonomy=taxonomy-name&meta_key=meta-filter&meta_value=foobar.

Client-side jQuery logic

(Make sure you include this file in wp_enqueue_script(), but only when $pagenow === 'edit-tags.php').

/**
 * Param helpers
 */

// Get params in a JS object
var params = {};
window.location.search.substr(1).split( '&' ).forEach(function(item) {
    params[ item.split( '=' )[0] ] = item.split( '=' )[1];
});

// Return param string based on the params object
function setParams() {
    var string = '?';

    for ( key in params ) {
        var value = params[ key ];
        string += key + '=' + value + '&';
    }

    return string.slice(0, -1); // Remove trailing &
}

/**
 * Add dropdown filters + functionality to term tables
 */

if ( 'taxonomy-name' === params.taxonomy ) {
    // Create the dropdown menu & HTML
    $( document.querySelector( '.tablenav .bulkactions' ) ).after( '\
        <div class="alignleft actions"> \
            <select id="js-filter-dropdown"> \
                <option value="">Term Meta Filter</option> \
                <option value="foo">Foo</option> \
                <option value="bar">Bar</option> \
                <option value="baz">Baz</option> \
            </select> \
            <button id="js-filter" class="button" type="button">Filter</button> \
        </div> \
    ' );

    // If we're already filtering the view, have the dropdown reflect that
    var value = decodeURIComponent( params.meta_value ).replace(/\+/g, ' ');
    $( '#js-filter-dropdown' ).find( 'option[value="' + value + '"]' ).prop( 'selected', true );

    // Set up the button action - see taxonomy_filter() for server-side filtering
    $( '#js-filter' ).click(function() {
        var value = $( '#js-filter-dropdown' ).val();

        if ( value ) {
            params.meta_key = 'meta-filter';
            params.meta_value = encodeURIComponent( value );
        } else {
            delete params.meta_key;
            delete params.meta_value;
        }

        window.location.search = setParams();
    });
}

Screenshot of the Filtering UI

Before

Screenshot of the filtered table

After image

3
  • thanx for the backup on the idea! I am currently using get_terms_args to do the filtering when one of my links is clicked on. Commented May 31, 2017 at 18:32
  • 2
    I like your JS workaround and hope to don't mind me borrowing the idea...until we get a real restrict_manage_terms filter. One thing I'd suggest is rather than hardcoding the meta_values, use wp_localize_script() to dynamically populate the dropdown with the currently used meta_values. Commented May 31, 2017 at 18:35
  • 1
    Paul - yes, totally agreed that it's better not to hardcode the meta_values! I left that as a generic example, but if possible they should be dynamically generated. For my specific use-case, I use the FieldManager plugin and my term meta is already a select - so I clone/append my <options> via jQuery :)
    – Cee
    Commented May 31, 2017 at 19:44
3

Quickly reviewing the class, it looks like we could hijack the bulk_actions-{$this->screen->id} filter, to add extra dropdowns at the table top.

Example

For the category taxonomy:

add_filter( 'bulk_actions-edit-category', function( $actions )
{
    echo '<select name="foo" ><option value="bar">Bar</option></select>';

    return $actions;
} );

Otherwise there's not an easy way to extend the WP_Terms_List_Table class that's instantiated with:

$wp_list_table = _get_list_table('WP_Terms_List_Table');

Update:

Here's another adventures hack, for the edit category screen, that needs some testing :-)

add_filter( 'manage_edit-category_columns', function( $columns ) use ( &$wp_list_table )
{    
    // Include the WP_Terms_List_Table class
    require_once ( ABSPATH . 'wp-admin/includes/class-wp-terms-list-table.php' );

    // Extend the WP_Terms_List_Table class
    class WPSE_Terms_List_Table extends WP_Terms_List_Table
    {
        // Override this method
        protected function extra_tablenav( $which )
        {
            echo '<select name="foo" ><option value="bar">Bar</option></select>';
        }
    } 
    // end class

    $obj = new WPSE_Terms_List_Table;
    $obj->prepare_items(); 

    // Let's clone it to the global object
    $wp_list_table = clone $obj;

    return $columns;
} );

Additionally one needs to filter with get_terms_args as mentioned by @winnietherpooh and adjust the location with the redirect_term_locationfilter as mentioned by @Paul 'Sparrow Hawk' Biron

11
  • the problem with this is that the dropdown would appear before the bulk actions dropdown (which would be different from the other WP_List_Table subclasses that support filtering...which wouldn't be a good UX). Commented May 31, 2017 at 18:46
  • yes, true, pherabs floating it to right with CSS could adjust it accordingly ;-)
    – birgire
    Commented May 31, 2017 at 18:47
  • 2
    adding a call to get_submit_button() and wrapping both the select element and the submit_button in a div and floating that div right works! Thanx, I like this even better than the JS workaround. Commented May 31, 2017 at 18:54
  • I updated the answer with some alternative hairy method ;-) @Paul'SparrowHawk'Biron
    – birgire
    Commented May 31, 2017 at 19:25
  • 1st: thanx for pointing out the use() syntax on anon functions, I had never seen that before. Since the documentation on it is almost non-existent in the PHP manual, took me some time to figure out what it meant. This answer over on Stack Overflow helped. Initially I was confused because of the ambiguity with PHP's 'use' syntax for traits. Commented Jun 1, 2017 at 15:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.