Custom Bulk Actions added in WordPress 4.7

With the release of WordPress 4.7, developers have a lot to be thankful for. Many have already covered post type templates, and the addition of the REST API in the core is a game changer. There’s a few more features that came along with the release, but I’d like to cover one that I’ve been looking forward to as a developer: custom bulk actions.

As a WordPress developer, I’m often asked for three different things: add X ability, make X ability easier, make X work in a bulk manner. The last request is especially common for site owners dealing with products while using WooCommerce, WP eCommerce, MarketPress, etc. For instance, administrators may want the ability to edit sale pricing in bulk without going in to edit each product individually. Since clients tend to pay for this sort of thing, I usually oblige. =)

The Old Way

I’ve been able to add custom bulk actions in the past, but I’ve never liked the “hacked” nature of the resulting code. Without a native, WordPress-y way of adding a custom bulk action for list tables, I had to rely on jQuery to inject my new action into the “Bulk Actions” dropdown. Then, I had to either check $_REQUEST vars or hook in to the load-edit.php action to process the custom action. Finally, sending a custom key to the query string to check on the next load for status of the action.

Needless to say, this was messy, and developers have requested a more proper and sanctioned way of working in bulk in list table screens.

Bulk Up With 4.7

As I mentioned earlier, the development team included new hooks for custom bulk actions in WordPress 4.7. The new functionality includes hooks that are familiar to developers, following the {action_type}-{screen_id} format we’re accustomed to. Specifically for custom bulk actions, the two new classes of filters are bulk_actions-{screen_id} and handle_bulk_actions-{screen_id}. The former inserts a new option into the “Bulk Actions” dropdown when using {screen_id}, and the latter actually processes bulk actions when the “Apply” button is clicked.

To notify the user of the results of our action, we can still use the admin_notices hook, looking for a specific argument added to the query string in our handle_bulk_actions-{screen_id} function.

A Quick Example

I’ll be using my example above of bulk setting sale pricing for selected products in WooCommerce. In order to update the sale price, we need to add an input once our bulk action is selected. That will require some JavaScript. However, we should probably start with the simplest bit by adding our action to the dropdown.

Create a quick plugin file1Not to beat a dead horse, but non-theme-specific modifications for WordPress belong in a plugin. With WordPress Plugin Boilerplate, it’s easy to start., and add in the following code:

<?php
function de_bulk_actions_edit_product( $bulk_actions ) {
	$bulk_actions['de_set_product_sale_price'] = __( 'Set sale price', 'wordpress' );
	return $bulk_actions;
}

add_filter( 'bulk_actions-edit-product', 'de_bulk_actions_edit_product' );

Once that’s done, we need to add our input where the user can enter the new sale price. We can accomplish this with a bit of jQuery. Create a script file and enqueue it properly2Do not use admin_head to insert admin-only scripts. Use admin_enqueue_scripts instead! on the admin side. The “Bulk Actions” dropdown element selectors are #bulk-action-selector-top and #bulk-action-selector-bottom.3There’s a “Bulk Actions” dropdown at the top AND bottom of all list tables. Be sure to select both if you want your custom action showing up in both. We need to watch the change event for them, checking for our custom option.

When our option is selected, we need to create our input element with appended HTML. In your script file, add the following:

$(document).ready(function() {
	// Watch the bulk actions dropdown, looking for custom bulk actions
  	$("#bulk-action-selector-top, #bulk-action-selector-bottom").on('change', function(e){
  		var $this = $(this);

  		if ( $this.val() == 'de_set_product_sale_price' ) {
  			$this.after($("<input>", { type: 'text', placeholder: "e.g. 14.99", name: "de_bulk_product_sale_price" }).addClass("de-custom-bulk-actions-elements"));
  		} else {
  			$(".de-custom-bulk-actions-elements").remove();
  		}
  	}); 
});

This code is pretty simple. We’re adding our custom input right next to the bulk actions dropdown in the DOM, so it appears inline with the rest of the row. When our new action is selected, this is what it looks like:

Custom Bulk Actions for WordPress - Input Value

The user can now enter a custom value for the bulk action.

Processing the Custom Action

Now we need to tell WordPress what to do when the user updates the sale price and clicks the “Apply” button. With the updates to the core in version 4.7, we can hook in to handle_bulk_actions-edit-product. Add the following code to your plugin file.

<?php
function de_handle_bulk_actions_edit_product( $redirect_to, $action, $post_ids ) {
	if ( $action !== 'de_set_product_sale_price' ) {
		return $redirect_to;
	} else if ( ! isset( $_REQUEST['de_bulk_product_sale_price'] ) || empty ( $_REQUEST['de_bulk_product_sale_price'] ) ) {
		return $redirect_to;
	}

	$updated_post_ids = array();
	$new_sale_price = (float) $_REQUEST['de_bulk_product_sale_price'];

	foreach ( $post_ids as $post_id ) {
    	$product = wc_get_product( $post_id );

    	if  ( $product->product_type == 'simple' ) {
    		if ( ! update_post_meta( $post_id, '_sale_price', $new_sale_price ) ) {
				wp_die( __( 'Error in updating the product sale price.' ) );
			}

			$updated_post_ids[] = $post_id;
    	}
  	}

	$redirect_to = add_query_arg( 'de_bulk_product_sale_price_update_results', count( $updated_post_ids ), $redirect_to );

	return $redirect_to;
}

