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.


git-svn-id: https://develop.svn.wordpress.org/trunk@39137 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Weston Ruter 2016-11-04 06:03:51 +00:00
parent 71f8622cdc
commit d6c217886a
3 changed files with 135 additions and 50 deletions

View File

@ -235,7 +235,9 @@
self.doSearch( self.pages.search ); self.doSearch( self.pages.search );
} }
} else { } else {
self.loadItems( type, object ); self.loadItems( [
{ type: type, object: object }
] );
} }
} }
}); });
@ -360,53 +362,82 @@
// Render the template for each item by type. // Render the template for each item by type.
_.each( api.Menus.data.itemTypes, function( itemType ) { _.each( api.Menus.data.itemTypes, function( itemType ) {
self.pages[ itemType.type + ':' + itemType.object ] = 0; self.pages[ itemType.type + ':' + itemType.object ] = 0;
self.loadItems( itemType.type, itemType.object ); // @todo we need to combine these Ajax requests.
} ); } );
self.loadItems( api.Menus.data.itemTypes );
}, },
// Load available menu items. /**
loadItems: function( type, object ) { * Load available nav menu items.
var self = this, params, request, itemTemplate, availableMenuItemContainer; *
* @since 4.3.0
* @since 4.7.0 Changed function signature to take list of item types instead of single type/object.
* @access private
*
* @param {Array.<object>} itemTypes List of objects containing type and key.
* @param {string} deprecated Formerly the object parameter.
* @returns {void}
*/
loadItems: function( itemTypes, deprecated ) {
var self = this, _itemTypes, requestItemTypes = [], request, itemTemplate, availableMenuItemContainers = {};
itemTemplate = wp.template( 'available-menu-item' ); itemTemplate = wp.template( 'available-menu-item' );
if ( -1 === self.pages[ type + ':' + object ] ) { if ( _.isString( itemTypes ) && _.isString( deprecated ) ) {
_itemTypes = [ { type: itemTypes, object: deprecated } ];
} else {
_itemTypes = itemTypes;
}
_.each( _itemTypes, function( itemType ) {
var container, name = itemType.type + ':' + itemType.object;
if ( -1 === self.pages[ name ] ) {
return; // Skip types for which there are no more results.
}
container = $( '#available-menu-items-' + itemType.type + '-' + itemType.object );
container.find( '.accordion-section-title' ).addClass( 'loading' );
availableMenuItemContainers[ name ] = container;
requestItemTypes.push( {
object: itemType.object,
type: itemType.type,
page: self.pages[ name ]
} );
} );
if ( 0 === requestItemTypes.length ) {
return; return;
} }
availableMenuItemContainer = $( '#available-menu-items-' + type + '-' + object );
availableMenuItemContainer.find( '.accordion-section-title' ).addClass( 'loading' );
self.loading = true; self.loading = true;
params = { request = wp.ajax.post( 'load-available-menu-items-customizer', {
'customize-menus-nonce': api.settings.nonce['customize-menus'], 'customize-menus-nonce': api.settings.nonce['customize-menus'],
'wp_customize': 'on', 'wp_customize': 'on',
'type': type, 'item_types': requestItemTypes
'object': object, } );
'page': self.pages[ type + ':' + object ]
};
request = wp.ajax.post( 'load-available-menu-items-customizer', params );
request.done(function( data ) { request.done(function( data ) {
var items, typeInner; var typeInner;
items = data.items; _.each( data.items, function( typeItems, name ) {
if ( 0 === items.length ) { if ( 0 === typeItems.length ) {
if ( 0 === self.pages[ type + ':' + object ] ) { if ( 0 === self.pages[ name ] ) {
availableMenuItemContainer availableMenuItemContainers[ name ].find( '.accordion-section-title' )
.addClass( 'cannot-expand' ) .addClass( 'cannot-expand' )
.removeClass( 'loading' ) .removeClass( 'loading' )
.find( '.accordion-section-title > button' ) .find( '.accordion-section-title > button' )
.prop( 'tabIndex', -1 ); .prop( 'tabIndex', -1 );
} }
self.pages[ type + ':' + object ] = -1; self.pages[ name ] = -1;
return; return;
} else if ( ( 'page' === object ) && ( ! availableMenuItemContainer.hasClass( 'open' ) ) ) { } else if ( ( 'post_type:page' === name ) && ( ! availableMenuItemContainers[ name ].hasClass( 'open' ) ) ) {
availableMenuItemContainer.find( '.accordion-section-title > button' ).click(); availableMenuItemContainers[ name ].find( '.accordion-section-title > button' ).click();
} }
items = new api.Menus.AvailableItemCollection( items ); // @todo Why is this collection created and then thrown away? typeItems = new api.Menus.AvailableItemCollection( typeItems ); // @todo Why is this collection created and then thrown away?
self.collection.add( items.models ); self.collection.add( typeItems.models );
typeInner = availableMenuItemContainer.find( '.available-menu-items-list' ); typeInner = availableMenuItemContainers[ name ].find( '.available-menu-items-list' );
items.each(function( menuItem ) { typeItems.each( function( menuItem ) {
typeInner.append( itemTemplate( menuItem.attributes ) ); typeInner.append( itemTemplate( menuItem.attributes ) );
} );
self.pages[ name ] += 1;
}); });
self.pages[ type + ':' + object ] += 1;
}); });
request.fail(function( data ) { request.fail(function( data ) {
if ( typeof console !== 'undefined' && console.error ) { if ( typeof console !== 'undefined' && console.error ) {
@ -414,7 +445,9 @@
} }
}); });
request.always(function() { request.always(function() {
availableMenuItemContainer.find( '.accordion-section-title' ).removeClass( 'loading' ); _.each( availableMenuItemContainers, function( container ) {
container.find( '.accordion-section-title' ).removeClass( 'loading' );
} );
self.loading = false; self.loading = false;
}); });
}, },

