Adding Screen Options To WordPress Admin Pages

Many of the built-in WordPress admin pages have a Screen Options drop-down tab on the top right of the page.   This is used to set how many items are shown in the default WordPress tables, such as the list of pages or posts.   Some of the WordPress admin interfaces also allow you to set which columns to show or hide on the page.

As a plugin or theme author it provides a consistent interface for your users if you utilize this built-in feature as a way to control the admin user experience for your users.   I’ve been busy adding this to the Store Locator Plus Manage Locations interface for version 4.8 and found the documentation to be lacking at best.   Here are my notes on how I hacked this into existence.

The Steps

There are a few basic steps to the process;  Tell WordPress you have screen options you want to track, Save those options when they are changed, Implement those options while rendering the page.

Seems easy enough, but they are all wired together in what is a little less elegant that some of the other components of WordPress Core.  Hopefully these notes will help you get starts on how to do this properly.

Tell WordPress You Have Options

This is done by adding options using the WordPress add_screen_option() method.   It should be called via the WordPress load-<hook> action hook to ensure the proper WP_Screen environment is active when this function is called.

load-{$page_hook} is called from within the base WordPress admin.php call.    This comes AFTER the admin_init hook. Which means your instructions to add screen options to an admin page can come much later than the “Save The Options” code below.   However, you will need to know the page you are hooked into.    Since most admin pages are accessed by adding an entry to the WordPress admin menu, you should be able to store the page hook when calling add_menu_page() or add_submenu_page() to attach your custom admin page to the WordPress admin interface.   This is also a good place to add your hook to setup the screen options.

In this example code I use a named array in $this->menu_items to build a list of several pages I want to add to the admin interface.   It has several properties including the name of the method that will render my page and the name of the method that will add my screen options.    It will also store the hook.     This SLP_AdminUI class is a simple “traffic cop” that loads other classes that do the work so we can isolate code to the specific page being processed.

class SLP_AdminUI extends SLP_BaseClass_Admin {

/**
 * Create the admin menu.
 *
 * Roles and Caps
 * manage_slp_admin
 * manage_slp_user
 *
 * WordPress Store Locator Plus Menu Roles & Caps
 *
 * Info : manage_slp_admin
 * Locations: manage_slp_user
 * Settings: manage_slp_admin
 * General: manage_slp_admin
 */
public function create_admin_menu() {

$this->menu_items['slp_manage_locations'] =
   array(
      'label'          => __( 'Locations', 'store-locator-le' ),
      'slug'           => 'slp_manage_locations',
      'class'          => $this,
      'function'       => 'renderPage_Locations',
       'screen_options' => 'slp_manage_locations_screen_options' ,
   );


// Attach Menu Items To Sidebar and Top Nav
//
foreach ( $this->menu_items as $slug => $menu_item ) {

  $this->menu_items[ $slug ][ 'hook' ] = add_submenu_page(
   SLPLUS_PREFIX,
   $menu_item['label'],
   $menu_item['label'],
   $slpCapability,
   $menu_item['slug'],
   array( $menu_item['class'], $menu_item['function'] )
  );
  add_action( 'load-' . $this->menu_items[ $slug ][ 'hook' ] , array( $this , $this->menu_items[ $slug ][ 'screen_options' ] ) );
}

}



/**
 * Render the Locations admin page.
 */
function renderPage_Locations() {
   $this->slplus->Admin_Locations->render_adminpage();
}

/**
 * Add Manage Locations Screen Options
 */
public function slp_manage_locations_screen_options() {
   $this->slplus->Admin_Locations->add_screen_options();
}



}

 

And the page-specific class that does the per-page magic…

class SLP_Admin_Locations extends WP_List_Table {

/**
 * Add screen options.
 */
 public function add_screen_options() {
 add_screen_option( 'per_page', array( 'option' => 'admin_locations_per_page' , 'label' => __( 'Locations Per Page' , 'store-locator-le' ) , 'default' => 50 ) );
 }
}

Save The Options

Options are saved via the generic ‘set-screen-option’ filter in WordPress.   The option is called via the set_screen_options() function as part of every single WordPress admin.php call.   At least there is top-of-function short circuits that will drop out of the process for a variety of reasons which means the majority of admin pages that do not have screen options will not process much code before moving on.   For those pages that do have screen options, here are the important notes.

You must add the filter to set the screen options by employing the WordPress set-screen-option filter your code.   Since this is fired off very early in the process, basically at the start of WordPress when the admin page is processed, you need to add the filter early in the WordPress feature stack.   Calling it too late, for example inside your WordPress ‘admin_menu’ action hook functions, will not help you.  I’ve hooked my set-screen-options to the WordPress init action.

set-screen-option filter call stack 2017-06-15_09-20-20.png

The call stack an variable references from WordPress Core 4.8 when the set-screen-option filter if applied.

Note: Finding the right point of where to hook your code into WordPress is a key to leveling up your WordPress Foo.   You can find the general order of action hooks on the old-school WordPress Codex.

WordPress Admin Hooks 2017-06-15_09-54-46.png

