-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Navigation screen: Atomic save using customizer API endpoint #22603
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
Merged
draganescu
merged 50 commits into
master
from
add/navigation-atomic-save-customizer-endpoint
May 29, 2020
Merged
Changes from all commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
e6cee9d
Squash
adamziel e48e4e3
Refactor useNavigationBlocks
adamziel b986510
First version of functional batch saving
adamziel 0975ff5
Call receiveEntityRecords with proper query
adamziel 4f780c9
Rename /save-hierarchy to /batch
adamziel 8964b29
Restore the original version of create_item function
adamziel c03ad4c
Call proper hooks and actions during batch delete and update
adamziel 047b70b
Cleanup batch processing code
adamziel ac860da
Remove MySQL transaction for now
adamziel 8af061a
Add actions
adamziel f30f92b
Clean up naming, add a few comments
adamziel 212acea
Add more documentation
adamziel 79ae366
sort menu items received from the API
adamziel 19dd479
Simplify validate functions signatures
adamziel 3b7e7ce
Restore the previous version of prepare_item_for_database
adamziel fae159e
Formatting
adamziel 6dd5ff5
Formatting
adamziel 87208e2
Remove the Operation abstraction
adamziel 591e142
Formatting
adamziel 1dc4372
Remove additional input argument, use just request
adamziel 49b449b
Formatting
adamziel 8b4f938
input->request
adamziel 115900b
Provide information to the client about the specific input that cause…
adamziel d9007ea
Clean pass through phpcs
adamziel 8695654
Clean pass through existing unit tests
adamziel 62682c4
Add initial unit test
adamziel e24b116
Add a few more tests
adamziel ba700d2
Use the existing customizer endpoint for batch saving of menu items
adamziel aa4a854
Basic batch save
adamziel f52abdc
Revert PHP changes
adamziel ee0c409
Add Nonce endpoint, simplify the batch save handler
adamziel 4464c0a
Properly fetch nonce
adamziel bd273df
Simplify batchSave even further
adamziel 48b0a76
Silence eslint in uuidv4()
adamziel 68aef18
Update comment in WP_Rest_Customizer_Nonces endpoint
adamziel db7caa4
Lint
adamziel 4561c9d
Simplify PromiseQueue
adamziel e08a2c6
Unshift -> shift
adamziel 64c9aa5
Correctly send information about deleted menu items
adamziel ce5bfc2
Keep track of deleted menu items in a hacky way
adamziel 13672c1
Update comment
adamziel fb8a430
Update comment
adamziel ce8f2f6
Update comments
adamziel af17f7a
Whitespace
adamziel 5195a2d
Update comments and simplify
adamziel 28eaf31
Fix re-appearing deleted menu items
adamziel b6315f4
Use uniq() to de-duplicate items returned from select() - due to a bu…
adamziel 4c3e129
Remove uniq and filter
adamziel 69001ac
Add permissions_check to the nonce endpoint
adamziel 1052525
Lint
adamziel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
|
|
||
| /** | ||
| * 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
|
||
| ); | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
packages/edit-navigation/src/components/menu-editor/batch-save.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 ); | ||
| } ); | ||
| } |
51 changes: 51 additions & 0 deletions
51
packages/edit-navigation/src/components/menu-editor/promise-queue.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } | ||
| } |
18 changes: 18 additions & 0 deletions
18
packages/edit-navigation/src/components/menu-editor/use-debounced-value.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.