Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation screen: Atomic save using customizer API endpoint #22603

Merged
merged 50 commits into from
May 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e6cee9d
Squash
adamziel May 6, 2020
e48e4e3
Refactor useNavigationBlocks
adamziel May 14, 2020
b986510
First version of functional batch saving
adamziel May 14, 2020
0975ff5
Call receiveEntityRecords with proper query
adamziel May 14, 2020
4f780c9
Rename /save-hierarchy to /batch
adamziel May 14, 2020
8964b29
Restore the original version of create_item function
adamziel May 14, 2020
c03ad4c
Call proper hooks and actions during batch delete and update
adamziel May 14, 2020
047b70b
Cleanup batch processing code
adamziel May 14, 2020
ac860da
Remove MySQL transaction for now
adamziel May 14, 2020
8af061a
Add actions
adamziel May 14, 2020
f30f92b
Clean up naming, add a few comments
adamziel May 14, 2020
212acea
Add more documentation
adamziel May 14, 2020
79ae366
sort menu items received from the API
adamziel May 14, 2020
19dd479
Simplify validate functions signatures
adamziel May 14, 2020
3b7e7ce
Restore the previous version of prepare_item_for_database
adamziel May 14, 2020
fae159e
Formatting
adamziel May 14, 2020
6dd5ff5
Formatting
adamziel May 14, 2020
87208e2
Remove the Operation abstraction
adamziel May 14, 2020
591e142
Formatting
adamziel May 14, 2020
1dc4372
Remove additional input argument, use just request
adamziel May 14, 2020
49b449b
Formatting
adamziel May 14, 2020
8b4f938
input->request
adamziel May 14, 2020
115900b
Provide information to the client about the specific input that cause…
adamziel May 14, 2020
d9007ea
Clean pass through phpcs
adamziel May 15, 2020
8695654
Clean pass through existing unit tests
adamziel May 15, 2020
62682c4
Add initial unit test
adamziel May 15, 2020
e24b116
Add a few more tests
adamziel May 15, 2020
ba700d2
Use the existing customizer endpoint for batch saving of menu items
adamziel May 25, 2020
aa4a854
Basic batch save
adamziel May 25, 2020
f52abdc
Revert PHP changes
adamziel May 25, 2020
ee0c409
Add Nonce endpoint, simplify the batch save handler
adamziel May 26, 2020
4464c0a
Properly fetch nonce
adamziel May 26, 2020
bd273df
Simplify batchSave even further
adamziel May 26, 2020
48b0a76
Silence eslint in uuidv4()
adamziel May 26, 2020
68aef18
Update comment in WP_Rest_Customizer_Nonces endpoint
adamziel May 26, 2020
db7caa4
Lint
adamziel May 26, 2020
4561c9d
Simplify PromiseQueue
adamziel May 26, 2020
e08a2c6
Unshift -> shift
adamziel May 26, 2020
64c9aa5
Correctly send information about deleted menu items
adamziel May 26, 2020
ce5bfc2
Keep track of deleted menu items in a hacky way
adamziel May 26, 2020
13672c1
Update comment
adamziel May 26, 2020
fb8a430
Update comment
adamziel May 26, 2020
ce8f2f6
Update comments
adamziel May 26, 2020
af17f7a
Whitespace
adamziel May 26, 2020
5195a2d
Update comments and simplify
adamziel May 26, 2020
28eaf31
Fix re-appearing deleted menu items
adamziel May 28, 2020
b6315f4
Use uniq() to de-duplicate items returned from select() - due to a bu…
adamziel May 28, 2020
4c3e129
Remove uniq and filter
adamziel May 29, 2020
69001ac
Add permissions_check to the nonce endpoint
adamziel May 29, 2020
1052525
Lint
adamziel May 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions lib/class-wp-rest-customizer-nonces.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php
/**
* WP_Rest_Customizer_Nonces class.
*
* @package gutenberg
*/

