Make WordPress Core

Changeset 39137


Ignore:
Timestamp:
11/04/2016 06:03:51 AM (10 years ago)
Author:
westonruter
Message:

Customize: Combine Ajax requests for initial load of available nav menu items into a single request.

When there are many post types registered, prevent Ajax requests from piling up and slamming WordPress with concurrent requests.

Props curdin, westonruter.
Fixes #36697.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/js/customize-nav-menus.js

    r39002 r39137  
    236236                                                }
    237237                                        } else {
    238                                                 self.loadItems( type, object );
     238                                                self.loadItems( [
     239                                                        { type: type, object: object }
     240                                                ] );
    239241                                        }
    240242                                }
     
    361363                        _.each( api.Menus.data.itemTypes, function( itemType ) {
    362364                                self.pages[ itemType.type + ':' + itemType.object ] = 0;
    363                                 self.loadItems( itemType.type, itemType.object ); // @todo we need to combine these Ajax requests.
    364365                        } );
    365                 },
    366 
    367                 // Load available menu items.
    368                 loadItems: function( type, object ) {
    369                         var self = this, params, request, itemTemplate, availableMenuItemContainer;
     366                        self.loadItems( api.Menus.data.itemTypes );
     367                },
     368
     369                /**
     370                 * Load available nav menu items.
     371                 *
     372                 * @since 4.3.0
     373                 * @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
     374                 * @access private
     375                 *
     376                 * @param {Array.<object>} itemTypes List of objects containing type and key.
     377                 * @param {string} deprecated Formerly the object parameter.
     378                 * @returns {void}
     379                 */
     380                loadItems: function( itemTypes, deprecated ) {
     381                        var self = this, _itemTypes, requestItemTypes = [], request, itemTemplate, availableMenuItemContainers = {};
    370382                        itemTemplate = wp.template( 'available-menu-item' );
    371383
    372                         if ( -1 === self.pages[ type + ':' + object ] ) {
     384                        if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
     385                                _itemTypes = [ { type: itemTypes, object: deprecated } ];
     386                        } else {
     387                                _itemTypes = itemTypes;
     388                        }
     389
     390                        _.each( _itemTypes, function( itemType ) {
     391                                var container, name = itemType.type + ':' + itemType.object;
     392                                if ( -1 === self.pages[ name ] ) {
     393                                        return; // Skip types for which there are no more results.
     394                                }
     395                                container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
     396                                container.find( '.accordion-section-title' ).addClass( 'loading' );
     397                                availableMenuItemContainers[ name ] = container;
     398
     399                                requestItemTypes.push( {
     400                                        object: itemType.object,
     401                                        type: itemType.type,
     402                                        page: self.pages[ name ]
     403                                } );
     404                        } );
     405
     406                        if ( 0 === requestItemTypes.length ) {
    373407                                return;
    374408                        }
    375                         availableMenuItemContainer = $( '#available-menu-items-' + type + '-' + object );
    376                         availableMenuItemContainer.find( '.accordion-section-title' ).addClass( 'loading' );
     409
    377410                        self.loading = true;
    378                         params = {
     411                        request = wp.ajax.post( 'load-available-menu-items-customizer', {
    379412                                'customize-menus-nonce': api.settings.nonce['customize-menus'],
    380413                                'wp_customize': 'on',
    381                                 'type': type,
    382                                 'object': object,
    383                                 'page': self.pages[ type + ':' + object ]
    384                         };
    385                         request = wp.ajax.post( 'load-available-menu-items-customizer', params );
     414                                'item_types': requestItemTypes
     415                        } );
    386416
    387417                        request.done(function( data ) {
    388                                 var items, typeInner;
    389                                 items = data.items;
    390                                 if ( 0 === items.length ) {
    391                                         if ( 0 === self.pages[ type + ':' + object ] ) {
    392                                                 availableMenuItemContainer
    393                                                         .addClass( 'cannot-expand' )
    394                                                         .removeClass( 'loading' )
    395                                                         .find( '.accordion-section-title > button' )
    396                                                         .prop( 'tabIndex', -1 );
    397                                         }
    398                                         self.pages[ type + ':' + object ] = -1;
    399                                         return;
    400                                 } else if ( ( 'page' === object ) && ( ! availableMenuItemContainer.hasClass( 'open' ) ) ) {
    401                                         availableMenuItemContainer.find( '.accordion-section-title > button' ).click();
    402                                 }
    403                                 items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away?
    404                                 self.collection.add( items.models );
    405                                 typeInner = availableMenuItemContainer.find( '.available-menu-items-list' );
    406                                 items.each(function( menuItem ) {
    407                                         typeInner.append( itemTemplate( menuItem.attributes ) );
     418                                var typeInner;
     419                                _.each( data.items, function( typeItems, name ) {
     420                                        if ( 0 === typeItems.length ) {
     421                                                if ( 0 === self.pages[ name ] ) {
     422                                                        availableMenuItemContainers[ name ].find( '.accordion-section-title' )
     423                                                                .addClass( 'cannot-expand' )
     424                                                                .removeClass( 'loading' )
     425                                                                .find( '.accordion-section-title > button' )
     426                                                                .prop( 'tabIndex', -1 );
     427                                                }
     428                                                self.pages[ name ] = -1;
     429                                                return;
     430                                        } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
     431                                                availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).click();
     432                                        }
     433                                        typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
     434                                        self.collection.add( typeItems.models );
     435                                        typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
     436                                        typeItems.each( function( menuItem ) {
     437                                                typeInner.append( itemTemplate( menuItem.attributes ) );
     438                                        } );
     439                                        self.pages[ name ] += 1;
    408440                                });
    409                                 self.pages[ type + ':' + object ] += 1;
    410441                        });
    411442                        request.fail(function( data ) {
     
    415446                        });
    416447                        request.always(function() {
    417                                 availableMenuItemContainer.find( '.accordion-section-title' ).removeClass( 'loading' );
     448                                _.each( availableMenuItemContainers, function( container ) {
     449                                        container.find( '.accordion-section-title' ).removeClass( 'loading' );
     450                                } );
    418451                                self.loading = false;
    419452                        });
  • trunk/src/wp-includes/class-wp-customize-nav-menus.php

    r39038 r39137  
    101101                }
    102102
    103                 if ( empty( $_POST['type'] ) || empty( $_POST['object'] ) ) {
     103                $all_items = array();
     104                $item_types = array();
     105                if ( isset( $_POST['item_types'] ) && is_array( $_POST['item_types'] ) ) {
     106                        $item_types = wp_unslash( $_POST['item_types'] );
     107                } elseif ( isset( $_POST['type'] ) && isset( $_POST['object'] ) ) { // Back compat.
     108                        $item_types[] = array(
     109                                'type' => wp_unslash( $_POST['type'] ),
     110                                'object' => wp_unslash( $_POST['object'] ),
     111                                'page' => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ),
     112                        );
     113                } else {
    104114                        wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
    105115                }
    106116
    107                 $type = sanitize_key( $_POST['type'] );
    108                 $object = sanitize_key( $_POST['object'] );
    109                 $page = empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] );
    110                 $items = $this->load_available_items_query( $type, $object, $page );
    111 
    112                 if ( is_wp_error( $items ) ) {
    113                         wp_send_json_error( $items->get_error_code() );
    114                 } else {
    115                         wp_send_json_success( array( 'items' => $items ) );
    116                 }
     117                foreach ( $item_types as $item_type ) {
     118                        if ( empty( $item_type['type'] ) || empty( $item_type['object'] ) ) {
     119                                wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
     120                        }
     121                        $type = sanitize_key( $item_type['type'] );
     122                        $object = sanitize_key( $item_type['object'] );
     123                        $page = empty( $item_type['page'] ) ? 0 : absint( $item_type['page'] );
     124                        $items = $this->load_available_items_query( $type, $object, $page );
     125                        if ( is_wp_error( $items ) ) {
     126                                wp_send_json_error( $items->get_error_code() );
     127                        }
     128                        $all_items[ $item_type['type'] . ':' . $item_type['object'] ] = $items;
     129                }
     130
     131                wp_send_json_success( array( 'items' => $all_items ) );
    117132        }
    118133
  • trunk/tests/phpunit/tests/ajax/CustomizeMenus.php

    r39038 r39137  
    175175                        array(
    176176                                array(
     177                                        'type'     => 'post_type',
     178                                        'object'   => '',
     179                                ),
     180                                array(
     181                                        'success'  => false,
     182                                        'data'     => 'nav_menus_missing_type_or_object_parameter',
     183                                ),
     184                        ),
     185                        // Testing empty type.
     186                        array(
     187                                array(
    177188                                        'type'     => '',
    178189                                        'object'   => 'post',
     
    183194                                ),
    184195                        ),
    185                         // Testing empty type.
    186                         array(
    187                                 array(
    188                                         'type'     => '',
    189                                         'object'   => 'post',
     196                        // Testing empty type of a bulk request.
     197                        array(
     198                                array(
     199                                        'item_types' => array(
     200                                                array(
     201                                                        'type'     => 'post_type',
     202                                                        'object'   => 'post',
     203                                                ),
     204                                                array(
     205                                                        'type'     => 'post_type',
     206                                                        'object'   => '',
     207                                                ),
     208                                        ),
    190209                                ),
    191210                                array(
     
    274293                                        'type'     => 'taxonomy',
    275294                                        'object'   => 'post_tag',
     295                                ),
     296                                true,
     297                        ),
     298                        // Testing a bulk request.
     299                        array(
     300                                array(
     301                                        'item_types' => array(
     302                                                array(
     303                                                        'type'     => 'post_type',
     304                                                        'object'   => 'post',
     305                                                ),
     306                                                array(
     307                                                        'type'     => 'post_type',
     308                                                        'object'   => 'page',
     309                                                ),
     310                                        ),
    276311                                ),
    277312                                true,
     
    314349                $response = json_decode( $this->_last_response, true );
    315350
    316                 $this->assertNotEmpty( $response['data']['items'] );
     351                $this->assertNotEmpty( current( $response['data']['items'] ) );
    317352
    318353                // Get the second index to avoid the home page edge case.
    319                 $test_item = $response['data']['items'][1];
     354                $first_prop = current( $response['data']['items'] );
     355                $test_item = $first_prop[1];
    320356
    321357                foreach ( $expected_keys as $key ) {
     
    326362                // Special test for the home page.
    327363                if ( 'page' === $test_item['object'] ) {
    328                         $home = $response['data']['items'][0];
     364                        $first_prop = current( $response['data']['items'] );
     365                        $home = $first_prop[0];
    329366                        foreach ( $expected_keys as $key ) {
    330367                                if ( 'object_id' !== $key ) {
Note: See TracChangeset for help on using the changeset viewer.