The filter passes in 3 parameters and you really should be using object-oriented programming techniques so the full call should look something like the code below.  The SLP_Actions init() method is called by the main plugin code via the WordPress init action hook: add_action( ‘init’ , array( $this->Actions , ‘init’ ) ).  The save_screen_options() method in this action class is only a traffic cop; it will load up the proper code module based on what page is being processed by WordPress and call the save_screen_options() method for that specific page.  In this example it is the Manage Locations page.

class SLP_Actions extends SLPlus_BaseClass_Object {

/**
 * Called when the WordPress init action is processed.
 *
 * Current user is authenticated by this time.
 */
public function init() {
 add_filter( 'set-screen-option' , array( $this , 'save_screen_options' ) , 10 , 3 );
}


/**
 * Save screen options.
 *
 * @param boolean $status false by default, return this to not save options
 * @param string  $option the option name, a key in user_meta
 * @param mixed   $value  the option value for user_meta
 * @return mixed
 */
public function save_screen_options( $status, $option, $value) {
   $this_page = isset( $_REQUEST['page'] ) ? $_REQUEST['page'] : '';
   switch ( $this_page ) {
      case 'slp_manage_locations':
         require_once( SLPLUS_PLUGINDIR . 'include/module/admin_tabs/SLP_Admin_Locations.php' );
         return $this->slplus->Admin_Locations->save_screen_options( $status, $option, $value );
         break;
   }
   return $status;
}

}

This SLP_Admin_Locations class is only loaded when the Locations tab is active in the Store Locator Plus plugin.    This ensures less memory is consumed when other WordPress admin pages are loaded and keeps our screen options isolated to just those we are interested in for this page.

class SLP_Admin_Locations extends WP_List_Table {
/**
 * Save screen options.
 *
 * @param $status
 * @param $option
 * @param $value
 * @return mixedf
 */
public function save_screen_options( $status, $option, $value) {
 $valid_options = array( 'admin_locations_per_page' );
 if ( in_array( $option , $valid_options ) ) return $value;
 return $status;
}
}

If the filter, save_screen_options() of SLP_Admin_Locations in this example, returns a value other than false then the set_screen_options() of WordPress will call

update_user_meta($user->ID, $option, $value);

Implement The Options

Implementing the options will depend on  your specific use case.   In my case I’m using the per_page setting to determine how many locations to show in a locations table.  It is a modified version of WP_List_Table so the typical prepare_items() examples do not apply.   In my class I’ve also decided to implement the WP_Screen per_page standard of fetching the user_meta with get_user_option() and fall back to the WP_Screen _options property fallbacks for default values.

Here is the function I call in various places to limit how many locations are shown in my table.

/**
 * Get the screen option per_page.
 * @return int
 */
private function get_screen_option_per_page() {
   $this->get_wp_screen();
 $option = $this->wp_screen->get_option( 'per_page', 'option' );
 if ( ! $option ) {
  $option = str_replace( '-', '_', "{$this->wp_screen->id}_per_page" );
 }

 $per_page = (int) get_user_option( $option );
 if ( empty( $per_page ) || $per_page < 1 ) {
  $per_page = $this->wp_screen->get_option( 'per_page', 'default' );
  if ( ! $per_page ) {
   $per_page = 20;
  }
 }
 return $per_page;
}

private function get_my_data() {
  $max_to_show = min( $this->get_screen_option_per_page() , $this->slplus->Location_Manager->location_limit );
}
Manage Locations Per Page Option 2017-06-15_15-27-19.png

The Manage Locations per-page screen option in SLP 4.8

Related Notes

Some interesting things I found along the way that may be worth noting…

The default per_page up/down toggle will fire the page update/save (form submit) every time someone clicks the toggle.  Ouch.   Typing a number an clicking the Apply button is a lot less server overhead.

The set-screen-option is a per-user setting.   It is stored in user_meta.    The $option value is the user meta key , and value is the value.     Using serialized options may be a good idea here but it will complicate code.  I’m a big fan of less data I/O, because that is a costly performance hit, over multiple get_option() or user_meta() requests that hammer the database.

The option name ‘per_page’ is super-special.   This single screen option name will alone trigger the Screen Options drop down to be active.   Sadly this is a standalone option in user_meta which means it will force at least TWO data I/O operations if you use this AND any other options (including a serialized option).  Ugggh, WordPress…

The Screen Options drop down will only render if you add a screen option with the name ‘per_page’, the page has meta boxes attached, or you have defined layout columns for the page. From the WP_Screen::show_screen_options() method:

$show_screen = ! empty( $wp_meta_boxes[ $this->id ] ) || $columns || $this->get_option( 'per_page' );

Browsers that disable JavaScript will not see the Screen Options drop-down.  WordPress will apply the hide-if-no-js class which hides elements that depend on JS to work properly.

Adopting GitFlow As My Branching Model

