From 632169f10320f442c5e496e40e82d5d814357d04 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Nov 2018 16:42:49 -0500 Subject: [PATCH 1/9] Editor: Support transient states for set block attributes --- .../src/components/autosave-monitor/index.js | 18 +++++- .../editor/src/components/block-list/block.js | 8 +-- packages/editor/src/store/actions.js | 10 +++- packages/editor/src/store/effects/posts.js | 12 ++++ packages/editor/src/store/reducer.js | 55 +++++++++++++++++++ packages/editor/src/store/selectors.js | 4 ++ 6 files changed, 97 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/autosave-monitor/index.js b/packages/editor/src/components/autosave-monitor/index.js index f3a7d81bd6481..3ab971ef43184 100644 --- a/packages/editor/src/components/autosave-monitor/index.js +++ b/packages/editor/src/components/autosave-monitor/index.js @@ -7,14 +7,24 @@ import { withSelect, withDispatch } from '@wordpress/data'; export class AutosaveMonitor extends Component { componentDidUpdate( prevProps ) { - const { isDirty, editsReference, isAutosaveable } = this.props; + const { + isDirty, + editsReference, + isAutosaveable, + hasPendingBlockOperations, + } = this.props; if ( prevProps.isDirty !== isDirty || prevProps.isAutosaveable !== isAutosaveable || - prevProps.editsReference !== editsReference + prevProps.editsReference !== editsReference || + prevProps.hasPendingBlockOperations !== hasPendingBlockOperations ) { - this.toggleTimer( isDirty && isAutosaveable ); + this.toggleTimer( + isDirty && + isAutosaveable && + ! hasPendingBlockOperations + ); } } @@ -45,6 +55,7 @@ export default compose( [ isEditedPostAutosaveable, getEditorSettings, getReferenceByDistinctEdits, + hasPendingBlockOperations, } = select( 'core/editor' ); const { autosaveInterval } = getEditorSettings(); @@ -53,6 +64,7 @@ export default compose( [ isDirty: isEditedPostDirty(), isAutosaveable: isEditedPostAutosaveable(), editsReference: getReferenceByDistinctEdits(), + hasPendingBlockOperations: hasPendingBlockOperations(), autosaveInterval, }; } ), diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index e57a221531dcc..c22b3497fa312 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -165,10 +165,10 @@ export class BlockListBlock extends Component { } } - setAttributes( attributes ) { + setAttributes( attributes, options ) { const { block, onChange } = this.props; const type = getBlockType( block.name ); - onChange( block.clientId, attributes ); + onChange( block.clientId, attributes, options ); const metaAttributes = reduce( attributes, ( result, value, key ) => { if ( get( type, [ 'attributes', key, 'source' ] ) === 'meta' ) { @@ -703,8 +703,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps ) => { } = dispatch( 'core/editor' ); return { - onChange( clientId, attributes ) { - updateBlockAttributes( clientId, attributes ); + onChange( clientId, attributes, options ) { + updateBlockAttributes( clientId, attributes, options ); }, onSelect( clientId = ownProps.clientId, initialPosition ) { selectBlock( clientId, initialPosition ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 6302ce8894756..a50f57b6234fc 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -122,16 +122,20 @@ export function receiveBlocks( blocks ) { * Returns an action object used in signalling that the block attributes with * the specified client ID has been updated. * - * @param {string} clientId Block client ID. - * @param {Object} attributes Block attributes to be merged. + * @param {string} clientId Block client ID. + * @param {Object} attributes Block attributes to be merged. + * @param {?Object} options Optional options. + * @param {?boolean} options.transient Whether attribute should be considered + * to be in a transient state. * * @return {Object} Action object. */ -export function updateBlockAttributes( clientId, attributes ) { +export function updateBlockAttributes( clientId, attributes, options ) { return { type: 'UPDATE_BLOCK_ATTRIBUTES', clientId, attributes, + ...options, }; } diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js index ed5f596b167d4..1f1bad707f64d 100644 --- a/packages/editor/src/store/effects/posts.js +++ b/packages/editor/src/store/effects/posts.js @@ -30,6 +30,7 @@ import { getCurrentPostType, isEditedPostSaveable, isEditedPostNew, + hasPendingBlockOperations, POST_UPDATE_TRANSACTION_ID, } from '../selectors'; import { resolveSelector } from './utils'; @@ -52,6 +53,17 @@ export const requestPostUpdate = async ( action, store ) => { const post = getCurrentPost( state ); const isAutosave = !! action.options.isAutosave; + if ( + hasPendingBlockOperations( state ) && + // Disable reason: A blocking prompt is justified to protect against + // potential content corruption. + // + // eslint-disable-next-line no-alert + ! window.confirm( __( 'A block operation is pending. Are you sure you want to save?' ) ) + ) { + return; + } + // Prevent save if not saveable. // We don't check for dirtiness here as this can be overriden in the UI. if ( ! isEditedPostSaveable( state ) ) { diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index d241bb008f45a..c6512b37f4058 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -15,6 +15,8 @@ import { isEqual, overSome, get, + intersection, + union, } from 'lodash'; /** @@ -199,6 +201,10 @@ export function isUpdatingSamePostProperty( action, previousAction ) { ); } +export function isUpdatingTransient( action ) { + return action.isUpdatingTransient; +} + /** * Returns true if, given the currently dispatching action and the previously * dispatched action, the two actions are modifying the same property such that @@ -217,6 +223,7 @@ export function shouldOverwriteState( action, previousAction ) { return overSome( [ isUpdatingSameBlockAttribute, isUpdatingSamePostProperty, + isUpdatingTransient, ] )( action, previousAction ); } @@ -273,6 +280,18 @@ const withBlockReset = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; +export const withTransientsHistoryOverride = ( reducer ) => ( state, action ) => { + if ( action.type === 'UPDATE_BLOCK_ATTRIBUTES' ) { + const { clientId, attributes } = action; + const { transients } = state.blocks; + if ( intersection( transients[ clientId ], keys( attributes ) ).length ) { + action = { ...action, isUpdatingTransient: true }; + } + } + + return reducer( state, action ); +}; + /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -290,6 +309,8 @@ const withBlockReset = ( reducer ) => ( state, action ) => { export const editor = flow( [ combineReducers, + withTransientsHistoryOverride, + withInnerBlocksRemoveCascade, // Track undo history, starting at editor initialization. @@ -588,6 +609,40 @@ export const editor = flow( [ return state; }, + + transients( state = {}, action ) { + if ( action.type === 'UPDATE_BLOCK_ATTRIBUTES' ) { + if ( action.transient ) { + return { + ...state, + [ action.clientId ]: union( + state[ action.clientId ], + keys( action.attributes ), + ), + }; + } + + if ( ! state[ action.clientId ] ) { + return state; + } + + const nextTransients = without( + state[ action.clientId ], + ...keys( action.attributes ), + ); + + if ( ! nextTransients.length ) { + return omit( state, action.clientId ); + } + + return { + ...state, + [ action.clientId ]: nextTransients, + }; + } + + return state; + }, } ), } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index ddbc7770a3c8e..4cd960d5b4587 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -2306,3 +2306,7 @@ export function isPublishSidebarEnabled( state ) { } return PREFERENCES_DEFAULTS.isPublishSidebarEnabled; } + +export function hasPendingBlockOperations( state ) { + return Object.keys( state.editor.present.blocks.transients ).length > 0; +} From c6f0a7a3a93986172f04b331988a295ae7947b99 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 19 Nov 2018 16:43:12 -0500 Subject: [PATCH 2/9] Blocks: Set URL as transient attribute while uploading --- packages/block-library/src/image/edit.js | 33 +++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index cb059b5bfcdc6..e27297d0ce261 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -121,20 +121,13 @@ class ImageEdit extends Component { } componentDidMount() { - const { attributes, setAttributes } = this.props; + const { attributes } = this.props; const { id, url = '' } = attributes; if ( isTemporaryImage( id, url ) ) { const file = getBlobByURL( url ); - if ( file ) { - mediaUpload( { - filesList: [ file ], - onFileChange: ( [ image ] ) => { - setAttributes( pickRelevantMediaFiles( image ) ); - }, - allowedTypes: ALLOWED_MEDIA_TYPES, - } ); + this.uploadFile( file ); } } } @@ -154,6 +147,28 @@ class ImageEdit extends Component { } } + uploadFile( file ) { + this.props.setAttributes( + { url: this.props.attributes.url }, + { transient: true } + ); + + mediaUpload( { + filesList: [ file ], + onFileChange: ( [ image ] ) => { + const { setAttributes } = this.props; + const { id, url, ...nextAttributes } = pickRelevantMediaFiles( image ); + if ( isTemporaryImage( id, url ) ) { + setAttributes( { ...nextAttributes, id } ); + setAttributes( { url }, { transient: true } ); + } else { + setAttributes( { ...nextAttributes, id, url } ); + } + }, + allowedTypes: ALLOWED_MEDIA_TYPES, + } ); + } + onUploadError( message ) { const { noticeOperations } = this.props; noticeOperations.createErrorNotice( message ); From c38394a29e8aa1b9ca56e1dce3a1e926b5653dea Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 27 Aug 2018 16:06:16 -0400 Subject: [PATCH 3/9] Editor: withHistory: Add isIgnored callback option --- .../editor/src/utils/with-history/index.js | 9 +++++++-- .../src/utils/with-history/test/index.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/utils/with-history/index.js b/packages/editor/src/utils/with-history/index.js index 665851e632bd3..521038a147964 100644 --- a/packages/editor/src/utils/with-history/index.js +++ b/packages/editor/src/utils/with-history/index.js @@ -15,6 +15,7 @@ const DEFAULT_OPTIONS = { resetTypes: [], ignoreTypes: [], shouldOverwriteState: () => false, + isIgnored: () => false, }; /** @@ -26,6 +27,9 @@ const DEFAULT_OPTIONS = { * clear past. * @param {?Array} options.ignoreTypes Action types upon which to * avoid history tracking. + * @param {?Function} options.isIgnored Function given action, to + * return true if intended to + * avoid history tracking. * @param {?Function} options.shouldOverwriteState Function receiving last and * current actions, returning * boolean indicating whether @@ -34,13 +38,14 @@ const DEFAULT_OPTIONS = { * * @return {Function} Higher-order reducer. */ -const withHistory = ( options = {} ) => ( reducer ) => { +const withHistory = ( options ) => ( reducer ) => { options = { ...DEFAULT_OPTIONS, ...options }; - // `ignoreTypes` is simply a convenience for `shouldOverwriteState` + // `ignoreTypes`, `isIgnored` are conveniences for `shouldOverwriteState` options.shouldOverwriteState = overSome( [ options.shouldOverwriteState, ( action ) => includes( options.ignoreTypes, action.type ), + ( action ) => options.isIgnored( action ), ] ); const initialState = { diff --git a/packages/editor/src/utils/with-history/test/index.js b/packages/editor/src/utils/with-history/test/index.js index e383988864f77..898a320866b8b 100644 --- a/packages/editor/src/utils/with-history/test/index.js +++ b/packages/editor/src/utils/with-history/test/index.js @@ -131,6 +131,25 @@ describe( 'withHistory', () => { } ); } ); + it( 'should ignore history by options.isIgnored', () => { + const reducer = withHistory( { + isIgnored: ( action ) => action.type === 'INCREMENT', + } )( counter ); + + let state; + state = reducer( undefined, {} ); + state = reducer( state, { type: 'INCREMENT' } ); + state = reducer( state, { type: 'INCREMENT' } ); + + expect( state ).toEqual( { + past: [], + present: 2, + future: [], + lastAction: { type: 'INCREMENT' }, + shouldCreateUndoLevel: false, + } ); + } ); + it( 'should return same reference if state has not changed', () => { const reducer = withHistory()( counter ); const original = reducer( undefined, {} ); From a97bc35c7fdcbdafa21fc2162e71fe2ebeb77d9c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Nov 2018 12:32:52 -0500 Subject: [PATCH 4/9] Editor: Add option to withHistory to pop level --- packages/editor/src/store/actions.js | 4 ++++ packages/editor/src/utils/with-history/index.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index a50f57b6234fc..6b1e94b94d1aa 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -480,6 +480,10 @@ export function createUndoLevel() { return { type: 'CREATE_UNDO_LEVEL' }; } +export function popUndoLevel() { + return { type: 'POP_UNDO_LEVEL' }; +} + /** * Returns an action object used in signalling that the blocks corresponding to * the set of specified client IDs are to be removed. diff --git a/packages/editor/src/utils/with-history/index.js b/packages/editor/src/utils/with-history/index.js index 521038a147964..591bcaa9147bb 100644 --- a/packages/editor/src/utils/with-history/index.js +++ b/packages/editor/src/utils/with-history/index.js @@ -46,6 +46,10 @@ const withHistory = ( options ) => ( reducer ) => { options.shouldOverwriteState, ( action ) => includes( options.ignoreTypes, action.type ), ( action ) => options.isIgnored( action ), + ( action, previousAction ) => ( + action.type === 'POP_UNDO_LEVEL' || + ( previousAction && previousAction.type === 'POP_UNDO_LEVEL' ) + ), ] ); const initialState = { From 055a0856f7f46b4f83c985820af4a48553c2ce44 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Nov 2018 12:33:58 -0500 Subject: [PATCH 5/9] Blocks: Pop undo level when image mounts with transient URL --- packages/block-library/src/image/edit.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index e27297d0ce261..ce5b93817cc61 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -32,7 +32,7 @@ import { withNotices, ToggleControl, } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; +import { withSelect, withDispatch } from '@wordpress/data'; import { RichText, BlockControls, @@ -125,6 +125,8 @@ class ImageEdit extends Component { const { id, url = '' } = attributes; if ( isTemporaryImage( id, url ) ) { + this.props.popUndoLevel(); + const file = getBlobByURL( url ); if ( file ) { this.uploadFile( file ); @@ -727,6 +729,13 @@ export default compose( [ imageSizes, }; } ), + withDispatch( ( dispatch ) => { + const { popUndoLevel } = dispatch( 'core/editor' ); + + return { + popUndoLevel, + }; + } ), withViewportMatch( { isLargeViewport: 'medium' } ), withNotices, ] )( ImageEdit ); From a357ed8b61f0e4eefc14b60c24eba278fdcae7a2 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Nov 2018 12:34:36 -0500 Subject: [PATCH 6/9] Blocks: Remove internal image isEditing state Incompatible with undo history --- packages/block-library/src/image/edit.js | 28 +++--------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index ce5b93817cc61..89f9942a40aaa 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -92,7 +92,7 @@ const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url ); const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url ); class ImageEdit extends Component { - constructor( { attributes } ) { + constructor() { super( ...arguments ); this.updateAlt = this.updateAlt.bind( this ); this.updateAlignment = this.updateAlignment.bind( this ); @@ -110,13 +110,11 @@ class ImageEdit extends Component { this.onSetLinkDestination = this.onSetLinkDestination.bind( this ); this.onSetNewTab = this.onSetNewTab.bind( this ); this.getFilename = this.getFilename.bind( this ); - this.toggleIsEditing = this.toggleIsEditing.bind( this ); this.onUploadError = this.onUploadError.bind( this ); this.onImageError = this.onImageError.bind( this ); this.state = { captionFocused: false, - isEditing: ! attributes.url, }; } @@ -174,9 +172,6 @@ class ImageEdit extends Component { onUploadError( message ) { const { noticeOperations } = this.props; noticeOperations.createErrorNotice( message ); - this.setState( { - isEditing: true, - } ); } onSelectImage( media ) { @@ -190,10 +185,6 @@ class ImageEdit extends Component { return; } - this.setState( { - isEditing: false, - } ); - this.props.setAttributes( { ...pickRelevantMediaFiles( media ), width: undefined, @@ -229,10 +220,6 @@ class ImageEdit extends Component { id: undefined, } ); } - - this.setState( { - isEditing: false, - } ); } onImageError( url ) { @@ -335,12 +322,6 @@ class ImageEdit extends Component { ]; } - toggleIsEditing() { - this.setState( { - isEditing: ! this.state.isEditing, - } ); - } - getImageSizeOptions() { const { imageSizes, image } = this.props; return compact( map( imageSizes, ( { name, slug } ) => { @@ -356,7 +337,6 @@ class ImageEdit extends Component { } render() { - const { isEditing } = this.state; const { attributes, setAttributes, @@ -393,7 +373,7 @@ class ImageEdit extends Component { setAttributes( { url: undefined } ) } icon="edit" /> @@ -431,8 +411,7 @@ class ImageEdit extends Component { ); - if ( isEditing ) { - const src = isExternal ? url : undefined; + if ( ! url ) { return ( { controls } @@ -445,7 +424,6 @@ class ImageEdit extends Component { onError={ this.onUploadError } accept="image/*" allowedTypes={ ALLOWED_MEDIA_TYPES } - value={ { id, src } } /> ); From da24d41514d8b888b83b3ea2c1ceac1975d41c5f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Nov 2018 12:35:33 -0500 Subject: [PATCH 7/9] Editor: Avoid merging states by transient update Unreliable with intermediary states --- packages/editor/src/store/reducer.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index c6512b37f4058..4b1cccc491bcc 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -201,10 +201,6 @@ export function isUpdatingSamePostProperty( action, previousAction ) { ); } -export function isUpdatingTransient( action ) { - return action.isUpdatingTransient; -} - /** * Returns true if, given the currently dispatching action and the previously * dispatched action, the two actions are modifying the same property such that @@ -223,7 +219,6 @@ export function shouldOverwriteState( action, previousAction ) { return overSome( [ isUpdatingSameBlockAttribute, isUpdatingSamePostProperty, - isUpdatingTransient, ] )( action, previousAction ); } @@ -280,18 +275,6 @@ const withBlockReset = ( reducer ) => ( state, action ) => { return reducer( state, action ); }; -export const withTransientsHistoryOverride = ( reducer ) => ( state, action ) => { - if ( action.type === 'UPDATE_BLOCK_ATTRIBUTES' ) { - const { clientId, attributes } = action; - const { transients } = state.blocks; - if ( intersection( transients[ clientId ], keys( attributes ) ).length ) { - action = { ...action, isUpdatingTransient: true }; - } - } - - return reducer( state, action ); -}; - /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -309,8 +292,6 @@ export const withTransientsHistoryOverride = ( reducer ) => ( state, action ) => export const editor = flow( [ combineReducers, - withTransientsHistoryOverride, - withInnerBlocksRemoveCascade, // Track undo history, starting at editor initialization. From 126044e895259c26544f4c1b4ff4e35a0d25ed7e Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Nov 2018 12:35:54 -0500 Subject: [PATCH 8/9] Editor: Ensure transient attributes update is skipped in history --- packages/editor/src/store/reducer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 4b1cccc491bcc..2ab6f416e62dc 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -298,6 +298,7 @@ export const editor = flow( [ withHistory( { resetTypes: [ 'SETUP_EDITOR_STATE' ], ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], + isIgnored: ( action ) => action.transient, shouldOverwriteState, } ), ] )( { From 3dad2db712f38c4cc37f1690da48adec5c6c831a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Nov 2018 12:36:32 -0500 Subject: [PATCH 9/9] Editor: Track block transients as separate, mutually exclusive with attributes --- packages/editor/src/store/reducer.js | 89 ++++++++++++++------------ packages/editor/src/store/selectors.js | 12 +++- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 2ab6f416e62dc..b8d580414d778 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -15,8 +15,6 @@ import { isEqual, overSome, get, - intersection, - union, } from 'lodash'; /** @@ -378,15 +376,23 @@ export const editor = flow( [ return state; } - // Consider as updates only changed values - const nextAttributes = reduce( action.attributes, ( result, value, key ) => { - if ( value !== result[ key ] ) { - result = getMutateSafeObject( state[ action.clientId ].attributes, result ); - result[ key ] = value; - } - - return result; - }, state[ action.clientId ].attributes ); + let nextAttributes; + if ( action.transient ) { + nextAttributes = omit( + state[ action.clientId ].attributes, + keys( action.attributes ) + ); + } else { + // Consider as updates only changed values + nextAttributes = reduce( action.attributes, ( result, value, key ) => { + if ( value !== result[ key ] ) { + result = getMutateSafeObject( state[ action.clientId ].attributes, result ); + result[ key ] = value; + } + + return result; + }, state[ action.clientId ].attributes ); + } // Skip update if nothing has been changed. The reference will // match the original block if `reduce` had no changed values. @@ -591,42 +597,42 @@ export const editor = flow( [ return state; }, + } ), +} ); - transients( state = {}, action ) { - if ( action.type === 'UPDATE_BLOCK_ATTRIBUTES' ) { - if ( action.transient ) { - return { - ...state, - [ action.clientId ]: union( - state[ action.clientId ], - keys( action.attributes ), - ), - }; - } +export function blockTransients( state = {}, action ) { + if ( action.type === 'UPDATE_BLOCK_ATTRIBUTES' ) { + if ( action.transient ) { + return { + ...state, + [ action.clientId ]: { + ...state[ action.clientId ], + ...action.attributes, + }, + }; + } - if ( ! state[ action.clientId ] ) { - return state; - } + if ( ! state[ action.clientId ] ) { + return state; + } - const nextTransients = without( - state[ action.clientId ], - ...keys( action.attributes ), - ); + const nextTransients = omit( + state[ action.clientId ], + ...keys( action.attributes ), + ); - if ( ! nextTransients.length ) { - return omit( state, action.clientId ); - } + if ( ! Object.keys( nextTransients ).length ) { + return omit( state, action.clientId ); + } - return { - ...state, - [ action.clientId ]: nextTransients, - }; - } + return { + ...state, + [ action.clientId ]: nextTransients, + }; + } - return state; - }, - } ), -} ); + return state; +} /** * Reducer returning the initial edits state. With matching shape to that of @@ -1273,6 +1279,7 @@ export function previewLink( state = null, action ) { export default optimist( combineReducers( { editor, + blockTransients, initialEdits, currentPost, isTyping, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 4cd960d5b4587..3bcaeb60b1748 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -612,6 +612,10 @@ export function isBlockValid( state, clientId ) { return !! block && block.isValid; } +export function getBlockTransients( state, clientId ) { + return state.blockTransients[ clientId ]; +} + /** * Returns a block given its client ID. This is a parsed copy of the block, * containing its `blockName`, `clientId`, and current `attributes` state. This @@ -651,6 +655,11 @@ export const getBlock = createSelector( }, attributes ); } + const transients = getBlockTransients( state, clientId ); + if ( transients ) { + attributes = { ...attributes, ...transients }; + } + return { ...block, attributes, @@ -659,6 +668,7 @@ export const getBlock = createSelector( }, ( state, clientId ) => [ state.editor.present.blocks.byClientId[ clientId ], + state.blockTransients[ clientId ], getBlockDependantsCacheBust( state, clientId ), state.editor.present.edits.meta, state.initialEdits.meta, @@ -2308,5 +2318,5 @@ export function isPublishSidebarEnabled( state ) { } export function hasPendingBlockOperations( state ) { - return Object.keys( state.editor.present.blocks.transients ).length > 0; + return Object.keys( state.blockTransients ).length > 0; }