Skip to content

Commit

Permalink
Meta Boxes: Changing the way we save metaboxes
Browse files Browse the repository at this point in the history
Hidden metaboxes (side) were not saved before
  • Loading branch information
youknowriad committed Jan 15, 2018
1 parent 0cb6eae commit 13f869e
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 299 deletions.
101 changes: 31 additions & 70 deletions editor/components/meta-boxes/meta-boxes-area/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,122 +3,83 @@
*/
import classnames from 'classnames';
import { connect } from 'react-redux';
import jQuery from 'jquery';

/**
* WordPress dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { Component } from '@wordpress/element';
import { Spinner } from '@wordpress/components';

/**
* Internal dependencies
*/
import './style.scss';
import { handleMetaBoxReload, metaBoxLoaded } from '../../../store/actions';
import { getMetaBox, isSavingPost } from '../../../store/selectors';
import { isSavingMetaBoxes } from '../../../store/selectors';

class MetaBoxesArea extends Component {
/**
* @inheritdoc
*/
constructor() {
super( ...arguments );

this.state = {
loading: false,
};
this.originalFormData = '';
this.bindNode = this.bindNode.bind( this );
}

bindNode( node ) {
this.node = node;
}

/**
* @inheritdoc
*/
componentDidMount() {
this.mounted = true;
this.fetchMetaboxes();
this.form = document.querySelector( '.metabox-location-' + this.props.location );
this.node.appendChild( this.form );
}

/**
* Get the meta box location form from the original location.
*/
componentWillUnmount() {
this.mounted = false;
document.querySelector( '#metaboxes' ).appendChild( this.form );
}

componentWillReceiveProps( nextProps ) {
if ( nextProps.isUpdating && ! this.props.isUpdating ) {
this.setState( { loading: true } );
const { location } = nextProps;
const headers = new window.Headers();
const fetchOptions = {
method: 'POST',
headers,
body: new window.FormData( this.form ),
credentials: 'include',
};

// Save the metaboxes
window.fetch( addQueryArgs( window._wpMetaBoxUrl, { meta_box: location } ), fetchOptions )
.then( () => {
if ( ! this.mounted ) {
return false;
}
this.setState( { loading: false } );
this.props.metaBoxReloaded( location );
} );
}
}

fetchMetaboxes() {
const { location } = this.props;
this.form = document.querySelector( '.metabox-location-' + location );
this.node.appendChild( this.form );
this.form.onSubmit = ( event ) => event.preventDefault();
this.originalFormData = this.getFormData();
this.props.metaBoxLoaded( location );
}

getFormData() {
return jQuery( this.form ).serialize();
/**
* Binds the metabox area container node.
*
* @param {Element} node DOM Node.
*/
bindNode( node ) {
this.node = node;
}

/**
* @inheritdoc
*/
render() {
const { location } = this.props;
const { loading } = this.state;
const { location, isSaving } = this.props;

const classes = classnames(
'editor-meta-boxes-area',
`is-${ location }`,
{
'is-loading': loading,
'is-loading': isSaving,
}
);

return (
<div className={ classes }>
{ loading && <Spinner /> }
{ isSaving && <Spinner /> }
<div className="editor-meta-boxes-area__container" ref={ this.bindNode } />
<div className="editor-meta-boxes-area__clear" />
</div>
);
}
}

function mapStateToProps( state, ownProps ) {
const metaBox = getMetaBox( state, ownProps.location );
const { isUpdating } = metaBox;

return {
isUpdating,
isPostSaving: isSavingPost( state ) ? true : false,
};
}

function mapDispatchToProps( dispatch ) {
/**
* @inheritdoc
*/
function mapStateToProps( state ) {
return {
// Used to set the reference to the MetaBox in redux, fired when the component mounts.
metaBoxReloaded: ( location ) => dispatch( handleMetaBoxReload( location ) ),
metaBoxLoaded: ( location ) => dispatch( metaBoxLoaded( location ) ),
isSaving: isSavingMetaBoxes( state ),
};
}

export default connect( mapStateToProps, mapDispatchToProps )( MetaBoxesArea );
export default connect( mapStateToProps )( MetaBoxesArea );
6 changes: 6 additions & 0 deletions editor/components/post-saved-state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import {
hasMetaBoxes,
} from '../../store/selectors';

/**
* Component showing whether the post is saved or not and displaying save links.
*
* @param {Object} Props Component Props.
* @returns {WPElement} WordPress Element.
*/
export function PostSavedState( { hasActiveMetaboxes, isNew, isPublished, isDirty, isSaving, isSaveable, status, onStatusChange, onSave } ) {
const className = 'editor-post-saved-state';

Expand Down
26 changes: 23 additions & 3 deletions editor/components/unsaved-changes-warning/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { connect } from 'react-redux';
import { some } from 'lodash';
import jQuery from 'jquery';

/**
* WordPress dependencies
Expand All @@ -14,32 +15,51 @@ import { Component } from '@wordpress/element';
* Internal dependencies
*/
import { isEditedPostDirty, getMetaBoxes } from '../../store/selectors';
import { getLocationHtml } from '../../edit-post/meta-boxes';
import { getMetaBoxContainer } from '../../edit-post/meta-boxes';

class UnsavedChangesWarning extends Component {
/**
* @inheritdoc
*/
constructor() {
super( ...arguments );
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind( this );
}

/**
* @inheritdoc
*/
componentDidMount() {
window.addEventListener( 'beforeunload', this.warnIfUnsavedChanges );
}

/**
* @inheritdoc
*/
componentWillUnmount() {
window.removeEventListener( 'beforeunload', this.warnIfUnsavedChanges );
}

/**
* Warns the user if there are unsaved changes before leaving the editor.
*
* @param {Event} event Event Object.
* @returns {string?} Warning message.
*/
warnIfUnsavedChanges( event ) {
const areMetaBoxesDirty = some( this.props.metaBoxes, ( metaBoxe, location ) => {
return metaBoxe.isActive && getLocationHtml( location ) !== metaBoxe.html;
const areMetaBoxesDirty = some( this.props.metaBoxes, ( metaBox, location ) => {
return metaBox.isActive &&
jQuery( getMetaBoxContainer( location ) ).serialize() !== metaBox.html;
} );
if ( this.props.isDirty || areMetaBoxesDirty ) {
event.returnValue = __( 'You have unsaved changes. If you proceed, they will be lost.' );
return event.returnValue;
}
}

/**
* @inheritdoc
*/
render() {
return null;
}
Expand Down
16 changes: 8 additions & 8 deletions editor/edit-post/meta-boxes.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/**
* Function returning the current Meta Boxes HTML in the editor
* Function returning the current Meta Boxes DOM Node in the editor
* whether the meta box area is opened or not.
* This is not so clear, but I believe it's the only way to have this data synchronously
* If the MetaBox Area is visible returns it, and returns the original container instead.
*
* @param {String} location Meta Box location
* @returns {String} HTML content
* @param {string} location Meta Box location.
* @returns {string} HTML content.
*/
export const getLocationHtml = ( location ) => {
const area = document.querySelector( `.editor-meta-boxes-area.is-${ location } .editor-meta-boxes-area__container` );
export const getMetaBoxContainer = ( location ) => {
const area = document.querySelector( `.editor-meta-boxes-area.is-${ location } .metabox-location-${ location }` );
if ( area ) {
return area.innerHTML;
return area;
}

return document.querySelector( '.metabox-location-' + location ).innerHTML;
return document.querySelector( '#metaboxes .metabox-location-' + location );
};
35 changes: 16 additions & 19 deletions editor/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,11 +438,11 @@ export function removeNotice( id ) {
* not render the meta box area.
*
* Example: metaBoxes = { side: true, normal: false }
* This indicates that the sidebar has a meta box but the normal area does not.
* this indicates that the sidebar has a meta box but the normal area does not.
*
* @param {Object} metaBoxes Whether meta box locations are active.
* @param {Object} metaBoxes Whether meta box locations are active.
*
* @returns {Object} Action object.
* @returns {Object} Action object.
*/
export function initializeMetaBoxState( metaBoxes ) {
return {
Expand All @@ -452,41 +452,38 @@ export function initializeMetaBoxState( metaBoxes ) {
}

/**
* Returns an action object used to signify that a meta box finished reloading.
*
* @param {String} location Location of meta box: 'normal', 'side' or 'advanced'.
* Returns an action object used to request meta box update.
*
* @returns {Object} Action object.
*/
export function handleMetaBoxReload( location ) {
export function requestMetaBoxUpdates() {
return {
type: 'HANDLE_META_BOX_RELOAD',
location,
type: 'REQUEST_META_BOX_UPDATES',
};
}

/**
* Returns an action object used to signify that a meta box finished loading.
*
* @param {String} location Location of meta box: 'normal', 'side' or 'advanced'.
* Returns an action object used signal a successfull meta nox update.
*
* @returns {Object} Action object.
*/
export function metaBoxLoaded( location ) {
export function metaBoxUpdatesSuccess() {
return {
type: 'META_BOX_LOADED',
location,
type: 'META_BOX_UPDATES_SUCCESS',
};
}

/**
* Returns an action object used to request meta box update.
* Returns an action object used set the saved meta boxes data.
* This is used to check if the meta boxes have been touched when leaving the editor.
*
* @returns {Object} Action object.
* @param {Object} dataPerLocation Meta Boxes Data per location.
* @returns {Object} Action object.
*/
export function requestMetaBoxUpdates() {
export function metaBoxSetSavedData( dataPerLocation ) {
return {
type: 'REQUEST_META_BOX_UPDATES',
type: 'META_BOX_SET_SAVED_DATA',
dataPerLocation,
};
}

Expand Down
40 changes: 39 additions & 1 deletion editor/store/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
import { get, includes, map, castArray, uniqueId } from 'lodash';
import { get, includes, map, castArray, uniqueId, reduce, values } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -36,9 +36,11 @@ import {
savePost,
editPost,
requestMetaBoxUpdates,
metaBoxUpdatesSuccess,
updateReusableBlock,
saveReusableBlock,
insertBlock,
metaBoxSetSavedData,
} from './actions';
import {
getCurrentPost,
Expand All @@ -52,8 +54,10 @@ import {
getBlock,
getBlocks,
getReusableBlock,
getMetaBoxes,
POST_UPDATE_TRANSACTION_ID,
} from './selectors';
import { getMetaBoxContainer } from '../edit-post/meta-boxes';

/**
* Module Constants
Expand Down Expand Up @@ -451,4 +455,38 @@ export default {
const message = spokenMessage || content;
speak( message, 'assertive' );
},
INITIALIZE_META_BOX_STATE( action, store ) {
const dataPerLocation = reduce( action.metaBoxes, ( memo, isActive, location ) => {
if ( isActive ) {
memo[ location ] = jQuery( getMetaBoxContainer( location ) ).serialize();
}
return memo;
}, {} );
store.dispatch( metaBoxSetSavedData( dataPerLocation ) );
},
REQUEST_META_BOX_UPDATES( action, store ) {
const dataPerLocation = reduce( getMetaBoxes( store.getState() ), ( memo, metabox, location ) => {
if ( metabox.isActive ) {
memo[ location ] = jQuery( getMetaBoxContainer( location ) ).serialize();
}
return memo;
}, {} );
store.dispatch( metaBoxSetSavedData( dataPerLocation ) );

// To save the metaboxes, we serialize each one of the location forms and combine them
// We also add the "common" hidden fields from the base .metabox-base-form
const formData = values( dataPerLocation ).concat(
jQuery( '.metabox-base-form' ).serialize()
).join( '&' );
const fetchOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData,
credentials: 'include',
};

// Save the metaboxes
window.fetch( window._wpMetaBoxUrl, fetchOptions )
.then( () => store.dispatch( metaBoxUpdatesSuccess() ) );
},
};
Loading

0 comments on commit 13f869e

Please sign in to comment.