A couple of months ago I noticed SmartGit, my preferred git management tool, had a  GitFlow button in the toolbar when I updated to the latest version.   Curious I decided to explore and a month later I was using GitFlow as my branching model for most of my Store Locator Plus code repositories.  While there are lot of arguments for and against the GitFlow model, just like any other code-related topic these days, I figure using a widely-accepted model was better than continuing with my own ad-hoc branching model.    At the very least I can tell new developers on the team “we use GitFlow” and they can “Google it” to learn more.
SmartGit GitFlow Button 2017-05-23_08-43-57
 GitFlow makes it very easy to start a feature/hotfix/support update and merge it back into the develop (our previous prerelease) branch as well as publishing releases and ensuring the updates are ported to master and develop.   It is even easier with SmartGit as I can click a buton to start a feature and click it again whent he feature is done.    Same for starting and deploying releases.    Anything I can do with one mouse click versus a dozen or two keystroke-click combinations is an efficiency gain.   I prefer being efficient whenever possible.
Below is my summary of how I view the model and how I’m using it.

The Short Notes On Branches

develop = completed stuff ready for other dev (was our prerelease)
master = the final production version
features = a new feature/patch that will go into a later develop release.  When you “finish” a feature using a GitFlow tool it will auto-merge to develop.  Fast forward or merge commit style depends on how you configure your GitFlow tools.
release = a new release candidate branch.   When you think develop is ready you bump versions in the product and start a release branch.   This tells developers “this is in testing and we think it is ready for production”.
There are other branches as well but I rarely use them.  They are the hotfix and support branches which you can read about on a lot of other GitFlow blogs.    hotfix bypasses the feature/develop/release merging-and-overhead and applies a code branch directly to master when finished, which then also pushed back to develop.

A Simplified Work Flow

Your existing codebase has a master branch in production.   Your develop branch is aligned and ready for new work.

You start a new feature or patch.   You create a feature branch usually named feature/super-cool-new-thing.   When you’ve completed the code and it looks stable you finish the feature.    GitFlow will merge this with your local develop.     No conflicts or other issues?  Push it.   The rest of your dev team has a new reference point for your upcoming release.

Start a release when you are done coming up with super cool new features.    When you start a release GitFlow will create a new branch named release/<version>.   If you’ve had prior release branches and tags GitFlow will see this and bump the next minor or point release.  If not you’ll need to name your first release.  Convention is to use the major.minor.point-release version format.

Put the release into production after testing.   When you finish a release, GitFlow will merge this with master, tag the commit with the version of that release, and make sure the develop branch is updated with any code changes that may have happened during testing/production.

Seems simple, but I quickly found a corner case that did not seem to be addressed.  What do you do when your release branch testing uncovers a bug?

My patch Branch

One of the complaints about GitFlow is what do you do when your code is ready for testing and find a bug.   The branch model does not clearly define the methodology and in this vacuum a lot of people have shared their opinions and many have tried to fill that void.  There were no methodologies that I came across that I really liked, so I created my own (which I’m sure someone else has done already).

After I start a new release branch I run through a series of tests.    Sometimes the tests fail and I need to update the code.    What do you do now?  Start a new feature branch?  It is not really a feature.  It also makes it very hard to see what “feature” is needed to get the release into production versus a feature you (or some other developer) has started for a different future release.    The bigger the team the more problematic this becomes.

In order to increase visibility for the team lead that is managing the repository I introduced the “patch” branch.  Patch branches in my branch model are specifically for code changes to be rolled directly to the release (and back to develop) branches.    It is the equivalent of a hotfix that goes on release instead of master.

While this is not a standard GitFlow model and thus I don’t have a one-button-click way to start and finish a patch release in SmartGit, the nomenclature “standard” keeps us all on the same page.

Configuring VirtualBox As A nginx RTMP Server

Intro

This is how I went about building a version of nginx that supports RTMP media streaming on an Ubuntu 16.04 VirtualBox.   It is not a simple plug-and-play exercise but anyone with basic Linux system admin skills can get this going.
Read More

#Apple #Music doesn’t want to buy #Tidal #streaming

Not surprising. Apple already has the user base. Doesn’t help make a solid business argument when you’re losing nearly $100M per year with no profitability in sight.

The current streaming business model does not work!

Apple Music doesn’t want to buy Tidal

http://www.businessinsider.com/apple-doesnt-want-to-buy-tidal-2016-9

The death of #localhost and the rise of #cloud #development (#coding #digitalnomads)

The biggest problem with could-centric development is the 100% loss of productivity when the network is down. While not an issue for most corporate lemmings pulling their 9-5, it is a notable issue for today’s digital nomads. While corporate techies will not go away, remote workers with flexible lifestyles and the inconsistent connectivity that goes with it will continue to grow. More tech workers will be connecting from outside the office. They may, or may not, have network access.

When you don’t have access to the network you cannot connect to your cloud service. You cannot work. With locally virtualized systems on the other hand, you have everything you need. As long as you have power or your battery life is good you can be productive. Next time you get back to civilization, meaning anywhere with a network connection, you push your work out to the world.

Cloud computing is the current way to do things in software but NOT for daily development. Unless you like having a reason to not do anything for an afternoon while your ISP restores the network or while you wait for the plane to get back on the ground.

—-
The death of localhost and the rise of cloud development

http://google.com/newsstand/s/CBIw3u-1wi0

%d bloggers like this: