Skip to content

Commit

Permalink
First stab at saving menu items using the REST API
Browse files Browse the repository at this point in the history
  • Loading branch information
adamziel committed Sep 3, 2021
1 parent e8a4228 commit ae9dba0
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 45 deletions.
3 changes: 2 additions & 1 deletion packages/data/src/redux-store/thunk-middleware.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export default function createThunkMiddleware( args ) {
return () => ( next ) => ( action ) => {
if ( typeof action === 'function' ) {
return action( args );
const retval = action( args );
return retval;
}

return next( action );
Expand Down
153 changes: 109 additions & 44 deletions packages/edit-navigation/src/store/actions.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
/**
* External dependencies
*/
import { invert } from 'lodash';
import { v4 as uuid } from 'uuid';
import { invert, omit } from 'lodash';

/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { serialize } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import {
getMenuItemToClientIdMapping,
resolveMenuItems,
dispatch,
select,
apiFetch,
dispatch as registryDispatch,
select as registrySelect,
apiFetch as apiFetchControl,
} from './controls';
import { NAVIGATION_POST_KIND, NAVIGATION_POST_POST_TYPE } from '../constants';
import {
menuItemsQuery,
serializeProcessing,
computeCustomizedAttribute,
blockAttributesToMenuItem,
} from './utils';

const { ajaxurl } = window;

/**
* Returns an action object used to select menu.
*
Expand Down Expand Up @@ -59,7 +57,7 @@ export const createMissingMenuItems = serializeProcessing( function* ( post ) {
while ( stack.length ) {
const block = stack.pop();
if ( ! ( block.clientId in clientIdToMenuId ) ) {
const menuItem = yield apiFetch( {
const menuItem = yield apiFetchControl( {
path: `/__experimental/menu-items`,
method: 'POST',
data: {
Expand All @@ -71,7 +69,7 @@ export const createMissingMenuItems = serializeProcessing( function* ( post ) {

mapping[ menuItem.id ] = block.clientId;
const menuItems = yield resolveMenuItems( menuId );
yield dispatch(
yield registryDispatch(
'core',
'receiveEntityRecords',
'root',
Expand Down Expand Up @@ -106,15 +104,15 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) {

try {
// Save edits to the menu, like the menu name.
yield dispatch(
yield registryDispatch(
'core',
'saveEditedEntityRecord',
'root',
'menu',
menuId
);

const error = yield select(
const error = yield registrySelect(
'core',
'getLastEntitySaveError',
'root',
Expand All @@ -126,6 +124,8 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) {
throw new Error( error.message );
}

// saveEntityRecord for each menu item with block-based data
// saveEntityRecord for each deleted menu item
// Save blocks as menu items.
const batchSaveResponse = yield* batchSave(
menuId,
Expand All @@ -138,7 +138,7 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) {
}

// Clear "stub" navigation post edits to avoid a false "dirty" state.
yield dispatch(
yield registryDispatch(
'core',
'receiveEntityRecords',
NAVIGATION_POST_KIND,
Expand All @@ -147,7 +147,7 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) {
undefined
);

yield dispatch(
yield registryDispatch(
noticesStore,
'createSuccessNotice',
__( 'Navigation saved.' ),
Expand All @@ -163,9 +163,14 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) {
saveError.message
)
: __( 'Unable to save: An error ocurred.' );
yield dispatch( noticesStore, 'createErrorNotice', errorMessage, {
type: 'snackbar',
} );
yield registryDispatch(
noticesStore,
'createErrorNotice',
errorMessage,
{
type: 'snackbar',
}
);
}
} );

Expand All @@ -184,34 +189,94 @@ function mapMenuItemsByClientId( menuItems, clientIdsByMenuId ) {
}

function* batchSave( menuId, menuItemsByClientId, navigationBlock ) {
const { nonce, stylesheet } = yield apiFetch( {
path: '/__experimental/customizer-nonces/get-save-nonce',
} );
if ( ! nonce ) {
throw new Error();
const blocksList = blocksTreeToFlatList( navigationBlock.innerBlocks );

const batchTasks = [];
// Enqueue updates
for ( const { block, parentId, position } of blocksList ) {
const menuItem = getMenuItemForBlock( block );

// Update an existing navigation item.
yield registryDispatch(
'core',
'editEntityRecord',
'root',
'menuItem',
menuItem.id,
blockToEntityRecord( block, parentId, position ),
{ undoIgnore: true }
);

const hasEdits = yield registrySelect(
'core',
'hasEditsForEntityRecord',
'root',
'menuItem',
menuItem.id
);

if ( ! hasEdits ) {
continue;
}

batchTasks.push( ( { saveEditedEntityRecord } ) =>
saveEditedEntityRecord( 'root', 'menuItem', menuItem.id )
);
}
return yield registryDispatch( 'core', '__experimentalBatch', batchTasks );

// 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', uuid() );
body.append( 'customize_autosaved', 'on' );
body.append( 'customize_changeset_status', 'publish' );
body.append( 'action', 'customize_save' );
body.append(
'customized',
computeCustomizedAttribute(
navigationBlock.innerBlocks,
menuId,
menuItemsByClientId
)
);
// Enqueue deletes
// @TODO

// 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 menuItemsByClientId ) {
// const key = computeKey( menuItemsByClientId[ clientId ] );
// if ( ! ( key in dataObject ) ) {
// dataObject[ key ] = false;
// }
// }

function blockToEntityRecord( block, parentId, position ) {
const menuItem = omit( getMenuItemForBlock( block ), 'menus', 'meta' );

let attributes;

return yield apiFetch( {
url: ajaxurl || '/wp-admin/admin-ajax.php',
method: 'POST',
body,
} );
if ( block.name === 'core/navigation-link' ) {
attributes = blockAttributesToMenuItem( block.attributes );
} else {
attributes = {
type: 'block',
content: serialize( block ),
};
}

return {
...menuItem,
...attributes,
position,
nav_menu_term_id: menuId,
menu_item_parent: parentId,
status: 'publish',
_invalid: false,
};
}

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

function getMenuItemForBlock( block ) {
return omit( menuItemsByClientId[ block.clientId ] || {}, '_links' );
}
}
1 change: 1 addition & 0 deletions packages/edit-navigation/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const storeConfig = {
resolvers,
actions,
persist: [ 'selectedMenuId' ],
__experimentalUseThunks: true,
};

/**
Expand Down

0 comments on commit ae9dba0

Please sign in to comment.