Make WordPress Core

Changeset 39137


Ignore:
Timestamp:
11/04/2016 06:03:51 AM (8 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.