Skip to content

Commit

Permalink
Merge pull request #6548 from Automattic/update/editor/cpt-term-token…
Browse files Browse the repository at this point in the history
…-field-redux

Editor: CPT: Manage non-hierarchical term edits using Redux state
  • Loading branch information
nylen authored Jul 17, 2016
2 parents ec09e3e + 0556ad0 commit a6d1224
Show file tree
Hide file tree
Showing 9 changed files with 492 additions and 51 deletions.
4 changes: 2 additions & 2 deletions client/post-editor/editor-drawer/taxonomies.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function EditorDrawerTaxonomies( { siteId, postType, postTerms, taxonomies } ) {
>
{ hierarchical
? <TermSelector postTerms={ postTerms } taxonomyName={ name } />
: <TermTokenField postTerms={ postTerms } taxonomyName={ name } />
: <TermTokenField taxonomyName={ name } />
}
</Accordion>
);
Expand All @@ -54,7 +54,7 @@ EditorDrawerTaxonomies.propTypes = {
siteId: PropTypes.number,
postType: PropTypes.string,
postTerms: PropTypes.object,
taxonomies: PropTypes.array
taxonomies: PropTypes.array,
};

export default connect( ( state ) => {
Expand Down
40 changes: 26 additions & 14 deletions client/post-editor/post-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { setEditorLastDraft, resetEditorLastDraft } from 'state/ui/editor/last-d
import { isEditorDraftsVisible, getEditorPostId, getEditorPath } from 'state/ui/editor/selectors';
import { toggleEditorDraftsVisible, setEditorPostId } from 'state/ui/editor/actions';
import { receivePost, editPost, resetPostEdits } from 'state/posts/actions';
import { getPostEdits, isEditedPostDirty } from 'state/posts/selectors';
import EditorSidebarHeader from 'post-editor/editor-sidebar/header';
import EditorDocumentHead from 'post-editor/editor-document-head';
import EditorPostTypeUnsupported from 'post-editor/editor-post-type-unsupported';
Expand Down Expand Up @@ -167,6 +168,7 @@ const messages = {

const PostEditor = React.createClass( {
propTypes: {
siteId: React.PropTypes.number,
preferences: React.PropTypes.object,
setEditorModePreference: React.PropTypes.func,
editorModePreference: React.PropTypes.string,
Expand Down Expand Up @@ -228,6 +230,12 @@ const PostEditor = React.createClass( {
if ( ! isNew && savedPost && savedPost !== this.state.savedPost ) {
nextProps.receivePost( savedPost );
}

if ( nextState.isDirty || nextProps.dirty ) {
this.markChanged();
} else {
this.markSaved();
}
},

componentDidMount: function() {
Expand Down Expand Up @@ -407,7 +415,7 @@ const PostEditor = React.createClass( {
savedPost={ this.state.savedPost }
post={ this.state.post }
isNew={ this.state.isNew }
isDirty={ this.state.isDirty }
isDirty={ this.state.isDirty || this.props.dirty }
isSaveBlocked={ this.isSaveBlocked() }
hasContent={ this.state.hasContent }
isSaving={ this.state.isSaving }
Expand Down Expand Up @@ -528,11 +536,6 @@ const PostEditor = React.createClass( {
}
} );
}
if ( PostEditStore.isDirty() ) {
this.markChanged();
} else {
this.markSaved();
}
},

isSaveBlocked() {
Expand Down Expand Up @@ -569,7 +572,11 @@ const PostEditor = React.createClass( {

this.saveRawContent();
// TODO: REDUX - remove flux actions when whole post-editor is reduxified
actions.edit( { content: this.refs.editor.getContent() } );
const edits = {
...this.props.edits,
content: this.refs.editor.getContent()
};
actions.edit( edits );

// Make sure that after TinyMCE processing that the post is still dirty
if ( ! PostEditStore.isDirty() || ! PostEditStore.hasContent() || ! this.state.post ) {
Expand Down Expand Up @@ -650,8 +657,7 @@ const PostEditor = React.createClass( {
},

onSave: function( status, callback ) {
var edits = {};

const edits = { ...this.props.edits };
if ( status ) {
edits.status = status;
}
Expand Down Expand Up @@ -719,7 +725,7 @@ const PostEditor = React.createClass( {
},

iframePreview: function() {
if ( this.state.isDirty ) {
if ( this.state.isDirty || this.props.dirty ) {
this.autosave();
// to avoid a weird UX we clear the iframe when (auto)saving
// so we need to delay opening it a bit to avoid flickering
Expand Down Expand Up @@ -769,7 +775,10 @@ const PostEditor = React.createClass( {
},

onPublish: function() {
var edits = { status: 'publish' };
const edits = {
...this.props.edits,
status: 'publish'
};

// determine if this is a private publish
if ( utils.isPrivate( this.state.post ) ) {
Expand Down Expand Up @@ -913,14 +922,17 @@ const PostEditor = React.createClass( {

export default connect(
( state ) => {
const postId = getEditorPostId( state );
const siteId = getSelectedSiteId( state );
const postId = getEditorPostId( state );

return {
siteId,
postId,
showDrafts: isEditorDraftsVisible( state ),
editorModePreference: getPreference( state, 'editor-mode' ),
editPath: getEditorPath( state, siteId, postId ),
siteId,
postId
edits: getPostEdits( state, siteId, postId ),
dirty: isEditedPostDirty( state, siteId, postId ),
};
},
( dispatch ) => {
Expand Down
50 changes: 29 additions & 21 deletions client/post-editor/term-token-field/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
* External dependencies
*/
import React from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { connect } from 'react-redux';
import { map } from 'lodash';
import _debug from 'debug';

/**
Expand All @@ -14,10 +14,10 @@ import { getTerms } from 'state/terms/selectors';
import { getEditorPostId } from 'state/ui/editor/selectors';
import { getEditedPostValue } from 'state/posts/selectors';
import { getPostTypeTaxonomy } from 'state/post-types/taxonomies/selectors';
import { editPost } from 'state/posts/actions';
import TokenField from 'components/token-field';
import { decodeEntities } from 'lib/formatting';
import TermsConstants from 'lib/terms/constants';
import PostActions from 'lib/posts/actions';
import { recordStat, recordEvent } from 'lib/posts/stats';
import QueryTerms from 'components/data/query-terms';

Expand All @@ -42,11 +42,12 @@ class TermTokenField extends React.Component {
recordStat( termStat );
recordEvent( 'Changed Terms', termEventLabel );

const { postTerms, taxonomyName } = this.props;
const terms = cloneDeep( postTerms ) || {};
terms[ taxonomyName ] = selectedTerms;
// TODO: REDUX - remove flux actions when whole post-editor is reduxified
PostActions.edit( { terms } );
const { siteId, postId, postTerms, taxonomyName } = this.props;
this.props.editPost( {
terms: {
[ taxonomyName ]: selectedTerms
}
}, siteId, postId );
}

getPostTerms() {
Expand All @@ -64,7 +65,7 @@ class TermTokenField extends React.Component {
}

render() {
const termNames = ( this.props.terms || [] ).map( term => term.name );
const termNames = map( this.props.terms, 'name' );

return (
<label className="editor-drawer__label">
Expand All @@ -90,22 +91,29 @@ class TermTokenField extends React.Component {

TermTokenField.propTypes = {
siteId: React.PropTypes.number,
postId: React.PropTypes.number,
postTerms: React.PropTypes.object,
taxonomyName: React.PropTypes.string,
taxonomyLabel: React.PropTypes.string,
terms: React.PropTypes.arrayOf( React.PropTypes.object ),
editPost: React.PropTypes.func,
};

export default connect( ( state, props ) => {
const siteId = getSelectedSiteId( state );

const postId = getEditorPostId( state );
const postType = getEditedPostValue( state, siteId, postId, 'type' );
const taxonomy = getPostTypeTaxonomy( state, siteId, postType, props.taxonomyName );

return {
siteId,
taxonomyLabel: taxonomy && taxonomy.label,
terms: getTerms( state, siteId, props.taxonomyName ),
};
} )( TermTokenField );
export default connect(
( state, props ) => {
const siteId = getSelectedSiteId( state );
const postId = getEditorPostId( state );

const postType = getEditedPostValue( state, siteId, postId, 'type' );
const taxonomy = getPostTypeTaxonomy( state, siteId, postType, props.taxonomyName );

return {
siteId,
postId,
taxonomyLabel: taxonomy && taxonomy.label,
terms: getTerms( state, siteId, props.taxonomyName ),
postTerms: getEditedPostValue( state, siteId, postId, 'terms' ),
};
},
{ editPost }
)( TermTokenField );
12 changes: 12 additions & 0 deletions client/state/posts/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ export const DEFAULT_POST_QUERY = {
sticky: 'include',
search: ''
};

export const DEFAULT_NEW_POST_VALUES = {
title: '',
content: '',
publicize: true,
status: 'draft',
sticky: false,
password: '',
type: 'post',
parent: 0,
format: 'default'
};
7 changes: 5 additions & 2 deletions client/state/posts/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ import {
DESERIALIZE
} from 'state/action-types';
import counts from './counts/reducer';
import { getSerializedPostsQuery } from './utils';
import {
getSerializedPostsQuery,
mergeIgnoringArrays,
} from './utils';
import { itemsSchema } from './schema';
import { isValidStateWithSchema, createReducer } from 'state/utils';

Expand Down Expand Up @@ -224,7 +227,7 @@ export function edits( state = {}, action ) {
}, state );

case POST_EDIT:
return merge( {}, state, {
return mergeIgnoringArrays( {}, state, {
[ action.siteId ]: {
[ action.postId || '' ]: action.post
}
Expand Down
57 changes: 49 additions & 8 deletions client/state/posts/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import get from 'lodash/get';
import createSelector from 'lib/create-selector';
import filter from 'lodash/filter';
import find from 'lodash/find';
import merge from 'lodash/merge';
import flow from 'lodash/flow';
import cloneDeep from 'lodash/cloneDeep';
import includes from 'lodash/includes';
Expand All @@ -20,9 +19,10 @@ import {
getNormalizedPostsQuery,
getSerializedPostsQuery,
getDeserializedPostsQueryDetails,
getSerializedPostsQueryWithoutPage
getSerializedPostsQueryWithoutPage,
mergeIgnoringArrays,
} from './utils';
import { DEFAULT_POST_QUERY } from './constants';
import { DEFAULT_POST_QUERY, DEFAULT_NEW_POST_VALUES } from './constants';
import firstPassCanonicalImage from 'lib/post-normalizer/rule-first-pass-canonical-image';
import decodeEntities from 'lib/post-normalizer/rule-decode-entities';
import stripHtml from 'lib/post-normalizer/rule-strip-html';
Expand Down Expand Up @@ -289,16 +289,29 @@ export function isRequestingSitePost( state, siteId, postId ) {
*/
export function getEditedPost( state, siteId, postId ) {
const post = getSitePost( state, siteId, postId );
if ( ! state.posts.edits[ siteId ] ) {
const edits = getPostEdits( state, siteId, postId );
if ( ! edits ) {
return post;
}

const edits = state.posts.edits[ siteId ][ postId || '' ];
if ( ! postId ) {
return edits || null;
if ( ! post ) {
return edits;
}

return merge( {}, post, edits );
return mergeIgnoringArrays( {}, post, edits );
}

/**
* Returns an object of edited post attributes for the site ID post ID pairing.
*
* @param {Object} state Global state tree
* @param {Number} siteId Site ID
* @param {Number} postId Post ID
* @return {Object} Post revisions
*/
export function getPostEdits( state, siteId, postId ) {
const { edits } = state.posts;
return get( edits, [ siteId, postId || '' ], null );
}

/**
Expand All @@ -313,3 +326,31 @@ export function getEditedPost( state, siteId, postId ) {
export function getEditedPostValue( state, siteId, postId, field ) {
return get( getEditedPost( state, siteId, postId ), field );
}

/**
* Returns true if there are "dirty" edited fields to be saved for the post
* corresponding with the site ID post ID pair, or false otherwise.
*
* @param {Object} state Global state tree
* @param {Number} siteId Site ID
* @param {Number} postId Post ID
* @return {Boolean} Whether dirty fields exist
*/
export const isEditedPostDirty = createSelector(
( state, siteId, postId ) => {
const post = getSitePost( state, siteId, postId );
const edits = getPostEdits( state, siteId, postId );

return some( edits, ( value, key ) => {
if ( post ) {
return post[ key ] !== value;
}

return (
! DEFAULT_NEW_POST_VALUES.hasOwnProperty( key ) ||
value !== DEFAULT_NEW_POST_VALUES[ key ]
);
} );
},
( state ) => [ state.posts.items, state.posts.edits ]
);
Loading

0 comments on commit a6d1224

Please sign in to comment.