View File

@ -100,20 +100,35 @@ final class WP_Customize_Nav_Menus {
wp_die( -1 ); wp_die( -1 );
} }
if ( empty( $_POST['type'] ) || empty( $_POST['object'] ) ) { $all_items = array();
$item_types = array();
if ( isset( $_POST['item_types'] ) && is_array( $_POST['item_types'] ) ) {
$item_types = wp_unslash( $_POST['item_types'] );
} elseif ( isset( $_POST['type'] ) && isset( $_POST['object'] ) ) { // Back compat.
$item_types[] = array(
'type' => wp_unslash( $_POST['type'] ),
'object' => wp_unslash( $_POST['object'] ),
'page' => empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ),
);
} else {
wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' ); wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
} }
$type = sanitize_key( $_POST['type'] ); foreach ( $item_types as $item_type ) {
$object = sanitize_key( $_POST['object'] ); if ( empty( $item_type['type'] ) || empty( $item_type['object'] ) ) {
$page = empty( $_POST['page'] ) ? 0 : absint( $_POST['page'] ); wp_send_json_error( 'nav_menus_missing_type_or_object_parameter' );
}
$type = sanitize_key( $item_type['type'] );
$object = sanitize_key( $item_type['object'] );
$page = empty( $item_type['page'] ) ? 0 : absint( $item_type['page'] );
$items = $this->load_available_items_query( $type, $object, $page ); $items = $this->load_available_items_query( $type, $object, $page );
if ( is_wp_error( $items ) ) { if ( is_wp_error( $items ) ) {
wp_send_json_error( $items->get_error_code() ); wp_send_json_error( $items->get_error_code() );
} else {
wp_send_json_success( array( 'items' => $items ) );
} }
$all_items[ $item_type['type'] . ':' . $item_type['object'] ] = $items;
}
wp_send_json_success( array( 'items' => $all_items ) );
} }
/** /**

View File

@ -174,8 +174,8 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase {
// Testing empty obj_type. // Testing empty obj_type.
array( array(
array( array(
'type' => '', 'type' => 'post_type',
'object' => 'post', 'object' => '',
), ),
array( array(
'success' => false, 'success' => false,
@ -193,6 +193,25 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase {
'data' => 'nav_menus_missing_type_or_object_parameter', 'data' => 'nav_menus_missing_type_or_object_parameter',
), ),
), ),
// Testing empty type of a bulk request.
array(
array(
'item_types' => array(
array(
'type' => 'post_type',
'object' => 'post',
),
array(
'type' => 'post_type',
'object' => '',
),
),
),
array(
'success' => false,
'data' => 'nav_menus_missing_type_or_object_parameter',
),
),
// Testing incorrect type option. // Testing incorrect type option.
array( array(
array( array(
@ -276,6 +295,22 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase {
), ),
true, true,
), ),
// Testing a bulk request.
array(
array(
'item_types' => array(
array(
'type' => 'post_type',
'object' => 'post',
),
array(
'type' => 'post_type',
'object' => 'page',
),
),
),
true,
),
); );
} }
@ -313,10 +348,11 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase {
// Get the results. // Get the results.
$response = json_decode( $this->_last_response, true ); $response = json_decode( $this->_last_response, true );
$this->assertNotEmpty( $response['data']['items'] ); $this->assertNotEmpty( current( $response['data']['items'] ) );
// Get the second index to avoid the home page edge case. // Get the second index to avoid the home page edge case.
$test_item = $response['data']['items'][1]; $first_prop = current( $response['data']['items'] );
$test_item = $first_prop[1];
foreach ( $expected_keys as $key ) { foreach ( $expected_keys as $key ) {
$this->assertArrayHasKey( $key, $test_item ); $this->assertArrayHasKey( $key, $test_item );
@ -325,7 +361,8 @@ class Tests_Ajax_CustomizeMenus extends WP_Ajax_UnitTestCase {
// Special test for the home page. // Special test for the home page.
if ( 'page' === $test_item['object'] ) { if ( 'page' === $test_item['object'] ) {
$home = $response['data']['items'][0]; $first_prop = current( $response['data']['items'] );
$home = $first_prop[0];
foreach ( $expected_keys as $key ) { foreach ( $expected_keys as $key ) {
if ( 'object_id' !== $key ) { if ( 'object_id' !== $key ) {
$this->assertArrayHasKey( $key, $home ); $this->assertArrayHasKey( $key, $home );