add_filter( 'handle_bulk_actions-edit-product', 'de_handle_bulk_actions_edit_product', 10, 3 );

In the code above, we’ve created a function to handle bulk actions on the “edit-product” screen. The function is expecting three variables. The first is $redirect_to which is the page we’re redirecting to after the form processing. Next, $action is the value of the “Bulk Actions” dropdown that was selected by the user. Finally, we have $post_ids that contains the array of post IDs that were selected in the list table by the user.

Inside our function, we start off by checking the bulk action selected is the custom “de_set_product_sale_price” action. We also ensure that something was entered for the new sale price. If either of these checks fail, we bail early and notify the user.

At this point, we begin looping through each of the selected post IDs. For the purposes of our simple example, I’m also verifying that this product is “simple” in type. Any “variable” product handles pricing at the variation level. We need products that have pricing set at the product level only. If the current post ID in our loop is of the appropriate type, we attempt to update the post meta key “_sale_price”.4Setting the “_sale_price” alone in WooCommerce does not put the product on sale. If you also want to enable the sale price for customers, you need to set the “_price” meta to the sale price as well. After checking for errors in the update process, we add the current post ID to an $updated_post_ids array to keep track of which posts were actually updated.

Results Handling

Finally, we add a custom query argument to the redirect URL that is a count of how many posts were updated. For all of this to function properly, our function needs to return the $redirect_to value back to the original hook.

Running the current code will produce the expected results. However, there’s nothing affirming that our new custom bulk action was processed or what the results were. By adding the query argument in our “handle” function, we can check the redirected URL for the results and update the user with a notice. Open up the plugin file again and add the following:

<?php
function de_bulk_action_edit_product_admin_notice() {
  	if ( isset( $_REQUEST['de_bulk_product_sale_price_update_results'] ) ) {
    	$updated_products_count = intval( $_REQUEST['de_bulk_product_sale_price_update_results'] );

    	echo '<div id="message" class="' . ( $updated_products_count > 0 ? 'updated' : 'error' ) . '">';

		if ( $updated_products_count > 0 ) {
			echo '<p>' . __( 'Updated sale price for '. $updated_products_count .' '. _n( 'product', 'products', $updated_products_count, 'wordpress' ).'!', 'wordpress' ) . '</p>';
		} else {
			echo '<p>' . __( 'No products were updated!', 'wordpress' ) . '</p>';
		}

		echo '</div>';
  	}
}

add_action( 'admin_notices', 'de_bulk_action_edit_product_admin_notice' );

This code should be familiar to WordPress plugin developers. The main point of this snippet is to check for the existence of our added query argument when the admin_notices action is run. If the value for de_bulk_product_sale_price_update_results is greater than zero, we display a success notice that says just how many products had their sale price updated. If the value is zero, we tell the user that no products were updated with an error notice.

WordPress Custom Bulk Actions - User Notification

Upon submission, the user is notified of the results of the custom bulk action handler.

Possibilities Abound

Although my example for a custom bulk action is rather simple, it demonstrates the basics of the new core functionality. The new bulk actions hooks are available for all screens using list tables, including posts, page, media, plugins, taxonomies, and more. There are plenty of WordPress plugins that could benefit from this as well. Forum plugins could add bulk actions for suspending users. Booking plugins could accept appointment requests in bulk. And so on.

If you found this or any of my other tutorials helpful, please leave a comment!

About

Web Developer

References   [ + ]

1. Not to beat a dead horse, but non-theme-specific modifications for WordPress belong in a plugin. With WordPress Plugin Boilerplate, it's easy to start.
2. Do not use admin_head to insert admin-only scripts. Use admin_enqueue_scripts instead!
3. There's a "Bulk Actions" dropdown at the top AND bottom of all list tables. Be sure to select both if you want your custom action showing up in both.
4. Setting the "_sale_price" alone in WooCommerce does not put the product on sale. If you also want to enable the sale price for customers, you need to set the "_price" meta to the sale price as well.

4 Comments

  1. Miriam

    Hi there! I really appreciate this tutorial but could need some help. I would like to do something quite similar but cannot figure out my mistake. I would like to add custom bulk action to the woocommerce order list to be able to send a customer note for multiple orders all at once. Can you maybe help me with that? Would be really appreciated 🙂

    1. David

      You should be able to complete the task you described with a very similar process. The {screen_id} for the orders listing page in WooCommerce is “edit-shop_order”. Using that, the two critical filters to hook in to are bulk_actions-edit-shop_order and handle_bulk_actions-edit-shop_order.

      For your note text, if you’d rather not statically define that value in the code, you can use JavaScript similar to the article to append a textarea modal to the body.

      Finally, you can either change the order status with the customer note, or you may send just the note without an order status update. For the latter, create a new WC_Email_Customer_Note() and calling the trigger( $args ) method with an array of $args containing both “order_id” and “customer_note”.

      Hope this helps!

Add Your Thoughts

Your email address will not be published. Required fields are marked *