/**
* Class that returns the customizer "save" nonce that's required for the
* batch save operation using the customizer API endpoint.
*/
class WP_Rest_Customizer_Nonces extends WP_REST_Controller {
draganescu marked this conversation as resolved.
Show resolved Hide resolved

/**
* Constructor.
*/
public function __construct() {
$this->namespace = '__experimental';
$this->rest_base = 'customizer-nonces';
}

/**
* Registers the necessary REST API routes.
*
* @access public
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/get-save-nonce',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_save_nonce' ),
'permission_callback' => array( $this, 'permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Checks if a given request has access to read menu items if they have access to edit them.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function permissions_check( $request ) {
$post_type = get_post_type_object( 'nav_menu_item' );
if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}

/**
* Returns the nonce required to request the customizer API endpoint.
*
* @access public
*/
public function get_save_nonce() {
require_once ABSPATH . 'wp-includes/class-wp-customize-manager.php';
$wp_customize = new WP_Customize_Manager();
$nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() );
return array(
'success' => true,
'nonce' => $nonce,
'stylesheet' => $wp_customize->get_stylesheet(),
draganescu marked this conversation as resolved.
Show resolved Hide resolved
);
}

}
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ function gutenberg_is_experiment_enabled( $name ) {
if ( ! class_exists( 'WP_REST_Menu_Locations_Controller' ) ) {
require_once dirname( __FILE__ ) . '/class-wp-rest-menu-locations-controller.php';
}
if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) {
require_once dirname( __FILE__ ) . '/class-wp-rest-customizer-nonces.php';
}
/**
* End: Include for phase 2
*/
Expand Down
10 changes: 10 additions & 0 deletions lib/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ function gutenberg_register_rest_menu_location() {
$nav_menu_location->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_menu_location' );

/**
* Registers the menu locations area REST API routes.
*/
function gutenberg_register_rest_customizer_nonces() {
$nav_menu_location = new WP_Rest_Customizer_Nonces();
$nav_menu_location->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_customizer_nonces' );

/**
* Hook in to the nav menu item post type and enable a post type rest endpoint.
*
Expand Down
108 changes: 108 additions & 0 deletions packages/edit-navigation/src/components/menu-editor/batch-save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* External dependencies
*/
import { keyBy, omit } from 'lodash';

/**
* WordPress dependencies
*/
import apiFetch from '@wordpress/api-fetch';

export default async function batchSave(
menuId,
menuItemsRef,
navigationBlock
) {
const { nonce, stylesheet } = await apiFetch( {
path: '/__experimental/customizer-nonces/get-save-nonce',
} );

// eslint-disable-next-line no-undef
const body = new FormData();
body.append( 'wp_customize', 'on' );
body.append( 'customize_theme', stylesheet );
body.append( 'nonce', nonce );
body.append( 'customize_changeset_uuid', uuidv4() );
body.append( 'customize_autosaved', 'on' );
body.append( 'customize_changeset_status', 'publish' );
body.append( 'action', 'customize_save' );
body.append(
'customized',
computeCustomizedAttribute(
navigationBlock.innerBlocks,
menuId,
menuItemsRef
)
);

return await apiFetch( {
url: '/wp-admin/admin-ajax.php',
method: 'POST',
body,
} );
}

function computeCustomizedAttribute( blocks, menuId, menuItemsRef ) {
const blocksList = blocksTreeToFlatList( blocks );
const dataList = blocksList.map( ( { block, parentId, position } ) =>
linkBlockToRequestItem( block, parentId, position )
);

// Create an object like { "nav_menu_item[12]": {...}} }
const computeKey = ( item ) => `nav_menu_item[${ item.id }]`;
const dataObject = keyBy( dataList, computeKey );

// Deleted menu items should be sent as false, e.g. { "nav_menu_item[13]": false }
for ( const clientId in menuItemsRef.current ) {
const key = computeKey( menuItemsRef.current[ clientId ] );
if ( ! ( key in dataObject ) ) {
dataObject[ key ] = false;
}
}

return JSON.stringify( dataObject );

function blocksTreeToFlatList( innerBlocks, parentId = 0 ) {
return innerBlocks.flatMap( ( block, index ) =>
[ { block, parentId, position: index + 1 } ].concat(
blocksTreeToFlatList(
block.innerBlocks,
getMenuItemForBlock( block )?.id
)
)
);
}

function linkBlockToRequestItem( block, parentId, position ) {
const menuItem = omit( getMenuItemForBlock( block ), 'menus', 'meta' );
return {
...menuItem,
position,
title: block.attributes?.label,
url: block.attributes.url,
original_title: '',
classes: ( menuItem.classes || [] ).join( ' ' ),
xfn: ( menuItem.xfn || [] ).join( ' ' ),
nav_menu_term_id: menuId,
menu_item_parent: parentId,
status: 'publish',
_invalid: false,
};
}

function getMenuItemForBlock( block ) {
return omit( menuItemsRef.current[ block.clientId ] || {}, '_links' );
}
}

function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, ( c ) => {
// eslint-disable-next-line no-restricted-syntax
const a = Math.random() * 16;
// eslint-disable-next-line no-bitwise
const r = a | 0;
// eslint-disable-next-line no-bitwise
const v = c === 'x' ? r : ( r & 0x3 ) | 0x8;
return v.toString( 16 );
} );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* A concurrency primitive that runs at most `concurrency` async tasks at once.
*/
export default class PromiseQueue {
constructor( concurrency = 1 ) {
this.concurrency = concurrency;
this.queue = [];
this.active = [];
this.listeners = [];
}

enqueue( action ) {
this.queue.push( action );
this.run();
}

run() {
while ( this.queue.length && this.active.length <= this.concurrency ) {
const action = this.queue.shift();
const promise = action().then( () => {
this.active.splice( this.active.indexOf( promise ), 1 );
this.run();
this.notifyIfEmpty();
} );
this.active.push( promise );
}
}

notifyIfEmpty() {
if ( this.active.length === 0 && this.queue.length === 0 ) {
for ( const l of this.listeners ) {
l();
}
this.listeners = [];
}
}

/**
* Calls `callback` once all async actions in the queue are finished,
* or immediately if no actions are running.
*
* @param {Function} callback Callback to call
*/
then( callback ) {
if ( this.active.length ) {
this.listeners.push( callback );
} else {
callback();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* WordPress dependencies
*/
import { useEffect, useState } from '@wordpress/element';

const useDebouncedValue = ( value, timeout ) => {
const [ state, setState ] = useState( value );

useEffect( () => {
const handler = setTimeout( () => setState( value ), timeout );

return () => clearTimeout( handler );
}, [ value, timeout ] );

return state;
};

export default useDebouncedValue;
Loading