From 7e1d857a20d0ba54c0fef7c934636e484ebf3474 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 6 Feb 2019 22:02:57 -0500 Subject: [PATCH 01/32] add controls for fetch, select, and dispatch --- packages/editor/src/store/controls.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index fc873ad43aa395..270a8d09aa9cda 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -1,8 +1,25 @@ /** * WordPress dependencies */ +import { default as triggerFetch } from '@wordpress/api-fetch'; import { createRegistryControl } from '@wordpress/data'; +export function apiFetch( request ) { + return { + type: 'API_FETCH', + request, + }; +} + +export function select( reducerKey, selectorName, ...args ) { + return { + type: 'SELECT', + reducerKey, + selectorName, + args, + }; +} + /** * Dispatches an action. * @@ -21,7 +38,13 @@ export function dispatch( storeKey, actionName, ...args ) { }; } -const controls = { +export default { + API_FETCH( { request } ) { + return triggerFetch( request ); + }, + SELECT: createRegistryControl( (registry ) => ( { storeKey, selectorName, args } ) { + return registry.select( storeKey )[ selectorName ]( ...args ); + } ), DISPATCH: createRegistryControl( ( registry ) => ( { storeKey, actionName, args } ) => { return registry.dispatch( storeKey )[ actionName ]( ...args ); } ), From 77a812c824326ecf4b05d49f9172f354bdce1b78 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 6 Feb 2019 22:03:37 -0500 Subject: [PATCH 02/32] move _all_ constants into the constants file for better organization --- packages/editor/src/store/constants.js | 19 +++++++++++++++++++ packages/editor/src/store/index.js | 6 +----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index f07ca417f9d6eb..6872def82c08a8 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -7,3 +7,22 @@ export const EDIT_MERGE_PROPERTIES = new Set( [ 'meta', ] ); + +/** + * Constant for the store module (or reducer) key. + * @type {string} + */ +export const MODULE_KEY = 'core/editor'; + +export const POST_UPDATE_TRANSACTION_ID = 'post-update'; +export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; +export const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; +export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; +export const INSERTER_UTILITY_HIGH = 3; +export const INSERTER_UTILITY_MEDIUM = 2; +export const INSERTER_UTILITY_LOW = 1; +export const INSERTER_UTILITY_NONE = 0; +export const MILLISECONDS_PER_HOUR = 3600 * 1000; +export const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; +export const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; +export const ONE_MINUTE_IN_MS = 60 * 1000; diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index bc7b51a604fad5..a8a9ef08177e10 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -11,11 +11,7 @@ import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; import * as actions from './actions'; import controls from './controls'; - -/** - * Module Constants - */ -const MODULE_KEY = 'core/editor'; +import { MODULE_KEY } from './constants'; const store = registerStore( MODULE_KEY, { reducer, From ae3f60b5899165b8907915410616464202b4b5d9 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 6 Feb 2019 22:07:23 -0500 Subject: [PATCH 03/32] establish file structure, begin refactoring post effects/actions --- .../editor/src/store/actions/post-actions.js | 113 +++++++++++ .../src/store/actions/post-generators.js | 184 ++++++++++++++++++ .../src/store/actions/utils/notice-builder.js | 106 ++++++++++ 3 files changed, 403 insertions(+) create mode 100644 packages/editor/src/store/actions/post-actions.js create mode 100644 packages/editor/src/store/actions/post-generators.js create mode 100644 packages/editor/src/store/actions/utils/notice-builder.js diff --git a/packages/editor/src/store/actions/post-actions.js b/packages/editor/src/store/actions/post-actions.js new file mode 100644 index 00000000000000..232546c9ec9a1f --- /dev/null +++ b/packages/editor/src/store/actions/post-actions.js @@ -0,0 +1,113 @@ +/** + * External dependencies + */ +import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; + +/** + * Internal dependencies + */ +import { POST_UPDATE_TRANSACTION_ID } from '../constants'; + +/** + * Optimistic action for requesting post update start. + * + * @param {Object} options + * @return {Object} An action object + */ +export function requestPostUpdateStart( options = {} ) { + return { + type: 'REQUEST_POST_UPDATE_START', + optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, + options, + }; +} + +/** + * Optimistic action for requesting post update success. + * + * @param {Object} previousPost The previous post prior to update. + * @param {Object} post The new post after update + * @param {boolean} isRevision Whether the post is a revision or not. + * @param {Object} options Options passed through from the original action + * dispatch. + * @param {Object} postType The post type object. + * @return {Object} Action object. + */ +export function requestPostUpdateSuccess( { + previousPost, + post, + isRevision, + options, + postType, +} ) { + return { + type: 'REQUEST_POST_UPDATE_SUCCESS', + previousPost, + post, + optimist: { + // Note: REVERT is not a failure case here. Rather, it + // is simply reversing the assumption that the updates + // were applied to the post proper, such that the post + // treated as having unsaved changes. + type: isRevision ? REVERT : COMMIT, + id: POST_UPDATE_TRANSACTION_ID, + }, + options, + postType, + }; +} + +/** + * Optimistic action for requesting post update failure. + * + * @param {Object} post The post that failed updating. + * @param {Object} edits The fields that were being updated. + * @param {*} error The error from the failed call. + * @param {Object} options Options passed through from the original action + * dispatch. + * @return {Object} An action object + */ +export function requestPostUpdateFailure( { + post, + edits, + error, + options, +} ) { + return { + type: 'REQUEST_POST_UPDATE_FAILURE', + optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, + post, + edits, + error, + options, + }; +} + +/** + * Returns an action object used in signalling that a patch of updates for the + * latest version of the post have been received. + * + * @param {Object} edits Updated post fields. + * + * @return {Object} Action object. + */ +export function updatePost( edits ) { + return { + type: 'UPDATE_POST', + edits, + }; +} + +/** + * Returns action object produced by the updatePost creator augmented by + * an optimist option that signals optimistically applying updates. + * + * @param {Object} edits Updated post fields. + * @return {Object} Action object. + */ +export function optimisticUpdatePost( edits ) { + return { + ...updatePost( edits ), + optimist: { id: POST_UPDATE_TRANSACTION_ID }, + }; +} diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js new file mode 100644 index 00000000000000..60b5ec00e0a7cf --- /dev/null +++ b/packages/editor/src/store/actions/post-generators.js @@ -0,0 +1,184 @@ +/** + * External dependencies + */ +import { pick } from 'lodash'; + +/** + * Internal dependencies + */ +import { select, dispatch, apiFetch } from '../controls'; +import { MODULE_KEY, SAVE_POST_NOTICE_ID } from '../constants'; +import { + getNotifyOnSuccessNotificationArguments, + getNotifyOnFailNotificationArguments, +} from './utils/notice-builder'; + +/** + * Action generator for saving a post. + * + * @param {Object} options + */ +export function* savePost( options = {} ) { + const isEditedPostSaveable = yield select( + MODULE_KEY, + 'isEditedPostSaveable' + ); + if ( ! isEditedPostSaveable ) { + return; + } + let edits = yield select( + MODULE_KEY, + 'getPostEdits' + ); + const isAutosave = !! options.isAutosave; + + if ( isAutosave ) { + edits = pick( edits, [ 'title', 'content', 'excerpt' ] ); + } + + const isEditedPostNew = yield select( + MODULE_KEY, + 'isEditedPostNew', + ); + + // New posts (with auto-draft status) must be explicitly assigned draft + // status if there is not already a status assigned in edits (publish). + // Otherwise, they are wrongly left as auto-draft. Status is not always + // respected for autosaves, so it cannot simply be included in the pick + // above. This behavior relies on an assumption that an auto-draft post + // would never be saved by anyone other than the owner of the post, per + // logic within autosaves REST controller to save status field only for + // draft/auto-draft by current user. + // + // See: https://core.trac.wordpress.org/ticket/43316#comment:88 + // See: https://core.trac.wordpress.org/ticket/43316#comment:89 + if ( isEditedPostNew ) { + edits = { status: 'draft', ...edits }; + } + + const post = yield select( + MODULE_KEY, + 'getCurrentPost' + ); + + const editedPostContent = yield select( + MODULE_KEY, + 'getEditedPostContent' + ); + + let toSend = { + ...edits, + content: editedPostContent, + id: post.id, + }; + + const currentPostType = yield select( + MODULE_KEY, + 'getCurrentPostType' + ); + + const postType = yield select( + 'core', + 'getPostType', + currentPostType + ); + + yield dispatch( + MODULE_KEY, + 'requestPostUpdateStart', + options, + ); + + // Optimistically apply updates under the assumption that the post + // will be updated. See below logic in success resolution for revert + // if the autosave is applied as a revision. + yield dispatch( + MODULE_KEY, + 'optimisticUpdatePost', + toSend + ); + + let path = `/wp/v2/${ postType.rest_base }/${ post.id }`; + if ( isAutosave ) { + const autoSavePost = yield select( + MODULE_KEY, + 'getAutosave', + ); + // Ensure autosaves contain all expected fields, using autosave or + // post values as fallback if not otherwise included in edits. + toSend = { + ...pick( post, [ 'title', 'content', 'excerpt' ] ), + ...autoSavePost, + ...toSend, + }; + path += '/autosaves'; + } else { + yield dispatch( + 'core/notices', + 'removeNotice', + SAVE_POST_NOTICE_ID, + ); + yield dispatch( + 'core/notices', + 'removeNotice', + 'autosave-exists', + ); + } + + try { + const newPost = yield apiFetch( { + path, + method: 'PUT', + data: toSend, + } ); + const resetAction = isAutosave ? 'resetAutosave' : 'resetPost'; + + yield dispatch( MODULE_KEY, resetAction, newPost ); + + yield dispatch( + MODULE_KEY, + 'requestPostUpdateSuccess', + { + previousPost: post, + post: newPost, + options, + postType, + // An autosave may be processed by the server as a regular save + // when its update is requested by the author and the post was + // draft or auto-draft. + isRevision: newPost.id !== post.id, + } + ); + + const notifySuccessArgs = getNotifyOnSuccessNotificationArguments( { + previousPost: post, + post: newPost, + postType, + } ); + if ( notifySuccessArgs.length > 0 ) { + yield dispatch( + 'core/notices', + 'createSuccessNotice', + ...notifySuccessArgs + ); + } + } catch ( error ) { + yield dispatch( + MODULE_KEY, + 'requestPostUpdateFailure', + { post, edits, error, options } + ); + const notifyFailArgs = getNotifyOnFailNotificationArguments( { + post, + edits, + error, + } ); + if ( notifyFailArgs.length > 0 ) { + yield dispatch( + 'core/notices', + 'createErrorNotice', + ...notifyFailArgs + ); + } + } +} diff --git a/packages/editor/src/store/actions/utils/notice-builder.js b/packages/editor/src/store/actions/utils/notice-builder.js new file mode 100644 index 00000000000000..4300cd26280bfa --- /dev/null +++ b/packages/editor/src/store/actions/utils/notice-builder.js @@ -0,0 +1,106 @@ +/** + * External imports + */ +import { get, includes } from 'lodash'; + +/** + * Internal imports + */ +import { SAVE_POST_NOTICE_ID } from '../../constants'; + +/** + * WordPress imports + */ +import { __ } from '@wordpress/i18n'; + +/** + * Builds the arguments for a success notifiation dispatch. + * + * @param {Object} data Incoming data to build the arguments from. + * @return {Array} Arguments for dispatch. An empty array signals no + * notification should be sent. + */ +export function getNotifyOnSuccessNotificationArguments( data ) { + const { previousPost, post, postType } = data; + // Autosaves are neither shown a notice nor redirected. + if ( get( data.options, [ 'isAutosave' ] ) ) { + return []; + } + + const publishStatus = [ 'publish', 'private', 'future' ]; + const isPublished = includes( publishStatus, previousPost.status ); + const willPublish = includes( publishStatus, post.status ); + + let noticeMessage; + let shouldShowLink = get( postType, [ 'viewable' ], false ); + + if ( ! isPublished && ! willPublish ) { + // If saving a non-published post, don't show notice. + noticeMessage = null; + } else if ( isPublished && ! willPublish ) { + // If undoing publish status, show specific notice + noticeMessage = postType.labels.item_reverted_to_draft; + shouldShowLink = false; + } else if ( ! isPublished && willPublish ) { + // If publishing or scheduling a post, show the corresponding + // publish message + noticeMessage = { + publish: postType.labels.item_published, + private: postType.labels.item_published_privately, + future: postType.labels.item_scheduled, + }[ post.status ]; + } else { + // Generic fallback notice + noticeMessage = postType.labels.item_updated; + } + + if ( noticeMessage ) { + const actions = []; + if ( shouldShowLink ) { + actions.push( { + label: postType.labels.view_item, + url: post.link, + } ); + } + return [ + noticeMessage, + { + id: SAVE_POST_NOTICE_ID, + actions, + }, + ]; + } + return []; +} + +/** + * Builds the fail notification arguments for dispatch. + * + * @param {Object} data Incoming data to build the arguments with. + * @return {Array} Arguments for dispatch. An empty array signals no + * notification should be sent. + */ +export function getNotifyOnFailNotificationArguments( data ) { + const { post, edits, error } = data; + + if ( error && 'rest_autosave_no_changes' === error.code ) { + // Autosave requested a new autosave, but there were no changes. This shouldn't + // result in an error notice for the user. + return []; + } + + const publishStatus = [ 'publish', 'private', 'future' ]; + const isPublished = publishStatus.indexOf( post.status ) !== -1; + // If the post was being published, we show the corresponding publish error message + // Unless we publish an "updating failed" message + const messages = { + publish: __( 'Publishing failed' ), + private: __( 'Publishing failed' ), + future: __( 'Scheduling failed' ), + }; + const noticeMessage = ! isPublished && publishStatus.indexOf( edits.status ) !== -1 ? + messages[ edits.status ] : + __( 'Updating failed' ); + + return [ noticeMessage, { id: SAVE_POST_NOTICE_ID } ]; +} From a673b8e66ec644b2814017831cb48959a3c128f1 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Thu, 7 Feb 2019 21:32:14 -0500 Subject: [PATCH 04/32] Implement trashPost action-generator - also rename functions for getting notification dispatch arguments (a little less mouthy) --- .../src/store/actions/post-generators.js | 63 +++++++++++++++++-- .../src/store/actions/utils/notice-builder.js | 21 ++++++- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index 60b5ec00e0a7cf..17c76f2f31ee83 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -7,14 +7,19 @@ import { pick } from 'lodash'; * Internal dependencies */ import { select, dispatch, apiFetch } from '../controls'; -import { MODULE_KEY, SAVE_POST_NOTICE_ID } from '../constants'; import { - getNotifyOnSuccessNotificationArguments, - getNotifyOnFailNotificationArguments, + MODULE_KEY, + SAVE_POST_NOTICE_ID, + TRASH_POST_NOTICE_ID, +} from '../constants'; +import { + getNotificationArgumentsForSaveSuccess, + getNotificationArgumentsForSaveFail, + getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; /** - * Action generator for saving a post. + * Action generator for saving the current post in the editor. * * @param {Object} options */ @@ -150,7 +155,7 @@ export function* savePost( options = {} ) { } ); - const notifySuccessArgs = getNotifyOnSuccessNotificationArguments( { + const notifySuccessArgs = getNotificationArgumentsForSaveSuccess( { previousPost: post, post: newPost, postType, @@ -168,7 +173,7 @@ export function* savePost( options = {} ) { 'requestPostUpdateFailure', { post, edits, error, options } ); - const notifyFailArgs = getNotifyOnFailNotificationArguments( { + const notifyFailArgs = getNotificationArgumentsForSaveFail( { post, edits, error, @@ -182,3 +187,49 @@ export function* savePost( options = {} ) { } } } + +/** + * Action generator for trashing the current post in the editor. + */ +export function* trashPost() { + const postTypeSlug = yield select( + MODULE_KEY, + 'getCurrentPostType' + ); + const postType = yield select( + 'core', + 'getPostType', + postTypeSlug + ); + yield dispatch( + 'core/notices', + 'removeNotice', + TRASH_POST_NOTICE_ID + ); + try { + const post = yield select( + MODULE_KEY, + 'getCurrentPost' + ); + yield apiFetch( + { + path: `/wp/v2/${ postType.rest_base }/${ post.id }`, + method: 'DELETE', + } + ); + + // TODO: This should be an updatePost action (updating subsets of post properties), + // But right now editPost is tied with change detection. + yield dispatch( + MODULE_KEY, + 'resetPost', + { ...post, status: 'trash' } + ); + } catch ( error ) { + yield dispatch( + 'core/notices', + 'createErrorNotice', + getNotificationArgumentsForTrashFail( { error } ), + ); + } +} diff --git a/packages/editor/src/store/actions/utils/notice-builder.js b/packages/editor/src/store/actions/utils/notice-builder.js index 4300cd26280bfa..1ff3690e4a307a 100644 --- a/packages/editor/src/store/actions/utils/notice-builder.js +++ b/packages/editor/src/store/actions/utils/notice-builder.js @@ -6,7 +6,7 @@ import { get, includes } from 'lodash'; /** * Internal imports */ -import { SAVE_POST_NOTICE_ID } from '../../constants'; +import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; /** * WordPress imports @@ -20,7 +20,7 @@ import { __ } from '@wordpress/i18n'; * @return {Array} Arguments for dispatch. An empty array signals no * notification should be sent. */ -export function getNotifyOnSuccessNotificationArguments( data ) { +export function getNotificationArgumentsForSaveSuccess( data ) { const { previousPost, post, postType } = data; // Autosaves are neither shown a notice nor redirected. if ( get( data.options, [ 'isAutosave' ] ) ) { @@ -80,7 +80,7 @@ export function getNotifyOnSuccessNotificationArguments( data ) { * @return {Array} Arguments for dispatch. An empty array signals no * notification should be sent. */ -export function getNotifyOnFailNotificationArguments( data ) { +export function getNotificationArgumentsForSaveFail( data ) { const { post, edits, error } = data; if ( error && 'rest_autosave_no_changes' === error.code ) { @@ -104,3 +104,18 @@ export function getNotifyOnFailNotificationArguments( data ) { return [ noticeMessage, { id: SAVE_POST_NOTICE_ID } ]; } + +/** + * Builds the trash fail notifiation arguments for dispatch. + * + * @param {Object} data + * @return {Array} Arguments for dispatch. + */ +export function getNotificationArgumentsForTrashFail( data ) { + return [ + data.error.message && data.error.code !== 'unknown_error' ? + data.error.message : + __( 'Trashing failed' ), + { id: TRASH_POST_NOTICE_ID }, + ]; +} From 25c74c2a0f8e8ad821f7b064e51466fee5ff32fc Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Fri, 8 Feb 2019 07:55:10 -0500 Subject: [PATCH 05/32] fix import jsdocs --- packages/editor/src/store/actions/utils/notice-builder.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/actions/utils/notice-builder.js b/packages/editor/src/store/actions/utils/notice-builder.js index 1ff3690e4a307a..6038790ef84e94 100644 --- a/packages/editor/src/store/actions/utils/notice-builder.js +++ b/packages/editor/src/store/actions/utils/notice-builder.js @@ -1,10 +1,10 @@ /** - * External imports + * External dependencies */ import { get, includes } from 'lodash'; /** - * Internal imports + * Internal dependencies */ import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; From 54fbc3870be1e8690b91cf50f88245331e49f1af Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Fri, 8 Feb 2019 15:12:46 -0500 Subject: [PATCH 06/32] update controls to use new createRegistryControl interface --- packages/editor/src/store/controls.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index 270a8d09aa9cda..442e02c9e5b085 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -42,12 +42,14 @@ export default { API_FETCH( { request } ) { return triggerFetch( request ); }, - SELECT: createRegistryControl( (registry ) => ( { storeKey, selectorName, args } ) { - return registry.select( storeKey )[ selectorName ]( ...args ); - } ), - DISPATCH: createRegistryControl( ( registry ) => ( { storeKey, actionName, args } ) => { - return registry.dispatch( storeKey )[ actionName ]( ...args ); - } ), + SELECT: createRegistryControl( + ( registry ) => ( { storeKey, selectorName, args } ) => { + return registry.select( storeKey )[ selectorName ]( ...args ); + } + ), + DISPATCH: createRegistryControl( + ( registry ) => ( { storeKey, actionName, args } ) => { + return registry.dispatch( storeKey )[ actionName ]( ...args ); + } + ), }; - -export default controls; From 455d6a8bc09cf9e67ecc6be3c059e407b1028ef9 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Fri, 8 Feb 2019 15:23:19 -0500 Subject: [PATCH 07/32] add action generator for refreshing the current post --- .../src/store/actions/post-generators.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index 17c76f2f31ee83..f1143389466d0e 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -233,3 +233,34 @@ export function* trashPost() { ); } } + +/** + * Action generator for handling refreshing the current post. + */ +export function* refreshPost() { + const post = yield select( + MODULE_KEY, + 'getCurrentPost' + ); + const postTypeSlug = yield select( + MODULE_KEY, + 'getCurrentPostType' + ); + const postType = yield select( + 'core', + 'getPostType', + postTypeSlug + ); + const newPost = yield apiFetch( + { + // Timestamp arg allows caller to bypass browser caching, which is + // expected for this specific function. + path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit&_timestamp=${ Date.now() }`, + } + ); + yield dispatch( + MODULE_KEY, + 'resetPost', + newPost + ); +} From 5d246e544107b18df7fed7b548638703c0c63392 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Sun, 10 Feb 2019 21:40:48 -0500 Subject: [PATCH 08/32] add tests for new code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - also removes old code that’s been replaced and fixes up imports --- packages/editor/src/store/actions.js | 49 +- packages/editor/src/store/actions/index.js | 2 + .../editor/src/store/actions/post-actions.js | 89 ++- .../src/store/actions/post-generators.js | 33 +- .../src/store/actions/test/post-actions.js | 138 ++++ .../src/store/actions/test/post-generators.js | 621 ++++++++++++++++++ .../src/store/actions/utils/notice-builder.js | 13 +- .../actions/utils/test/notice-builder.js | 179 +++++ packages/editor/src/store/effects/posts.js | 319 --------- packages/editor/src/store/index.js | 6 +- packages/editor/src/store/test/actions.js | 2 +- 11 files changed, 1104 insertions(+), 347 deletions(-) create mode 100644 packages/editor/src/store/actions/index.js create mode 100644 packages/editor/src/store/actions/test/post-actions.js create mode 100644 packages/editor/src/store/actions/test/post-generators.js create mode 100644 packages/editor/src/store/actions/utils/test/notice-builder.js delete mode 100644 packages/editor/src/store/effects/posts.js diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 0d508375c37c84..a6ccbc1829a1e3 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -44,9 +44,52 @@ export function resetPost( post ) { /** * Returns an action object used in signalling that the latest autosave of the - * post has been received, by initialization or autosave. + * specified client ID has been selected, optionally accepting a position + * value reflecting its selection directionality. An initialPosition of -1 + * reflects a reverse selection. * - * @param {Object} post Autosave post object. + * @param {string} clientId Block client ID. + * @param {?number} initialPosition Optional initial position. Pass as -1 to + * reflect reverse selection. + * + * @return {Object} Action object. + */ +export function selectBlock( clientId, initialPosition = null ) { + return { + type: 'SELECT_BLOCK', + initialPosition, + clientId, + }; +} + +export function startMultiSelect() { + return { + type: 'START_MULTI_SELECT', + }; +} + +export function stopMultiSelect() { + return { + type: 'STOP_MULTI_SELECT', + }; +} + +export function multiSelect( start, end ) { + return { + type: 'MULTI_SELECT', + start, + end, + }; +} + +export function clearSelectedBlock() { + return { + type: 'CLEAR_SELECTED_BLOCK', + }; +} + +/** + * Returns an action object that enables or disables block selection. * * @return {Object} Action object. */ @@ -125,8 +168,6 @@ export function refreshPost() { export function trashPost( postId, postType ) { return { type: 'TRASH_POST', - postId, - postType, }; } diff --git a/packages/editor/src/store/actions/index.js b/packages/editor/src/store/actions/index.js new file mode 100644 index 00000000000000..30000f58e19226 --- /dev/null +++ b/packages/editor/src/store/actions/index.js @@ -0,0 +1,2 @@ +export * from './post-actions'; +export * from './post-generators'; diff --git a/packages/editor/src/store/actions/post-actions.js b/packages/editor/src/store/actions/post-actions.js index 232546c9ec9a1f..4dc8bf651bf8fd 100644 --- a/packages/editor/src/store/actions/post-actions.js +++ b/packages/editor/src/store/actions/post-actions.js @@ -9,12 +9,42 @@ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; import { POST_UPDATE_TRANSACTION_ID } from '../constants'; /** - * Optimistic action for requesting post update start. + * Returns an action object used in signalling that the latest version of the + * post has been received, either by initialization or save. + * + * @param {Object} post Post object. + * + * @return {Object} Action object. + */ +export function resetPost( post ) { + return { + type: 'RESET_POST', + post, + }; +} + +/** + * Returns an action object used in signalling that the latest autosave of the + * post has been received, by initialization or autosave. + * + * @param {Object} post Autosave post object. + * + * @return {Object} Action object. + */ +export function resetAutosave( post ) { + return { + type: 'RESET_AUTOSAVE', + post, + }; +} + +/** + * Optimistic action for dispatching that a post update request has started. * * @param {Object} options * @return {Object} An action object */ -export function requestPostUpdateStart( options = {} ) { +export function __experimentalRequestPostUpdateStart( options = {} ) { return { type: 'REQUEST_POST_UPDATE_START', optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, @@ -23,7 +53,8 @@ export function requestPostUpdateStart( options = {} ) { } /** - * Optimistic action for requesting post update success. + * Optimistic action for indicating that the request post update has completed + * successfully. * * @param {Object} previousPost The previous post prior to update. * @param {Object} post The new post after update @@ -33,7 +64,7 @@ export function requestPostUpdateStart( options = {} ) { * @param {Object} postType The post type object. * @return {Object} Action object. */ -export function requestPostUpdateSuccess( { +export function __experimentalRequestPostUpdateSuccess( { previousPost, post, isRevision, @@ -58,7 +89,8 @@ export function requestPostUpdateSuccess( { } /** - * Optimistic action for requesting post update failure. + * Optimistic action for indicating that the request post update has completed + * with a failure. * * @param {Object} post The post that failed updating. * @param {Object} edits The fields that were being updated. @@ -67,7 +99,7 @@ export function requestPostUpdateSuccess( { * dispatch. * @return {Object} An action object */ -export function requestPostUpdateFailure( { +export function __experimentalRequestPostUpdateFailure( { post, edits, error, @@ -98,6 +130,21 @@ export function updatePost( edits ) { }; } +/** + * Returns an action object used in signalling that attributes of the post have + * been edited. + * + * @param {Object} edits Post attributes to edit. + * + * @return {Object} Action object. + */ +export function editPost( edits ) { + return { + type: 'EDIT_POST', + edits, + }; +} + /** * Returns action object produced by the updatePost creator augmented by * an optimist option that signals optimistically applying updates. @@ -105,9 +152,37 @@ export function updatePost( edits ) { * @param {Object} edits Updated post fields. * @return {Object} Action object. */ -export function optimisticUpdatePost( edits ) { +export function __experimentalOptimisticUpdatePost( edits ) { return { ...updatePost( edits ), optimist: { id: POST_UPDATE_TRANSACTION_ID }, }; } + +/** + * Returns an action object used to signal that post saving is locked. + * + * @param {string} lockName The lock name. + * + * @return {Object} Action object + */ +export function lockPostSaving( lockName ) { + return { + type: 'LOCK_POST_SAVING', + lockName, + }; +} + +/** + * Returns an action object used to signal that post saving is unlocked. + * + * @param {string} lockName The lock name. + * + * @return {Object} Action object + */ +export function unlockPostSaving( lockName ) { + return { + type: 'UNLOCK_POST_SAVING', + lockName, + }; +} diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index f1143389466d0e..038132f9fd3e59 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -90,7 +90,7 @@ export function* savePost( options = {} ) { yield dispatch( MODULE_KEY, - 'requestPostUpdateStart', + '__experimentalRequestPostUpdateStart', options, ); @@ -99,7 +99,7 @@ export function* savePost( options = {} ) { // if the autosave is applied as a revision. yield dispatch( MODULE_KEY, - 'optimisticUpdatePost', + '__experimentalOptimisticUpdatePost', toSend ); @@ -142,7 +142,7 @@ export function* savePost( options = {} ) { yield dispatch( MODULE_KEY, - 'requestPostUpdateSuccess', + '__experimentalRequestPostUpdateSuccess', { previousPost: post, post: newPost, @@ -159,6 +159,7 @@ export function* savePost( options = {} ) { previousPost: post, post: newPost, postType, + options, } ); if ( notifySuccessArgs.length > 0 ) { yield dispatch( @@ -170,7 +171,7 @@ export function* savePost( options = {} ) { } catch ( error ) { yield dispatch( MODULE_KEY, - 'requestPostUpdateFailure', + '__experimentalRequestPostUpdateFailure', { post, edits, error, options } ); const notifyFailArgs = getNotificationArgumentsForSaveFail( { @@ -188,6 +189,15 @@ export function* savePost( options = {} ) { } } +/** + * Action generator used in signalling that the post should autosave. + * + * @param {Object?} options Extra flags to identify the autosave. + */ +export function* autosave( options ) { + yield* actions.savePost( { isAutosave: true, ...options } ); +} + /** * Action generator for trashing the current post in the editor. */ @@ -218,8 +228,8 @@ export function* trashPost() { } ); - // TODO: This should be an updatePost action (updating subsets of post properties), - // But right now editPost is tied with change detection. + // TODO: This should be an updatePost action (updating subsets of post + // properties), but right now editPost is tied with change detection. yield dispatch( MODULE_KEY, 'resetPost', @@ -229,7 +239,7 @@ export function* trashPost() { yield dispatch( 'core/notices', 'createErrorNotice', - getNotificationArgumentsForTrashFail( { error } ), + ...getNotificationArgumentsForTrashFail( { error } ), ); } } @@ -264,3 +274,12 @@ export function* refreshPost() { newPost ); } + +const actions = { + savePost, + autosave, + trashPost, + refreshPost, +}; + +export default actions; diff --git a/packages/editor/src/store/actions/test/post-actions.js b/packages/editor/src/store/actions/test/post-actions.js new file mode 100644 index 00000000000000..3c8ff72205cebd --- /dev/null +++ b/packages/editor/src/store/actions/test/post-actions.js @@ -0,0 +1,138 @@ +/** + * External dependencies + */ +import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; + +/** + * Internal dependencies + */ +import { + resetPost, + resetAutosave, + __experimentalRequestPostUpdateStart as requestPostUpdateStart, + __experimentalRequestPostUpdateSuccess as requestPostUpdateSuccess, + __experimentalRequestPostUpdateFailure as requestPostUpdateFailure, + updatePost, + editPost, + __experimentalOptimisticUpdatePost as optimisticUpdatePost, + lockPostSaving, + unlockPostSaving, +} from '../post-actions'; +import { POST_UPDATE_TRANSACTION_ID } from '../../constants'; + +describe( 'actions', () => { + describe( 'resetPost', () => { + it( 'should return the RESET_POST action', () => { + const post = {}; + const result = resetPost( post ); + expect( result ).toEqual( { + type: 'RESET_POST', + post, + } ); + } ); + } ); + describe( 'resetAutosave', () => { + it( 'should return the RESET_AUTOSAVE action', () => { + const post = {}; + const result = resetAutosave( post ); + expect( result ).toEqual( { + type: 'RESET_AUTOSAVE', + post, + } ); + } ); + } ); + describe( 'requestPostUpdateStart', () => { + it( 'should return the REQUEST_POST_UPDATE_START action', () => { + const result = requestPostUpdateStart(); + expect( result ).toEqual( { + type: 'REQUEST_POST_UPDATE_START', + optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, + options: {}, + } ); + } ); + } ); + describe( 'requestPostUpdateSuccess', () => { + it( 'should return the REQUEST_POST_UPDATE_SUCCESS action', () => { + const testActionData = { + previousPost: {}, + post: {}, + options: {}, + postType: 'post', + }; + const result = requestPostUpdateSuccess( { + ...testActionData, + isRevision: false, + } ); + expect( result ).toEqual( { + ...testActionData, + type: 'REQUEST_POST_UPDATE_SUCCESS', + optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, + } ); + } ); + } ); + describe( 'requestPostUpdateFailure', () => { + it( 'should return the REQUEST_POST_UPDATE_FAILURE action', () => { + const testActionData = { + post: {}, + options: {}, + edits: {}, + error: {}, + }; + const result = requestPostUpdateFailure( testActionData ); + expect( result ).toEqual( { + ...testActionData, + type: 'REQUEST_POST_UPDATE_FAILURE', + optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, + } ); + } ); + } ); + describe( 'updatePost', () => { + it( 'should return the UPDATE_POST action', () => { + const edits = {}; + const result = updatePost( edits ); + expect( result ).toEqual( { + type: 'UPDATE_POST', + edits, + } ); + } ); + } ); + describe( 'editPost', () => { + it( 'should return the EDIT_POST action', () => { + const edits = {}; + const result = editPost( edits ); + expect( result ).toEqual( { + type: 'EDIT_POST', + edits, + } ); + } ); + } ); + describe( 'optimisticUpdatePost', () => { + it( 'should return the UPDATE_POST action with optimist property', () => { + const edits = {}; + const result = optimisticUpdatePost( edits ); + expect( result ).toEqual( { + type: 'UPDATE_POST', + edits, + optimist: { id: POST_UPDATE_TRANSACTION_ID }, + } ); + } ); + } ); + describe( 'lockPostSaving', () => { + it( 'should return the LOCK_POST_SAVING action', () => { + const result = lockPostSaving( 'test' ); + expect( result ).toEqual( { + type: 'LOCK_POST_SAVING', + lockName: 'test', + } ); + } ); + } ); + describe( 'unlockPostSaving', () => { + it( 'should return the UNLOCK_POST_SAVING action', () => { + const result = unlockPostSaving( 'test' ); + expect( result ).toEqual( { + type: 'UNLOCK_POST_SAVING', + lockName: 'test', + } ); + } ); + } ); +} ); diff --git a/packages/editor/src/store/actions/test/post-generators.js b/packages/editor/src/store/actions/test/post-generators.js new file mode 100644 index 00000000000000..4a44c6b923790c --- /dev/null +++ b/packages/editor/src/store/actions/test/post-generators.js @@ -0,0 +1,621 @@ +/** + * Internal dependencies. + */ +import actions, { + savePost, + autosave, + trashPost, + refreshPost, +} from '../post-generators'; +import { select, dispatch, apiFetch } from '../../controls'; +import { + MODULE_KEY, + SAVE_POST_NOTICE_ID, + TRASH_POST_NOTICE_ID, +} from '../../constants'; + +jest.mock( '../../controls' ); + +select.mockImplementation( ( ...args ) => { + const { select: actualSelect } = jest.requireActual( '../../controls' ); + return actualSelect( ...args ); +} ); + +dispatch.mockImplementation( ( ...args ) => { + const { dispatch: actualDispatch } = jest.requireActual( '../../controls' ); + return actualDispatch( ...args ); +} ); + +const apiFetchThrowError = ( error ) => { + apiFetch.mockClear(); + apiFetch.mockImplementation( () => { + throw error; + } ); +}; + +const apiFetchDoActual = () => { + apiFetch.mockClear(); + apiFetch.mockImplementation( ( ...args ) => { + const { apiFetch: fetch } = jest.requireActual( '../../controls' ); + return fetch( ...args ); + } ); +}; + +const postType = { + rest_base: 'posts', + labels: { + item_updated: 'Updated Post', + item_published: 'Post published', + }, +}; +const postTypeSlug = 'post'; + +describe( 'Post generator actions', () => { + describe( 'savePost()', () => { + let fulfillment, + edits, + currentPost, + currentPostStatus, + editPostToSendOptimistic, + autoSavePost, + autoSavePostToSend, + savedPost, + savedPostStatus, + isAutosave, + isEditedPostNew, + savedPostMessage; + beforeEach( () => { + edits = ( defaultStatus = null ) => { + const postObject = { + title: 'foo', + content: 'bar', + excerpt: 'cheese', + foo: 'bar', + }; + if ( defaultStatus !== null ) { + postObject.status = defaultStatus; + } + return postObject; + }; + currentPost = () => ( { + id: 44, + title: 'bar', + content: 'bar', + excerpt: 'crackers', + status: currentPostStatus, + } ); + editPostToSendOptimistic = () => { + const postObject = { + ...edits(), + content: editedPostContent, + id: currentPost().id, + }; + if ( ! postObject.status && isEditedPostNew ) { + postObject.status = 'draft'; + } + if ( isAutosave ) { + delete postObject.foo; + } + return postObject; + }; + autoSavePost = { status: 'autosave', bar: 'foo' }; + autoSavePostToSend = () => ( + { + ...editPostToSendOptimistic(), + bar: 'foo', + status: 'autosave', + } + ); + savedPost = () => ( + { + ...currentPost(), + ...editPostToSendOptimistic(), + content: editedPostContent, + status: savedPostStatus, + } + ); + } ); + const editedPostContent = 'to infinity and beyond'; + const reset = ( isAutosaving ) => fulfillment = savePost( + { isAutosave: isAutosaving } + ); + const rewind = ( isAutosaving, isNewPost ) => { + reset( isAutosaving ); + fulfillment.next(); + fulfillment.next( true ); + fulfillment.next( edits() ); + fulfillment.next( isNewPost ); + fulfillment.next( currentPost() ); + fulfillment.next( editedPostContent ); + fulfillment.next( postTypeSlug ); + fulfillment.next( postType ); + fulfillment.next(); + if ( isAutosaving ) { + fulfillment.next(); + } else { + fulfillment.next(); + fulfillment.next(); + } + }; + const initialTestConditions = [ + [ + 'yields action for selecting if edited post is saveable', + () => true, + () => { + reset( isAutosave ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( MODULE_KEY, 'isEditedPostSaveable' ) + ); + }, + ], + [ + 'yields action for selecting the post edits done', + () => true, + () => { + const { value } = fulfillment.next( true ); + expect( value ).toEqual( + select( MODULE_KEY, 'getPostEdits' ) + ); + }, + ], + [ + 'yields action for selecting whether the edited post is new', + () => true, + () => { + const { value } = fulfillment.next( edits() ); + expect( value ).toEqual( + select( MODULE_KEY, 'isEditedPostNew' ) + ); + }, + ], + [ + 'yields action for selecting the current post', + () => true, + () => { + const { value } = fulfillment.next( isEditedPostNew ); + expect( value ).toEqual( + select( MODULE_KEY, 'getCurrentPost' ) + ); + }, + ], + [ + 'yields action for selecting the edited post content', + () => true, + () => { + const { value } = fulfillment.next( currentPost() ); + expect( value ).toEqual( + select( MODULE_KEY, 'getEditedPostContent' ) + ); + }, + ], + [ + 'yields action for selecting current post type slug', + () => true, + () => { + const { value } = fulfillment.next( editedPostContent ); + expect( value ).toEqual( + select( MODULE_KEY, 'getCurrentPostType' ) + ); + }, + ], + [ + 'yields action for selecting the post type object', + () => true, + () => { + const { value } = fulfillment.next( postTypeSlug ); + expect( value ).toEqual( + select( 'core', 'getPostType', postTypeSlug ) + ); + }, + ], + [ + 'yields action for dispatching request post update start', + () => true, + () => { + const { value } = fulfillment.next( postType ); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateStart', + { isAutosave } + ) + ); + }, + ], + [ + 'yields action for dispatching optimistic update of post', + () => true, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalOptimisticUpdatePost', + editPostToSendOptimistic() + ) + ); + }, + ], + [ + 'yields action for dispatching the removal of save post notice', + ( isAutosaving ) => ! isAutosaving, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'removeNotice', + SAVE_POST_NOTICE_ID, + ) + ); + }, + ], + [ + 'yields action for dispatching the removal of autosave notice', + ( isAutosaving ) => ! isAutosaving, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'removeNotice', + 'autosave-exists' + ) + ); + }, + ], + [ + 'yield action for selecting the autoSavePost', + ( isAutosaving ) => isAutosaving, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( + MODULE_KEY, + 'getAutosave' + ) + ); + }, + ], + ]; + const fetchErrorConditions = [ + [ + 'yields action for dispatching post update failure', + () => { + const error = { foo: 'bar', code: 'fail' }; + apiFetchThrowError( error ); + const editsObject = edits(); + const { value } = isAutosave ? + fulfillment.next( autoSavePost ) : + fulfillment.next(); + if ( isAutosave ) { + delete editsObject.foo; + } + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateFailure', + { + post: currentPost(), + edits: isEditedPostNew ? + { ...editsObject, status: 'draft' } : + editsObject, + error, + options: { isAutosave }, + } + ) + ); + }, + ], + [ + 'yields action for dispatching an appropriate error notice', + () => { + const { value } = fulfillment.next( [ 'foo', 'bar' ] ); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'createErrorNotice', + ...[ 'Updating failed', { id: 'SAVE_POST_NOTICE_ID' } ] + ) + ); + }, + ], + ]; + const fetchSuccessConditions = [ + [ + 'yields action for updating the post via the api', + () => { + apiFetchDoActual(); + rewind( isAutosave, isEditedPostNew ); + const { value } = isAutosave ? + fulfillment.next( autoSavePost ) : + fulfillment.next(); + const data = isAutosave ? + autoSavePostToSend() : + editPostToSendOptimistic(); + const path = isAutosave ? '/autosaves' : ''; + expect( value ).toEqual( + apiFetch( + { + path: `/wp/v2/${ postType.rest_base }/${ editPostToSendOptimistic().id }${ path }`, + method: 'PUT', + data, + } + ) + ); + }, + ], + [ + 'yields action for dispatch the appropriate reset action', + () => { + const { value } = fulfillment.next( savedPost() ); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + isAutosave ? 'resetAutosave' : 'resetPost', + savedPost() + ) + ); + }, + ], + [ + 'yields action for dispatching the post update success', + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateSuccess', + { + previousPost: currentPost(), + post: savedPost(), + options: { isAutosave }, + postType, + isRevision: false, + } + ) + ); + }, + ], + [ + 'yields dispatch action for success notification', + () => { + const { value } = fulfillment.next( [ 'foo', 'bar' ] ); + const expected = isAutosave ? + undefined : + dispatch( + 'core/notices', + 'createSuccessNotice', + ...[ + savedPostMessage, + { actions: [], id: 'SAVE_POST_NOTICE_ID' }, + ] + ); + expect( value ).toEqual( expected ); + }, + ], + ]; + + const conditionalRunTestRoutine = ( isAutosaving ) => ( [ + testDescription, + shouldRun, + testRoutine, + ] ) => { + if ( shouldRun( isAutosaving ) ) { + it( testDescription, () => { + testRoutine(); + } ); + } + }; + + const testRunRoutine = ( [ testDescription, testRoutine ] ) => { + it( testDescription, () => { + testRoutine(); + } ); + }; + + describe( 'yields with expected responses when edited post is not ' + + 'saveable', () => { + it( 'yields action for selecting if edited post is saveable', () => { + reset( false ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( MODULE_KEY, 'isEditedPostSaveable' ) + ); + } ); + it( 'if edited post is not saveable then bails', () => { + const { value, done } = fulfillment.next( false ); + expect( done ).toBe( true ); + expect( value ).toBeUndefined(); + } ); + } ); + describe( 'yields with expected responses for when not autosaving ' + + 'and edited post is new', () => { + beforeEach( () => { + isAutosave = false; + isEditedPostNew = true; + savedPostStatus = 'publish'; + currentPostStatus = 'draft'; + savedPostMessage = 'Post published'; + } ); + initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); + describe( 'fetch action throwing an error', () => { + fetchErrorConditions.forEach( testRunRoutine ); + } ); + describe( 'fetch action not throwing an error', () => { + fetchSuccessConditions.forEach( testRunRoutine ); + } ); + } ); + + describe( 'yields with expected responses for when not autosaving ' + + 'and edited post is not new', () => { + beforeEach( () => { + isAutosave = false; + isEditedPostNew = false; + currentPostStatus = 'publish'; + savedPostStatus = 'publish'; + savedPostMessage = 'Updated Post'; + } ); + initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); + describe( 'fetch action throwing error', () => { + fetchErrorConditions.forEach( testRunRoutine ); + } ); + describe( 'fetch action not throwing error', () => { + fetchSuccessConditions.forEach( testRunRoutine ); + } ); + } ); + describe( 'yields with expected responses for when autosaving is true ' + + 'and edited post is not new', () => { + beforeEach( () => { + isAutosave = true; + isEditedPostNew = false; + currentPostStatus = 'autosave'; + savedPostStatus = 'publish'; + savedPostMessage = 'Post published'; + } ); + initialTestConditions.forEach( conditionalRunTestRoutine( true ) ); + describe( 'fetch action throwing error', () => { + fetchErrorConditions.forEach( testRunRoutine ); + } ); + describe( 'fetch action not throwing error', () => { + fetchSuccessConditions.forEach( testRunRoutine ); + } ); + } ); + } ); +} ); +describe( 'autosave()', () => { + let savePostSpy; + beforeAll( () => savePostSpy = jest.spyOn( actions, 'savePost' ) ); + afterAll( () => savePostSpy.mockRestore() ); + // autosave is mostly covered by `savePost` tests so just test the correct call + it( 'calls savePost with the correct arguments', () => { + const fulfillment = autosave(); + fulfillment.next(); + expect( savePostSpy ).toHaveBeenCalled(); + expect( savePostSpy ).toHaveBeenCalledWith( { isAutosave: true } ); + } ); +} ); +describe( 'trashPost()', () => { + let fulfillment; + const currentPost = { id: 10, content: 'foo', status: 'publish' }; + const reset = () => fulfillment = trashPost(); + const rewind = () => { + reset(); + fulfillment.next(); + fulfillment.next( postTypeSlug ); + fulfillment.next( postType ); + fulfillment.next(); + }; + it( 'yields expected action for selecting the current post type slug', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPostType', + ) ); + } ); + it( 'yields expected action for selecting the post type object', () => { + const { value } = fulfillment.next( postTypeSlug ); + expect( value ).toEqual( select( + 'core', + 'getPostType', + postTypeSlug + ) ); + } ); + it( 'yields expected action for dispatching removing the trash notice ' + + 'for the post', () => { + const { value } = fulfillment.next( postType ); + expect( value ).toEqual( dispatch( + 'core/notices', + 'removeNotice', + TRASH_POST_NOTICE_ID + ) ); + } ); + it( 'yields expected action for selecting the currentPost', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPost' + ) ); + } ); + describe( 'expected yields when fetch throws an error', () => { + it( 'yields expected action for dispatching an error notice', () => { + const error = { foo: 'bar', code: 'fail' }; + apiFetchThrowError( error ); + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( dispatch( + 'core/notices', + 'createErrorNotice', + 'Trashing failed', + { id: TRASH_POST_NOTICE_ID }, + ) ); + } ); + } ); + describe( 'expected yields when fetch does not throw an error', () => { + it( 'yields expected action object for the api fetch', () => { + apiFetchDoActual(); + rewind(); + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( apiFetch( + { + path: `/wp/v2/${ postType.rest_base }/${ currentPost.id }`, + method: 'DELETE', + } + ) ); + } ); + it( 'yields expected dispatch action for resetting the post', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( dispatch( + MODULE_KEY, + 'resetPost', + { ...currentPost, status: 'trash' } + ) ); + } ); + } ); +} ); +describe( 'refreshPost()', () => { + let fulfillment; + const currentPost = { id: 10, content: 'foo' }; + const reset = () => fulfillment = refreshPost(); + it( 'yields expected action for selecting the currentPost', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPost', + ) ); + } ); + it( 'yields expected action for selecting the current post type', () => { + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPostType' + ) ); + } ); + it( 'yields expected action for selecting the post type object', () => { + const { value } = fulfillment.next( postTypeSlug ); + expect( value ).toEqual( select( + 'core', + 'getPostType', + postTypeSlug + ) ); + } ); + it( 'yields expected action for the api fetch call', () => { + const { value } = fulfillment.next( postType ); + apiFetchDoActual(); + // since the timestamp is a computed value we can't do a direct comparison. + // so we'll just see if the path has most of the value. + expect( value.request.path ).toEqual( expect.stringContaining( + `/wp/v2/${ postType.rest_base }/${ currentPost.id }?context=edit&_timestamp=` + ) ); + } ); + it( 'yields expected action for dispatching the reset of the post', () => { + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( dispatch( + MODULE_KEY, + 'resetPost', + currentPost + ) ); + } ); +} ); diff --git a/packages/editor/src/store/actions/utils/notice-builder.js b/packages/editor/src/store/actions/utils/notice-builder.js index 6038790ef84e94..237203b561a0a3 100644 --- a/packages/editor/src/store/actions/utils/notice-builder.js +++ b/packages/editor/src/store/actions/utils/notice-builder.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + /** * External dependencies */ @@ -9,12 +14,7 @@ import { get, includes } from 'lodash'; import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; /** - * WordPress imports - */ -import { __ } from '@wordpress/i18n'; - -/** - * Builds the arguments for a success notifiation dispatch. + * Builds the arguments for a success notification dispatch. * * @param {Object} data Incoming data to build the arguments from. * @return {Array} Arguments for dispatch. An empty array signals no @@ -82,7 +82,6 @@ export function getNotificationArgumentsForSaveSuccess( data ) { */ export function getNotificationArgumentsForSaveFail( data ) { const { post, edits, error } = data; - if ( error && 'rest_autosave_no_changes' === error.code ) { // Autosave requested a new autosave, but there were no changes. This shouldn't // result in an error notice for the user. diff --git a/packages/editor/src/store/actions/utils/test/notice-builder.js b/packages/editor/src/store/actions/utils/test/notice-builder.js new file mode 100644 index 00000000000000..efc52c5947145d --- /dev/null +++ b/packages/editor/src/store/actions/utils/test/notice-builder.js @@ -0,0 +1,179 @@ +/** + * Internal dependencies. + */ +import { + getNotificationArgumentsForSaveSuccess, + getNotificationArgumentsForSaveFail, + getNotificationArgumentsForTrashFail, +} from '../notice-builder'; +import { + SAVE_POST_NOTICE_ID, + TRASH_POST_NOTICE_ID, +} from '../../../constants'; + +describe( 'getNotificationArgumentsForSaveSuccess()', () => { + const postType = { + labels: { + item_reverted_to_draft: 'draft', + item_published: 'publish', + item_published_privately: 'private', + item_scheduled: 'scheduled', + item_updated: 'updated', + view_item: 'view', + }, + viewable: false, + }; + const previousPost = { + status: 'publish', + link: 'some_link', + }; + const post = { ...previousPost }; + const defaultExpectedAction = { id: SAVE_POST_NOTICE_ID, actions: [] }; + [ + [ + 'when previous post is not published and post will not be published', + [ 'draft', 'draft', false ], + [], + ], + [ + 'when previous post is published and post will be unpublished', + [ 'publish', 'draft', false ], + [ 'draft', defaultExpectedAction ], + ], + [ + 'when previous post is not published and post will be published', + [ 'draft', 'publish', false ], + [ 'publish', defaultExpectedAction ], + ], + [ + 'when previous post is not published and post will be privately ' + + 'published', + [ 'draft', 'private', false ], + [ 'private', defaultExpectedAction ], + ], + [ + 'when previous post is not published and post will be scheduled for ' + + 'publishing', + [ 'draft', 'future', false ], + [ 'scheduled', defaultExpectedAction ], + ], + [ + 'when both are considered published', + [ 'private', 'publish', false ], + [ 'updated', defaultExpectedAction ], + ], + [ + 'when both are considered published and the post type is viewable', + [ 'private', 'publish', true ], + [ + 'updated', + { + ...defaultExpectedAction, + actions: [ { label: 'view', url: 'some_link' } ], + }, + ], + ], + ].forEach( ( [ + description, + [ previousPostStatus, postStatus, isViewable ], + expectedValue, + ] ) => { + it( description, () => { + previousPost.status = previousPostStatus; + post.status = postStatus; + postType.viewable = isViewable; + expect( getNotificationArgumentsForSaveSuccess( + { + previousPost, + post, + postType, + } + ) ).toEqual( expectedValue ); + } ); + } ); +} ); +describe( 'getNotificationArgumentsForSaveFail()', () => { + const error = { code: '42' }; + const post = { status: 'publish' }; + const edits = { status: 'publish' }; + const defaultExpectedAction = { id: SAVE_POST_NOTICE_ID, actions: [] }; + [ + [ + 'when error code is `rest_autosave_no_changes`', + 'rest_autosave_no_changes', + [ 'publish', 'publish' ], + [], + ], + [ + 'when post is not published and edits is published', + '', + [ 'draft', 'publish' ], + [ 'Publishing failed', defaultExpectedAction ], + ], + [ + 'when post is published and edits is privately published', + '', + [ 'draft', 'private' ], + [ 'Publishing failed', defaultExpectedAction ], + ], + [ + 'when post is published and edits is scheduled to be published', + '', + [ 'draft', 'future' ], + [ 'Scheduling failed', defaultExpectedAction ], + ], + [ + 'when post is published and edits is published', + '', + [ 'publish', 'publish' ], + [ 'Updating failed', defaultExpectedAction ], + ], + ].forEach( ( [ + description, + errorCode, + [ postStatus, editsStatus ], + expectedValue, + ] ) => { + it( description, () => { + post.status = postStatus; + error.code = errorCode; + edits.status = editsStatus; + expect( getNotificationArgumentsForSaveFail( + { + post, + edits, + error, + } + ) ).toEqual( expectedValue ); + } ); + } ); +} ); +describe( 'getNotificationArgumentsForTrashFail()', () => { + const defaultExpectedAction = { id: TRASH_POST_NOTICE_ID }; + [ + [ + 'when there is an error message and the error code is not "unknown_error"', + { message: 'foo', code: '' }, + [ 'foo', defaultExpectedAction ], + ], + [ + 'when there is an error message and the error code is "unknown error"', + { message: 'foo', code: 'unknown_error' }, + [ 'Trashing failed', defaultExpectedAction ], + ], + [ + 'when there is not an error message', + { code: 42 }, + [ 'Trashing failed', defaultExpectedAction ], + ], + ].forEach( ( [ + description, + error, + expectedValue, + ] ) => { + it( description, () => { + expect( getNotificationArgumentsForTrashFail( { error } ) ) + .toEqual( expectedValue ); + } ); + } ); +} ); diff --git a/packages/editor/src/store/effects/posts.js b/packages/editor/src/store/effects/posts.js deleted file mode 100644 index f9cd28fd0adbb4..00000000000000 --- a/packages/editor/src/store/effects/posts.js +++ /dev/null @@ -1,319 +0,0 @@ -/** - * External dependencies - */ -import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; -import { get, pick, includes } from 'lodash'; - -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { __ } from '@wordpress/i18n'; -// TODO: Ideally this would be the only dispatch in scope. This requires either -// refactoring editor actions to yielded controls, or replacing direct dispatch -// on the editor store with action creators (e.g. `REQUEST_POST_UPDATE_START`). -import { dispatch as dataDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { - resetAutosave, - resetPost, - updatePost, -} from '../actions'; -import { - getCurrentPost, - getPostEdits, - getEditedPostContent, - getAutosave, - getCurrentPostType, - isEditedPostSaveable, - isEditedPostNew, - POST_UPDATE_TRANSACTION_ID, -} from '../selectors'; -import { resolveSelector } from './utils'; - -/** - * Module Constants - */ -export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; -const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; - -/** - * Request Post Update Effect handler - * - * @param {Object} action the fetchReusableBlocks action object. - * @param {Object} store Redux Store. - */ -export const requestPostUpdate = async ( action, store ) => { - const { dispatch, getState } = store; - const state = getState(); - - // Prevent save if not saveable. - // We don't check for dirtiness here as this can be overridden in the UI. - if ( ! isEditedPostSaveable( state ) ) { - return; - } - - let edits = getPostEdits( state ); - const isAutosave = !! action.options.isAutosave; - if ( isAutosave ) { - edits = pick( edits, [ 'title', 'content', 'excerpt' ] ); - } - - // New posts (with auto-draft status) must be explicitly assigned draft - // status if there is not already a status assigned in edits (publish). - // Otherwise, they are wrongly left as auto-draft. Status is not always - // respected for autosaves, so it cannot simply be included in the pick - // above. This behavior relies on an assumption that an auto-draft post - // would never be saved by anyone other than the owner of the post, per - // logic within autosaves REST controller to save status field only for - // draft/auto-draft by current user. - // - // See: https://core.trac.wordpress.org/ticket/43316#comment:88 - // See: https://core.trac.wordpress.org/ticket/43316#comment:89 - if ( isEditedPostNew( state ) ) { - edits = { status: 'draft', ...edits }; - } - - const post = getCurrentPost( state ); - - let toSend = { - ...edits, - content: getEditedPostContent( state ), - id: post.id, - }; - - const postType = await resolveSelector( 'core', 'getPostType', getCurrentPostType( state ) ); - - dispatch( { - type: 'REQUEST_POST_UPDATE_START', - optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, - options: action.options, - } ); - - // Optimistically apply updates under the assumption that the post - // will be updated. See below logic in success resolution for revert - // if the autosave is applied as a revision. - dispatch( { - ...updatePost( toSend ), - optimist: { id: POST_UPDATE_TRANSACTION_ID }, - } ); - - let request; - if ( isAutosave ) { - // Ensure autosaves contain all expected fields, using autosave or - // post values as fallback if not otherwise included in edits. - toSend = { - ...pick( post, [ 'title', 'content', 'excerpt' ] ), - ...getAutosave( state ), - ...toSend, - }; - - request = apiFetch( { - path: `/wp/v2/${ postType.rest_base }/${ post.id }/autosaves`, - method: 'POST', - data: toSend, - } ); - } else { - dataDispatch( 'core/notices' ).removeNotice( SAVE_POST_NOTICE_ID ); - dataDispatch( 'core/notices' ).removeNotice( 'autosave-exists' ); - - request = apiFetch( { - path: `/wp/v2/${ postType.rest_base }/${ post.id }`, - method: 'PUT', - data: toSend, - } ); - } - - try { - const newPost = await request; - const reset = isAutosave ? resetAutosave : resetPost; - dispatch( reset( newPost ) ); - - // An autosave may be processed by the server as a regular save - // when its update is requested by the author and the post was - // draft or auto-draft. - const isRevision = newPost.id !== post.id; - - dispatch( { - type: 'REQUEST_POST_UPDATE_SUCCESS', - previousPost: post, - post: newPost, - optimist: { - // Note: REVERT is not a failure case here. Rather, it - // is simply reversing the assumption that the updates - // were applied to the post proper, such that the post - // treated as having unsaved changes. - type: isRevision ? REVERT : COMMIT, - id: POST_UPDATE_TRANSACTION_ID, - }, - options: action.options, - postType, - } ); - } catch ( error ) { - dispatch( { - type: 'REQUEST_POST_UPDATE_FAILURE', - optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, - post, - edits, - error, - options: action.options, - } ); - } -}; - -/** - * Request Post Update Success Effect handler - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const requestPostUpdateSuccess = ( action ) => { - const { previousPost, post, postType } = action; - - // Autosaves are neither shown a notice nor redirected. - if ( get( action.options, [ 'isAutosave' ] ) ) { - return; - } - - const publishStatus = [ 'publish', 'private', 'future' ]; - const isPublished = includes( publishStatus, previousPost.status ); - const willPublish = includes( publishStatus, post.status ); - - let noticeMessage; - let shouldShowLink = get( postType, [ 'viewable' ], false ); - - if ( ! isPublished && ! willPublish ) { - // If saving a non-published post, don't show notice. - noticeMessage = null; - } else if ( isPublished && ! willPublish ) { - // If undoing publish status, show specific notice - noticeMessage = postType.labels.item_reverted_to_draft; - shouldShowLink = false; - } else if ( ! isPublished && willPublish ) { - // If publishing or scheduling a post, show the corresponding - // publish message - noticeMessage = { - publish: postType.labels.item_published, - private: postType.labels.item_published_privately, - future: postType.labels.item_scheduled, - }[ post.status ]; - } else { - // Generic fallback notice - noticeMessage = postType.labels.item_updated; - } - - if ( noticeMessage ) { - const actions = []; - if ( shouldShowLink ) { - actions.push( { - label: postType.labels.view_item, - url: post.link, - } ); - } - - dataDispatch( 'core/notices' ).createSuccessNotice( - noticeMessage, - { - id: SAVE_POST_NOTICE_ID, - actions, - } - ); - } -}; - -/** - * Request Post Update Failure Effect handler - * - * @param {Object} action action object. - */ -export const requestPostUpdateFailure = ( action ) => { - const { post, edits, error } = action; - - if ( error && 'rest_autosave_no_changes' === error.code ) { - // Autosave requested a new autosave, but there were no changes. This shouldn't - // result in an error notice for the user. - return; - } - - const publishStatus = [ 'publish', 'private', 'future' ]; - const isPublished = publishStatus.indexOf( post.status ) !== -1; - // If the post was being published, we show the corresponding publish error message - // Unless we publish an "updating failed" message - const messages = { - publish: __( 'Publishing failed' ), - private: __( 'Publishing failed' ), - future: __( 'Scheduling failed' ), - }; - const noticeMessage = ! isPublished && publishStatus.indexOf( edits.status ) !== -1 ? - messages[ edits.status ] : - __( 'Updating failed' ); - - dataDispatch( 'core/notices' ).createErrorNotice( noticeMessage, { - id: SAVE_POST_NOTICE_ID, - } ); -}; - -/** - * Trash Post Effect handler - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const trashPost = async ( action, store ) => { - const { dispatch, getState } = store; - const { postId } = action; - const postTypeSlug = getCurrentPostType( getState() ); - const postType = await resolveSelector( 'core', 'getPostType', postTypeSlug ); - - dataDispatch( 'core/notices' ).removeNotice( TRASH_POST_NOTICE_ID ); - try { - await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ postId }`, method: 'DELETE' } ); - const post = getCurrentPost( getState() ); - - // TODO: This should be an updatePost action (updating subsets of post properties), - // But right now editPost is tied with change detection. - dispatch( resetPost( { ...post, status: 'trash' } ) ); - } catch ( error ) { - dispatch( { - ...action, - type: 'TRASH_POST_FAILURE', - error, - } ); - } -}; - -/** - * Trash Post Failure Effect handler - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const trashPostFailure = ( action ) => { - const message = action.error.message && action.error.code !== 'unknown_error' ? action.error.message : __( 'Trashing failed' ); - dataDispatch( 'core/notices' ).createErrorNotice( message, { - id: TRASH_POST_NOTICE_ID, - } ); -}; - -/** - * Refresh Post Effect handler - * - * @param {Object} action action object. - * @param {Object} store Redux Store. - */ -export const refreshPost = async ( action, store ) => { - const { dispatch, getState } = store; - - const state = getState(); - const post = getCurrentPost( state ); - const postTypeSlug = getCurrentPostType( getState() ); - const postType = await resolveSelector( 'core', 'getPostType', postTypeSlug ); - const newPost = await apiFetch( { - // Timestamp arg allows caller to bypass browser caching, which is expected for this specific function. - path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit&_timestamp=${ Date.now() }`, - } ); - dispatch( resetPost( newPost ) ); -}; diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index a8a9ef08177e10..6d6c6fafc83316 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -9,14 +9,16 @@ import { registerStore } from '@wordpress/data'; import reducer from './reducer'; import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; -import * as actions from './actions'; +import * as generalActions from './actions.js'; +import * as actions from './actions/index.js'; import controls from './controls'; import { MODULE_KEY } from './constants'; const store = registerStore( MODULE_KEY, { reducer, + controls, selectors, - actions, + actions: { ...actions, ...generalActions }, controls, persist: [ 'preferences' ], } ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index ec46b287c394e9..993a86d1441709 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -14,7 +14,7 @@ import { trashPost, redo, undo, -} from '../actions'; +} from '../actions.js'; describe( 'actions', () => { describe( 'setupEditor', () => { From 000828817224b081e325f2efb7d94c8d9239afdf Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Sun, 10 Feb 2019 21:58:17 -0500 Subject: [PATCH 09/32] fix broken tests --- .../store/actions/utils/test/notice-builder.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/store/actions/utils/test/notice-builder.js b/packages/editor/src/store/actions/utils/test/notice-builder.js index efc52c5947145d..e18b4c97d06d1f 100644 --- a/packages/editor/src/store/actions/utils/test/notice-builder.js +++ b/packages/editor/src/store/actions/utils/test/notice-builder.js @@ -96,7 +96,7 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { const error = { code: '42' }; const post = { status: 'publish' }; const edits = { status: 'publish' }; - const defaultExpectedAction = { id: SAVE_POST_NOTICE_ID, actions: [] }; + const defaultExpectedAction = { id: SAVE_POST_NOTICE_ID }; [ [ 'when error code is `rest_autosave_no_changes`', @@ -149,29 +149,32 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { } ); } ); describe( 'getNotificationArgumentsForTrashFail()', () => { - const defaultExpectedAction = { id: TRASH_POST_NOTICE_ID }; [ [ 'when there is an error message and the error code is not "unknown_error"', { message: 'foo', code: '' }, - [ 'foo', defaultExpectedAction ], + 'foo', ], [ 'when there is an error message and the error code is "unknown error"', { message: 'foo', code: 'unknown_error' }, - [ 'Trashing failed', defaultExpectedAction ], + 'Trashing failed', ], [ 'when there is not an error message', { code: 42 }, - [ 'Trashing failed', defaultExpectedAction ], + 'Trashing failed', ], ].forEach( ( [ description, error, - expectedValue, + message, ] ) => { it( description, () => { + const expectedValue = [ + message, + { id: TRASH_POST_NOTICE_ID }, + ]; expect( getNotificationArgumentsForTrashFail( { error } ) ) .toEqual( expectedValue ); } ); From de8e6d16b38b378ddd81614ce259c77cf8ee792c Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Mon, 11 Feb 2019 10:32:23 -0500 Subject: [PATCH 10/32] =?UTF-8?q?don=E2=80=99t=20continue=20with=20refresh?= =?UTF-8?q?Post=20if=20there=E2=80=99s=20no=20current=20post=20yet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor/src/store/actions/post-generators.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index 038132f9fd3e59..28c239e1f89413 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -252,6 +252,10 @@ export function* refreshPost() { MODULE_KEY, 'getCurrentPost' ); + if ( ! post.id ) { + // bail because there's no current post yet so nothing to refresh. + return; + } const postTypeSlug = yield select( MODULE_KEY, 'getCurrentPostType' From 64216f439de6d52d86e9fc88240b032ca3df12ce Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Mon, 11 Feb 2019 12:07:47 -0500 Subject: [PATCH 11/32] remove unnecessary bail --- packages/editor/src/store/actions/post-generators.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index 28c239e1f89413..038132f9fd3e59 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -252,10 +252,6 @@ export function* refreshPost() { MODULE_KEY, 'getCurrentPost' ); - if ( ! post.id ) { - // bail because there's no current post yet so nothing to refresh. - return; - } const postTypeSlug = yield select( MODULE_KEY, 'getCurrentPostType' From f80fae42f65b7457ed4e1dc1ebd8c551d87fb2ce Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Mon, 11 Feb 2019 14:12:27 -0500 Subject: [PATCH 12/32] add changelog entry --- packages/editor/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 8b64a350c46acd..7c9eb8c623ed92 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -18,6 +18,7 @@ - Removed `jQuery` dependency. - Removed `TinyMCE` dependency. - RichText: improve format boundaries. +- Refactor all post effects to action-generators using controls ([#13716](https://github.com/WordPress/gutenberg/pull/13716)) ## 9.0.7 (2019-01-03) From 97b1ed50ad5146bda7e1d4338f80cc41aeac86f5 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 12 Feb 2019 08:15:59 -0500 Subject: [PATCH 13/32] fix lint failure for import order --- packages/editor/src/store/actions/post-generators.js | 10 +++++----- .../editor/src/store/actions/utils/notice-builder.js | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index 038132f9fd3e59..ef09b0cad7f6f1 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { pick } from 'lodash'; - /** * Internal dependencies */ @@ -18,6 +13,11 @@ import { getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; +/** + * External dependencies + */ +import { pick } from 'lodash'; + /** * Action generator for saving the current post in the editor. * diff --git a/packages/editor/src/store/actions/utils/notice-builder.js b/packages/editor/src/store/actions/utils/notice-builder.js index 237203b561a0a3..075541b118d348 100644 --- a/packages/editor/src/store/actions/utils/notice-builder.js +++ b/packages/editor/src/store/actions/utils/notice-builder.js @@ -4,14 +4,14 @@ import { __ } from '@wordpress/i18n'; /** - * External dependencies + * Internal dependencies */ -import { get, includes } from 'lodash'; +import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; /** - * Internal dependencies + * External dependencies */ -import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; +import { get, includes } from 'lodash'; /** * Builds the arguments for a success notification dispatch. From 9bce642edf167c0dd7fe4eb08a2802c38ba7b3f2 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 12 Feb 2019 08:57:51 -0500 Subject: [PATCH 14/32] fix incorrect method used for autosave requests --- packages/editor/src/store/actions/post-generators.js | 4 +++- packages/editor/src/store/actions/test/post-generators.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index ef09b0cad7f6f1..35ac66fe6ac3e1 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -104,6 +104,7 @@ export function* savePost( options = {} ) { ); let path = `/wp/v2/${ postType.rest_base }/${ post.id }`; + let method = 'PUT'; if ( isAutosave ) { const autoSavePost = yield select( MODULE_KEY, @@ -117,6 +118,7 @@ export function* savePost( options = {} ) { ...toSend, }; path += '/autosaves'; + method = 'POST'; } else { yield dispatch( 'core/notices', @@ -133,7 +135,7 @@ export function* savePost( options = {} ) { try { const newPost = yield apiFetch( { path, - method: 'PUT', + method, data: toSend, } ); const resetAction = isAutosave ? 'resetAutosave' : 'resetPost'; diff --git a/packages/editor/src/store/actions/test/post-generators.js b/packages/editor/src/store/actions/test/post-generators.js index 4a44c6b923790c..aaa2cd3cc55cf0 100644 --- a/packages/editor/src/store/actions/test/post-generators.js +++ b/packages/editor/src/store/actions/test/post-generators.js @@ -1,5 +1,5 @@ /** - * Internal dependencies. + * Internal dependencies */ import actions, { savePost, @@ -339,7 +339,7 @@ describe( 'Post generator actions', () => { apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ editPostToSendOptimistic().id }${ path }`, - method: 'PUT', + method: isAutosave ? 'POST' : 'PUT', data, } ) From 36f962dce8846722a1370d7c0e5ae0be7fb42bcf Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 12 Feb 2019 09:25:00 -0500 Subject: [PATCH 15/32] remove another wayward period. --- packages/editor/src/store/actions/utils/test/notice-builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions/utils/test/notice-builder.js b/packages/editor/src/store/actions/utils/test/notice-builder.js index e18b4c97d06d1f..bd3b8c19ff8e71 100644 --- a/packages/editor/src/store/actions/utils/test/notice-builder.js +++ b/packages/editor/src/store/actions/utils/test/notice-builder.js @@ -1,5 +1,5 @@ /** - * Internal dependencies. + * Internal dependencies */ import { getNotificationArgumentsForSaveSuccess, From d69599fc056942cffc845466543a955615008d26 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 19 Feb 2019 09:49:52 -0500 Subject: [PATCH 16/32] Add resolveSelect control and implement --- .../src/store/actions/post-generators.js | 8 ++-- .../src/store/actions/test/post-generators.js | 14 ++++-- packages/editor/src/store/controls.js | 45 ++++++++++++++----- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js index 35ac66fe6ac3e1..58854b87ae9cad 100644 --- a/packages/editor/src/store/actions/post-generators.js +++ b/packages/editor/src/store/actions/post-generators.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { select, dispatch, apiFetch } from '../controls'; +import { select, resolveSelect, dispatch, apiFetch } from '../controls'; import { MODULE_KEY, SAVE_POST_NOTICE_ID, @@ -82,7 +82,7 @@ export function* savePost( options = {} ) { 'getCurrentPostType' ); - const postType = yield select( + const postType = yield resolveSelect( 'core', 'getPostType', currentPostType @@ -208,7 +208,7 @@ export function* trashPost() { MODULE_KEY, 'getCurrentPostType' ); - const postType = yield select( + const postType = yield resolveSelect( 'core', 'getPostType', postTypeSlug @@ -258,7 +258,7 @@ export function* refreshPost() { MODULE_KEY, 'getCurrentPostType' ); - const postType = yield select( + const postType = yield resolveSelect( 'core', 'getPostType', postTypeSlug diff --git a/packages/editor/src/store/actions/test/post-generators.js b/packages/editor/src/store/actions/test/post-generators.js index aaa2cd3cc55cf0..6b04f91380d380 100644 --- a/packages/editor/src/store/actions/test/post-generators.js +++ b/packages/editor/src/store/actions/test/post-generators.js @@ -7,7 +7,7 @@ import actions, { trashPost, refreshPost, } from '../post-generators'; -import { select, dispatch, apiFetch } from '../../controls'; +import { select, dispatch, apiFetch, resolveSelect } from '../../controls'; import { MODULE_KEY, SAVE_POST_NOTICE_ID, @@ -26,6 +26,12 @@ dispatch.mockImplementation( ( ...args ) => { return actualDispatch( ...args ); } ); +resolveSelect.mockImplementation( ( ...args ) => { + const { resolveSelect: selectResolver } = jest + .requireActual( '../../controls' ); + return selectResolver( ...args ); +} ); + const apiFetchThrowError = ( error ) => { apiFetch.mockClear(); apiFetch.mockImplementation( () => { @@ -205,7 +211,7 @@ describe( 'Post generator actions', () => { () => { const { value } = fulfillment.next( postTypeSlug ); expect( value ).toEqual( - select( 'core', 'getPostType', postTypeSlug ) + resolveSelect( 'core', 'getPostType', postTypeSlug ) ); }, ], @@ -517,7 +523,7 @@ describe( 'trashPost()', () => { } ); it( 'yields expected action for selecting the post type object', () => { const { value } = fulfillment.next( postTypeSlug ); - expect( value ).toEqual( select( + expect( value ).toEqual( resolveSelect( 'core', 'getPostType', postTypeSlug @@ -595,7 +601,7 @@ describe( 'refreshPost()', () => { } ); it( 'yields expected action for selecting the post type object', () => { const { value } = fulfillment.next( postTypeSlug ); - expect( value ).toEqual( select( + expect( value ).toEqual( resolveSelect( 'core', 'getPostType', postTypeSlug diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index 442e02c9e5b085..e19de8aee105c5 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -11,24 +11,24 @@ export function apiFetch( request ) { }; } -export function select( reducerKey, selectorName, ...args ) { +export function select( storeKey, selectorName, ...args ) { return { type: 'SELECT', - reducerKey, + storeKey, + selectorName, + args, + }; +} + +export function resolveSelect( storeKey, selectorName, ...args ) { + return { + type: 'RESOLVE_SELECT', + storeKey, selectorName, args, }; } -/** - * Dispatches an action. - * - * @param {string} storeKey Store key. - * @param {string} actionName Action name. - * @param {Array} args Action arguments. - * - * @return {Object} control descriptor. - */ export function dispatch( storeKey, actionName, ...args ) { return { type: 'DISPATCH', @@ -52,4 +52,27 @@ export default { return registry.dispatch( storeKey )[ actionName ]( ...args ); } ), + RESOLVE_SELECT: createRegistryControl( + ( registry ) => ( { storeKey, selectorName, args } ) => { + return new Promise( ( resolve ) => { + const hasFinished = () => registry.select( 'core/data' ) + .hasFinishedResolution( storeKey, selectorName, args ); + const getResult = () => registry.select( storeKey )[ selectorName ] + .apply( null, args ); + + // trigger the selector (to trigger the resolver) + const result = getResult(); + if ( hasFinished() ) { + return resolve( result ); + } + + const unsubscribe = registry.subscribe( () => { + if ( hasFinished() ) { + unsubscribe(); + resolve( getResult() ); + } + } ); + } ); + } + ), }; From 4cba06aac93e091a274cc79530c2a7beec08c8dc Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Fri, 22 Feb 2019 16:43:21 -0500 Subject: [PATCH 17/32] refactor to condense code and update tests. --- packages/editor/src/store/actions.js | 404 ++++++++-- packages/editor/src/store/actions/index.js | 2 - .../editor/src/store/actions/post-actions.js | 188 ----- .../src/store/actions/post-generators.js | 287 ------- .../src/store/actions/test/post-actions.js | 138 ---- .../src/store/actions/test/post-generators.js | 627 --------------- packages/editor/src/store/effects.js | 20 - packages/editor/src/store/index.js | 6 +- packages/editor/src/store/test/actions.js | 756 +++++++++++++++++- packages/editor/src/store/test/effects.js | 197 ----- .../{actions => }/utils/notice-builder.js | 2 +- .../utils/test/notice-builder.js | 2 +- 12 files changed, 1086 insertions(+), 1543 deletions(-) delete mode 100644 packages/editor/src/store/actions/index.js delete mode 100644 packages/editor/src/store/actions/post-actions.js delete mode 100644 packages/editor/src/store/actions/post-generators.js delete mode 100644 packages/editor/src/store/actions/test/post-actions.js delete mode 100644 packages/editor/src/store/actions/test/post-generators.js rename packages/editor/src/store/{actions => }/utils/notice-builder.js (97%) rename packages/editor/src/store/{actions => }/utils/test/notice-builder.js (99%) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index a6ccbc1829a1e3..a0525a39c85e16 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,12 +1,29 @@ /** * External dependencies */ -import { castArray } from 'lodash'; +import { castArray, pick } from 'lodash'; +import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** * Internal dependencies */ -import { dispatch } from './controls'; +import { + dispatch, + select, + resolveSelect, + apiFetch, +} from './controls'; +import { + MODULE_KEY, + POST_UPDATE_TRANSACTION_ID, + SAVE_POST_NOTICE_ID, + TRASH_POST_NOTICE_ID, +} from './constants'; +import { + getNotificationArgumentsForSaveSuccess, + getNotificationArgumentsForSaveFail, + getNotificationArgumentsForTrashFail, +} from './utils/notice-builder'; /** * Returns an action object used in signalling that editor has initialized with @@ -44,59 +61,92 @@ export function resetPost( post ) { /** * Returns an action object used in signalling that the latest autosave of the - * specified client ID has been selected, optionally accepting a position - * value reflecting its selection directionality. An initialPosition of -1 - * reflects a reverse selection. - * - * @param {string} clientId Block client ID. - * @param {?number} initialPosition Optional initial position. Pass as -1 to - * reflect reverse selection. + * post has been received, by initialization or autosave. * + * @param {Object} post Autosave post object. * @return {Object} Action object. */ -export function selectBlock( clientId, initialPosition = null ) { - return { - type: 'SELECT_BLOCK', - initialPosition, - clientId, - }; -} - -export function startMultiSelect() { - return { - type: 'START_MULTI_SELECT', - }; -} - -export function stopMultiSelect() { +export function resetAutosave( post ) { return { - type: 'STOP_MULTI_SELECT', + type: 'RESET_AUTOSAVE', + post, }; } -export function multiSelect( start, end ) { +/** + * Optimistic action for dispatching that a post update request has started. + * + * @param {Object} options + * @return {Object} An action object + */ +export function __experimentalRequestPostUpdateStart( options = {} ) { return { - type: 'MULTI_SELECT', - start, - end, + type: 'REQUEST_POST_UPDATE_START', + optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, + options, }; } -export function clearSelectedBlock() { +/** + * Optimistic action for indicating that the request post update has completed + * successfully. + * + * @param {Object} previousPost The previous post prior to update. + * @param {Object} post The new post after update + * @param {boolean} isRevision Whether the post is a revision or not. + * @param {Object} options Options passed through from the original action + * dispatch. + * @param {Object} postType The post type object. + * @return {Object} Action object. + */ +export function __experimentalRequestPostUpdateSuccess( { + previousPost, + post, + isRevision, + options, + postType, +} ) { return { - type: 'CLEAR_SELECTED_BLOCK', + type: 'REQUEST_POST_UPDATE_SUCCESS', + previousPost, + post, + optimist: { + // Note: REVERT is not a failure case here. Rather, it + // is simply reversing the assumption that the updates + // were applied to the post proper, such that the post + // treated as having unsaved changes. + type: isRevision ? REVERT : COMMIT, + id: POST_UPDATE_TRANSACTION_ID, + }, + options, + postType, }; } /** - * Returns an action object that enables or disables block selection. - * - * @return {Object} Action object. + * Optimistic action for indicating that the request post update has completed + * with a failure. + * + * @param {Object} post The post that failed updating. + * @param {Object} edits The fields that were being updated. + * @param {*} error The error from the failed call. + * @param {Object} options Options passed through from the original action + * dispatch. + * @return {Object} An action object */ -export function resetAutosave( post ) { +export function __experimentalRequestPostUpdateFailure( { + post, + edits, + error, + options, +} ) { return { - type: 'RESET_AUTOSAVE', + type: 'REQUEST_POST_UPDATE_FAILURE', + optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, post, + edits, + error, + options, }; } @@ -145,41 +195,276 @@ export function editPost( edits ) { } /** - * Returns an action object to save the post. - * - * @param {Object} options Options for the save. - * @param {boolean} options.isAutosave Perform an autosave if true. + * Returns action object produced by the updatePost creator augmented by + * an optimist option that signals optimistically applying updates. * + * @param {Object} edits Updated post fields. * @return {Object} Action object. */ -export function savePost( options = {} ) { +export function __experimentalOptimisticUpdatePost( edits ) { return { - type: 'REQUEST_POST_UPDATE', - options, + ...updatePost( edits ), + optimist: { id: POST_UPDATE_TRANSACTION_ID }, }; } -export function refreshPost() { - return { - type: 'REFRESH_POST', +/** + * Action generator for saving the current post in the editor. + * + * @param {Object} options + */ +export function* savePost( options = {} ) { + const isEditedPostSaveable = yield select( + MODULE_KEY, + 'isEditedPostSaveable' + ); + if ( ! isEditedPostSaveable ) { + return; + } + let edits = yield select( + MODULE_KEY, + 'getPostEdits' + ); + const isAutosave = !! options.isAutosave; + + if ( isAutosave ) { + edits = pick( edits, [ 'title', 'content', 'excerpt' ] ); + } + + const isEditedPostNew = yield select( + MODULE_KEY, + 'isEditedPostNew', + ); + + // New posts (with auto-draft status) must be explicitly assigned draft + // status if there is not already a status assigned in edits (publish). + // Otherwise, they are wrongly left as auto-draft. Status is not always + // respected for autosaves, so it cannot simply be included in the pick + // above. This behavior relies on an assumption that an auto-draft post + // would never be saved by anyone other than the owner of the post, per + // logic within autosaves REST controller to save status field only for + // draft/auto-draft by current user. + // + // See: https://core.trac.wordpress.org/ticket/43316#comment:88 + // See: https://core.trac.wordpress.org/ticket/43316#comment:89 + if ( isEditedPostNew ) { + edits = { status: 'draft', ...edits }; + } + + const post = yield select( + MODULE_KEY, + 'getCurrentPost' + ); + + const editedPostContent = yield select( + MODULE_KEY, + 'getEditedPostContent' + ); + + let toSend = { + ...edits, + content: editedPostContent, + id: post.id, }; + + const currentPostType = yield select( + MODULE_KEY, + 'getCurrentPostType' + ); + + const postType = yield resolveSelect( + 'core', + 'getPostType', + currentPostType + ); + + yield dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateStart', + options, + ); + + // Optimistically apply updates under the assumption that the post + // will be updated. See below logic in success resolution for revert + // if the autosave is applied as a revision. + yield dispatch( + MODULE_KEY, + '__experimentalOptimisticUpdatePost', + toSend + ); + + let path = `/wp/v2/${ postType.rest_base }/${ post.id }`; + let method = 'PUT'; + if ( isAutosave ) { + const autoSavePost = yield select( + MODULE_KEY, + 'getAutosave', + ); + // Ensure autosaves contain all expected fields, using autosave or + // post values as fallback if not otherwise included in edits. + toSend = { + ...pick( post, [ 'title', 'content', 'excerpt' ] ), + ...autoSavePost, + ...toSend, + }; + path += '/autosaves'; + method = 'POST'; + } else { + yield dispatch( + 'core/notices', + 'removeNotice', + SAVE_POST_NOTICE_ID, + ); + yield dispatch( + 'core/notices', + 'removeNotice', + 'autosave-exists', + ); + } + + try { + const newPost = yield apiFetch( { + path, + method, + data: toSend, + } ); + const resetAction = isAutosave ? 'resetAutosave' : 'resetPost'; + + yield dispatch( MODULE_KEY, resetAction, newPost ); + + yield dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateSuccess', + { + previousPost: post, + post: newPost, + options, + postType, + // An autosave may be processed by the server as a regular save + // when its update is requested by the author and the post was + // draft or auto-draft. + isRevision: newPost.id !== post.id, + } + ); + + const notifySuccessArgs = getNotificationArgumentsForSaveSuccess( { + previousPost: post, + post: newPost, + postType, + options, + } ); + if ( notifySuccessArgs.length > 0 ) { + yield dispatch( + 'core/notices', + 'createSuccessNotice', + ...notifySuccessArgs + ); + } + } catch ( error ) { + yield dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateFailure', + { post, edits, error, options } + ); + const notifyFailArgs = getNotificationArgumentsForSaveFail( { + post, + edits, + error, + } ); + if ( notifyFailArgs.length > 0 ) { + yield dispatch( + 'core/notices', + 'createErrorNotice', + ...notifyFailArgs + ); + } + } } -export function trashPost( postId, postType ) { - return { - type: 'TRASH_POST', - }; +/** + * Action generator for handling refreshing the current post. + */ +export function* refreshPost() { + const post = yield select( + MODULE_KEY, + 'getCurrentPost' + ); + const postTypeSlug = yield select( + MODULE_KEY, + 'getCurrentPostType' + ); + const postType = yield resolveSelect( + 'core', + 'getPostType', + postTypeSlug + ); + const newPost = yield apiFetch( + { + // Timestamp arg allows caller to bypass browser caching, which is + // expected for this specific function. + path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit&_timestamp=${ Date.now() }`, + } + ); + yield dispatch( + MODULE_KEY, + 'resetPost', + newPost + ); +} + +/** + * Action generator for trashing the current post in the editor. + */ +export function* trashPost() { + const postTypeSlug = yield select( + MODULE_KEY, + 'getCurrentPostType' + ); + const postType = yield resolveSelect( + 'core', + 'getPostType', + postTypeSlug + ); + yield dispatch( + 'core/notices', + 'removeNotice', + TRASH_POST_NOTICE_ID + ); + try { + const post = yield select( + MODULE_KEY, + 'getCurrentPost' + ); + yield apiFetch( + { + path: `/wp/v2/${ postType.rest_base }/${ post.id }`, + method: 'DELETE', + } + ); + + // TODO: This should be an updatePost action (updating subsets of post + // properties), but right now editPost is tied with change detection. + yield dispatch( + MODULE_KEY, + 'resetPost', + { ...post, status: 'trash' } + ); + } catch ( error ) { + yield dispatch( + 'core/notices', + 'createErrorNotice', + ...getNotificationArgumentsForTrashFail( { error } ), + ); + } } /** - * Returns an action object used in signalling that the post should autosave. + * Action generator used in signalling that the post should autosave. * * @param {Object?} options Extra flags to identify the autosave. - * - * @return {Object} Action object. */ -export function autosave( options ) { - return savePost( { isAutosave: true, ...options } ); +export function* autosave( options ) { + yield* generatorActions.savePost( { isAutosave: true, ...options } ); } /** @@ -437,3 +722,12 @@ export const exitFormattedText = getBlockEditorAction( 'exitFormattedText' ); export const insertDefaultBlock = getBlockEditorAction( 'insertDefaultBlock' ); export const updateBlockListSettings = getBlockEditorAction( 'updateBlockListSettings' ); export const updateEditorSettings = getBlockEditorAction( 'updateEditorSettings' ); + +// default export of generator actions. +const generatorActions = { + savePost, + autosave, + trashPost, + refreshPost, +}; +export default generatorActions; diff --git a/packages/editor/src/store/actions/index.js b/packages/editor/src/store/actions/index.js deleted file mode 100644 index 30000f58e19226..00000000000000 --- a/packages/editor/src/store/actions/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './post-actions'; -export * from './post-generators'; diff --git a/packages/editor/src/store/actions/post-actions.js b/packages/editor/src/store/actions/post-actions.js deleted file mode 100644 index 4dc8bf651bf8fd..00000000000000 --- a/packages/editor/src/store/actions/post-actions.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * External dependencies - */ -import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; - -/** - * Internal dependencies - */ -import { POST_UPDATE_TRANSACTION_ID } from '../constants'; - -/** - * Returns an action object used in signalling that the latest version of the - * post has been received, either by initialization or save. - * - * @param {Object} post Post object. - * - * @return {Object} Action object. - */ -export function resetPost( post ) { - return { - type: 'RESET_POST', - post, - }; -} - -/** - * Returns an action object used in signalling that the latest autosave of the - * post has been received, by initialization or autosave. - * - * @param {Object} post Autosave post object. - * - * @return {Object} Action object. - */ -export function resetAutosave( post ) { - return { - type: 'RESET_AUTOSAVE', - post, - }; -} - -/** - * Optimistic action for dispatching that a post update request has started. - * - * @param {Object} options - * @return {Object} An action object - */ -export function __experimentalRequestPostUpdateStart( options = {} ) { - return { - type: 'REQUEST_POST_UPDATE_START', - optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, - options, - }; -} - -/** - * Optimistic action for indicating that the request post update has completed - * successfully. - * - * @param {Object} previousPost The previous post prior to update. - * @param {Object} post The new post after update - * @param {boolean} isRevision Whether the post is a revision or not. - * @param {Object} options Options passed through from the original action - * dispatch. - * @param {Object} postType The post type object. - * @return {Object} Action object. - */ -export function __experimentalRequestPostUpdateSuccess( { - previousPost, - post, - isRevision, - options, - postType, -} ) { - return { - type: 'REQUEST_POST_UPDATE_SUCCESS', - previousPost, - post, - optimist: { - // Note: REVERT is not a failure case here. Rather, it - // is simply reversing the assumption that the updates - // were applied to the post proper, such that the post - // treated as having unsaved changes. - type: isRevision ? REVERT : COMMIT, - id: POST_UPDATE_TRANSACTION_ID, - }, - options, - postType, - }; -} - -/** - * Optimistic action for indicating that the request post update has completed - * with a failure. - * - * @param {Object} post The post that failed updating. - * @param {Object} edits The fields that were being updated. - * @param {*} error The error from the failed call. - * @param {Object} options Options passed through from the original action - * dispatch. - * @return {Object} An action object - */ -export function __experimentalRequestPostUpdateFailure( { - post, - edits, - error, - options, -} ) { - return { - type: 'REQUEST_POST_UPDATE_FAILURE', - optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, - post, - edits, - error, - options, - }; -} - -/** - * Returns an action object used in signalling that a patch of updates for the - * latest version of the post have been received. - * - * @param {Object} edits Updated post fields. - * - * @return {Object} Action object. - */ -export function updatePost( edits ) { - return { - type: 'UPDATE_POST', - edits, - }; -} - -/** - * Returns an action object used in signalling that attributes of the post have - * been edited. - * - * @param {Object} edits Post attributes to edit. - * - * @return {Object} Action object. - */ -export function editPost( edits ) { - return { - type: 'EDIT_POST', - edits, - }; -} - -/** - * Returns action object produced by the updatePost creator augmented by - * an optimist option that signals optimistically applying updates. - * - * @param {Object} edits Updated post fields. - * @return {Object} Action object. - */ -export function __experimentalOptimisticUpdatePost( edits ) { - return { - ...updatePost( edits ), - optimist: { id: POST_UPDATE_TRANSACTION_ID }, - }; -} - -/** - * Returns an action object used to signal that post saving is locked. - * - * @param {string} lockName The lock name. - * - * @return {Object} Action object - */ -export function lockPostSaving( lockName ) { - return { - type: 'LOCK_POST_SAVING', - lockName, - }; -} - -/** - * Returns an action object used to signal that post saving is unlocked. - * - * @param {string} lockName The lock name. - * - * @return {Object} Action object - */ -export function unlockPostSaving( lockName ) { - return { - type: 'UNLOCK_POST_SAVING', - lockName, - }; -} diff --git a/packages/editor/src/store/actions/post-generators.js b/packages/editor/src/store/actions/post-generators.js deleted file mode 100644 index 58854b87ae9cad..00000000000000 --- a/packages/editor/src/store/actions/post-generators.js +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Internal dependencies - */ -import { select, resolveSelect, dispatch, apiFetch } from '../controls'; -import { - MODULE_KEY, - SAVE_POST_NOTICE_ID, - TRASH_POST_NOTICE_ID, -} from '../constants'; -import { - getNotificationArgumentsForSaveSuccess, - getNotificationArgumentsForSaveFail, - getNotificationArgumentsForTrashFail, -} from './utils/notice-builder'; - -/** - * External dependencies - */ -import { pick } from 'lodash'; - -/** - * Action generator for saving the current post in the editor. - * - * @param {Object} options - */ -export function* savePost( options = {} ) { - const isEditedPostSaveable = yield select( - MODULE_KEY, - 'isEditedPostSaveable' - ); - if ( ! isEditedPostSaveable ) { - return; - } - let edits = yield select( - MODULE_KEY, - 'getPostEdits' - ); - const isAutosave = !! options.isAutosave; - - if ( isAutosave ) { - edits = pick( edits, [ 'title', 'content', 'excerpt' ] ); - } - - const isEditedPostNew = yield select( - MODULE_KEY, - 'isEditedPostNew', - ); - - // New posts (with auto-draft status) must be explicitly assigned draft - // status if there is not already a status assigned in edits (publish). - // Otherwise, they are wrongly left as auto-draft. Status is not always - // respected for autosaves, so it cannot simply be included in the pick - // above. This behavior relies on an assumption that an auto-draft post - // would never be saved by anyone other than the owner of the post, per - // logic within autosaves REST controller to save status field only for - // draft/auto-draft by current user. - // - // See: https://core.trac.wordpress.org/ticket/43316#comment:88 - // See: https://core.trac.wordpress.org/ticket/43316#comment:89 - if ( isEditedPostNew ) { - edits = { status: 'draft', ...edits }; - } - - const post = yield select( - MODULE_KEY, - 'getCurrentPost' - ); - - const editedPostContent = yield select( - MODULE_KEY, - 'getEditedPostContent' - ); - - let toSend = { - ...edits, - content: editedPostContent, - id: post.id, - }; - - const currentPostType = yield select( - MODULE_KEY, - 'getCurrentPostType' - ); - - const postType = yield resolveSelect( - 'core', - 'getPostType', - currentPostType - ); - - yield dispatch( - MODULE_KEY, - '__experimentalRequestPostUpdateStart', - options, - ); - - // Optimistically apply updates under the assumption that the post - // will be updated. See below logic in success resolution for revert - // if the autosave is applied as a revision. - yield dispatch( - MODULE_KEY, - '__experimentalOptimisticUpdatePost', - toSend - ); - - let path = `/wp/v2/${ postType.rest_base }/${ post.id }`; - let method = 'PUT'; - if ( isAutosave ) { - const autoSavePost = yield select( - MODULE_KEY, - 'getAutosave', - ); - // Ensure autosaves contain all expected fields, using autosave or - // post values as fallback if not otherwise included in edits. - toSend = { - ...pick( post, [ 'title', 'content', 'excerpt' ] ), - ...autoSavePost, - ...toSend, - }; - path += '/autosaves'; - method = 'POST'; - } else { - yield dispatch( - 'core/notices', - 'removeNotice', - SAVE_POST_NOTICE_ID, - ); - yield dispatch( - 'core/notices', - 'removeNotice', - 'autosave-exists', - ); - } - - try { - const newPost = yield apiFetch( { - path, - method, - data: toSend, - } ); - const resetAction = isAutosave ? 'resetAutosave' : 'resetPost'; - - yield dispatch( MODULE_KEY, resetAction, newPost ); - - yield dispatch( - MODULE_KEY, - '__experimentalRequestPostUpdateSuccess', - { - previousPost: post, - post: newPost, - options, - postType, - // An autosave may be processed by the server as a regular save - // when its update is requested by the author and the post was - // draft or auto-draft. - isRevision: newPost.id !== post.id, - } - ); - - const notifySuccessArgs = getNotificationArgumentsForSaveSuccess( { - previousPost: post, - post: newPost, - postType, - options, - } ); - if ( notifySuccessArgs.length > 0 ) { - yield dispatch( - 'core/notices', - 'createSuccessNotice', - ...notifySuccessArgs - ); - } - } catch ( error ) { - yield dispatch( - MODULE_KEY, - '__experimentalRequestPostUpdateFailure', - { post, edits, error, options } - ); - const notifyFailArgs = getNotificationArgumentsForSaveFail( { - post, - edits, - error, - } ); - if ( notifyFailArgs.length > 0 ) { - yield dispatch( - 'core/notices', - 'createErrorNotice', - ...notifyFailArgs - ); - } - } -} - -/** - * Action generator used in signalling that the post should autosave. - * - * @param {Object?} options Extra flags to identify the autosave. - */ -export function* autosave( options ) { - yield* actions.savePost( { isAutosave: true, ...options } ); -} - -/** - * Action generator for trashing the current post in the editor. - */ -export function* trashPost() { - const postTypeSlug = yield select( - MODULE_KEY, - 'getCurrentPostType' - ); - const postType = yield resolveSelect( - 'core', - 'getPostType', - postTypeSlug - ); - yield dispatch( - 'core/notices', - 'removeNotice', - TRASH_POST_NOTICE_ID - ); - try { - const post = yield select( - MODULE_KEY, - 'getCurrentPost' - ); - yield apiFetch( - { - path: `/wp/v2/${ postType.rest_base }/${ post.id }`, - method: 'DELETE', - } - ); - - // TODO: This should be an updatePost action (updating subsets of post - // properties), but right now editPost is tied with change detection. - yield dispatch( - MODULE_KEY, - 'resetPost', - { ...post, status: 'trash' } - ); - } catch ( error ) { - yield dispatch( - 'core/notices', - 'createErrorNotice', - ...getNotificationArgumentsForTrashFail( { error } ), - ); - } -} - -/** - * Action generator for handling refreshing the current post. - */ -export function* refreshPost() { - const post = yield select( - MODULE_KEY, - 'getCurrentPost' - ); - const postTypeSlug = yield select( - MODULE_KEY, - 'getCurrentPostType' - ); - const postType = yield resolveSelect( - 'core', - 'getPostType', - postTypeSlug - ); - const newPost = yield apiFetch( - { - // Timestamp arg allows caller to bypass browser caching, which is - // expected for this specific function. - path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit&_timestamp=${ Date.now() }`, - } - ); - yield dispatch( - MODULE_KEY, - 'resetPost', - newPost - ); -} - -const actions = { - savePost, - autosave, - trashPost, - refreshPost, -}; - -export default actions; diff --git a/packages/editor/src/store/actions/test/post-actions.js b/packages/editor/src/store/actions/test/post-actions.js deleted file mode 100644 index 3c8ff72205cebd..00000000000000 --- a/packages/editor/src/store/actions/test/post-actions.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * External dependencies - */ -import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; - -/** - * Internal dependencies - */ -import { - resetPost, - resetAutosave, - __experimentalRequestPostUpdateStart as requestPostUpdateStart, - __experimentalRequestPostUpdateSuccess as requestPostUpdateSuccess, - __experimentalRequestPostUpdateFailure as requestPostUpdateFailure, - updatePost, - editPost, - __experimentalOptimisticUpdatePost as optimisticUpdatePost, - lockPostSaving, - unlockPostSaving, -} from '../post-actions'; -import { POST_UPDATE_TRANSACTION_ID } from '../../constants'; - -describe( 'actions', () => { - describe( 'resetPost', () => { - it( 'should return the RESET_POST action', () => { - const post = {}; - const result = resetPost( post ); - expect( result ).toEqual( { - type: 'RESET_POST', - post, - } ); - } ); - } ); - describe( 'resetAutosave', () => { - it( 'should return the RESET_AUTOSAVE action', () => { - const post = {}; - const result = resetAutosave( post ); - expect( result ).toEqual( { - type: 'RESET_AUTOSAVE', - post, - } ); - } ); - } ); - describe( 'requestPostUpdateStart', () => { - it( 'should return the REQUEST_POST_UPDATE_START action', () => { - const result = requestPostUpdateStart(); - expect( result ).toEqual( { - type: 'REQUEST_POST_UPDATE_START', - optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, - options: {}, - } ); - } ); - } ); - describe( 'requestPostUpdateSuccess', () => { - it( 'should return the REQUEST_POST_UPDATE_SUCCESS action', () => { - const testActionData = { - previousPost: {}, - post: {}, - options: {}, - postType: 'post', - }; - const result = requestPostUpdateSuccess( { - ...testActionData, - isRevision: false, - } ); - expect( result ).toEqual( { - ...testActionData, - type: 'REQUEST_POST_UPDATE_SUCCESS', - optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, - } ); - } ); - } ); - describe( 'requestPostUpdateFailure', () => { - it( 'should return the REQUEST_POST_UPDATE_FAILURE action', () => { - const testActionData = { - post: {}, - options: {}, - edits: {}, - error: {}, - }; - const result = requestPostUpdateFailure( testActionData ); - expect( result ).toEqual( { - ...testActionData, - type: 'REQUEST_POST_UPDATE_FAILURE', - optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, - } ); - } ); - } ); - describe( 'updatePost', () => { - it( 'should return the UPDATE_POST action', () => { - const edits = {}; - const result = updatePost( edits ); - expect( result ).toEqual( { - type: 'UPDATE_POST', - edits, - } ); - } ); - } ); - describe( 'editPost', () => { - it( 'should return the EDIT_POST action', () => { - const edits = {}; - const result = editPost( edits ); - expect( result ).toEqual( { - type: 'EDIT_POST', - edits, - } ); - } ); - } ); - describe( 'optimisticUpdatePost', () => { - it( 'should return the UPDATE_POST action with optimist property', () => { - const edits = {}; - const result = optimisticUpdatePost( edits ); - expect( result ).toEqual( { - type: 'UPDATE_POST', - edits, - optimist: { id: POST_UPDATE_TRANSACTION_ID }, - } ); - } ); - } ); - describe( 'lockPostSaving', () => { - it( 'should return the LOCK_POST_SAVING action', () => { - const result = lockPostSaving( 'test' ); - expect( result ).toEqual( { - type: 'LOCK_POST_SAVING', - lockName: 'test', - } ); - } ); - } ); - describe( 'unlockPostSaving', () => { - it( 'should return the UNLOCK_POST_SAVING action', () => { - const result = unlockPostSaving( 'test' ); - expect( result ).toEqual( { - type: 'UNLOCK_POST_SAVING', - lockName: 'test', - } ); - } ); - } ); -} ); diff --git a/packages/editor/src/store/actions/test/post-generators.js b/packages/editor/src/store/actions/test/post-generators.js deleted file mode 100644 index 6b04f91380d380..00000000000000 --- a/packages/editor/src/store/actions/test/post-generators.js +++ /dev/null @@ -1,627 +0,0 @@ -/** - * Internal dependencies - */ -import actions, { - savePost, - autosave, - trashPost, - refreshPost, -} from '../post-generators'; -import { select, dispatch, apiFetch, resolveSelect } from '../../controls'; -import { - MODULE_KEY, - SAVE_POST_NOTICE_ID, - TRASH_POST_NOTICE_ID, -} from '../../constants'; - -jest.mock( '../../controls' ); - -select.mockImplementation( ( ...args ) => { - const { select: actualSelect } = jest.requireActual( '../../controls' ); - return actualSelect( ...args ); -} ); - -dispatch.mockImplementation( ( ...args ) => { - const { dispatch: actualDispatch } = jest.requireActual( '../../controls' ); - return actualDispatch( ...args ); -} ); - -resolveSelect.mockImplementation( ( ...args ) => { - const { resolveSelect: selectResolver } = jest - .requireActual( '../../controls' ); - return selectResolver( ...args ); -} ); - -const apiFetchThrowError = ( error ) => { - apiFetch.mockClear(); - apiFetch.mockImplementation( () => { - throw error; - } ); -}; - -const apiFetchDoActual = () => { - apiFetch.mockClear(); - apiFetch.mockImplementation( ( ...args ) => { - const { apiFetch: fetch } = jest.requireActual( '../../controls' ); - return fetch( ...args ); - } ); -}; - -const postType = { - rest_base: 'posts', - labels: { - item_updated: 'Updated Post', - item_published: 'Post published', - }, -}; -const postTypeSlug = 'post'; - -describe( 'Post generator actions', () => { - describe( 'savePost()', () => { - let fulfillment, - edits, - currentPost, - currentPostStatus, - editPostToSendOptimistic, - autoSavePost, - autoSavePostToSend, - savedPost, - savedPostStatus, - isAutosave, - isEditedPostNew, - savedPostMessage; - beforeEach( () => { - edits = ( defaultStatus = null ) => { - const postObject = { - title: 'foo', - content: 'bar', - excerpt: 'cheese', - foo: 'bar', - }; - if ( defaultStatus !== null ) { - postObject.status = defaultStatus; - } - return postObject; - }; - currentPost = () => ( { - id: 44, - title: 'bar', - content: 'bar', - excerpt: 'crackers', - status: currentPostStatus, - } ); - editPostToSendOptimistic = () => { - const postObject = { - ...edits(), - content: editedPostContent, - id: currentPost().id, - }; - if ( ! postObject.status && isEditedPostNew ) { - postObject.status = 'draft'; - } - if ( isAutosave ) { - delete postObject.foo; - } - return postObject; - }; - autoSavePost = { status: 'autosave', bar: 'foo' }; - autoSavePostToSend = () => ( - { - ...editPostToSendOptimistic(), - bar: 'foo', - status: 'autosave', - } - ); - savedPost = () => ( - { - ...currentPost(), - ...editPostToSendOptimistic(), - content: editedPostContent, - status: savedPostStatus, - } - ); - } ); - const editedPostContent = 'to infinity and beyond'; - const reset = ( isAutosaving ) => fulfillment = savePost( - { isAutosave: isAutosaving } - ); - const rewind = ( isAutosaving, isNewPost ) => { - reset( isAutosaving ); - fulfillment.next(); - fulfillment.next( true ); - fulfillment.next( edits() ); - fulfillment.next( isNewPost ); - fulfillment.next( currentPost() ); - fulfillment.next( editedPostContent ); - fulfillment.next( postTypeSlug ); - fulfillment.next( postType ); - fulfillment.next(); - if ( isAutosaving ) { - fulfillment.next(); - } else { - fulfillment.next(); - fulfillment.next(); - } - }; - const initialTestConditions = [ - [ - 'yields action for selecting if edited post is saveable', - () => true, - () => { - reset( isAutosave ); - const { value } = fulfillment.next(); - expect( value ).toEqual( - select( MODULE_KEY, 'isEditedPostSaveable' ) - ); - }, - ], - [ - 'yields action for selecting the post edits done', - () => true, - () => { - const { value } = fulfillment.next( true ); - expect( value ).toEqual( - select( MODULE_KEY, 'getPostEdits' ) - ); - }, - ], - [ - 'yields action for selecting whether the edited post is new', - () => true, - () => { - const { value } = fulfillment.next( edits() ); - expect( value ).toEqual( - select( MODULE_KEY, 'isEditedPostNew' ) - ); - }, - ], - [ - 'yields action for selecting the current post', - () => true, - () => { - const { value } = fulfillment.next( isEditedPostNew ); - expect( value ).toEqual( - select( MODULE_KEY, 'getCurrentPost' ) - ); - }, - ], - [ - 'yields action for selecting the edited post content', - () => true, - () => { - const { value } = fulfillment.next( currentPost() ); - expect( value ).toEqual( - select( MODULE_KEY, 'getEditedPostContent' ) - ); - }, - ], - [ - 'yields action for selecting current post type slug', - () => true, - () => { - const { value } = fulfillment.next( editedPostContent ); - expect( value ).toEqual( - select( MODULE_KEY, 'getCurrentPostType' ) - ); - }, - ], - [ - 'yields action for selecting the post type object', - () => true, - () => { - const { value } = fulfillment.next( postTypeSlug ); - expect( value ).toEqual( - resolveSelect( 'core', 'getPostType', postTypeSlug ) - ); - }, - ], - [ - 'yields action for dispatching request post update start', - () => true, - () => { - const { value } = fulfillment.next( postType ); - expect( value ).toEqual( - dispatch( - MODULE_KEY, - '__experimentalRequestPostUpdateStart', - { isAutosave } - ) - ); - }, - ], - [ - 'yields action for dispatching optimistic update of post', - () => true, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - MODULE_KEY, - '__experimentalOptimisticUpdatePost', - editPostToSendOptimistic() - ) - ); - }, - ], - [ - 'yields action for dispatching the removal of save post notice', - ( isAutosaving ) => ! isAutosaving, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - 'core/notices', - 'removeNotice', - SAVE_POST_NOTICE_ID, - ) - ); - }, - ], - [ - 'yields action for dispatching the removal of autosave notice', - ( isAutosaving ) => ! isAutosaving, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - 'core/notices', - 'removeNotice', - 'autosave-exists' - ) - ); - }, - ], - [ - 'yield action for selecting the autoSavePost', - ( isAutosaving ) => isAutosaving, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - select( - MODULE_KEY, - 'getAutosave' - ) - ); - }, - ], - ]; - const fetchErrorConditions = [ - [ - 'yields action for dispatching post update failure', - () => { - const error = { foo: 'bar', code: 'fail' }; - apiFetchThrowError( error ); - const editsObject = edits(); - const { value } = isAutosave ? - fulfillment.next( autoSavePost ) : - fulfillment.next(); - if ( isAutosave ) { - delete editsObject.foo; - } - expect( value ).toEqual( - dispatch( - MODULE_KEY, - '__experimentalRequestPostUpdateFailure', - { - post: currentPost(), - edits: isEditedPostNew ? - { ...editsObject, status: 'draft' } : - editsObject, - error, - options: { isAutosave }, - } - ) - ); - }, - ], - [ - 'yields action for dispatching an appropriate error notice', - () => { - const { value } = fulfillment.next( [ 'foo', 'bar' ] ); - expect( value ).toEqual( - dispatch( - 'core/notices', - 'createErrorNotice', - ...[ 'Updating failed', { id: 'SAVE_POST_NOTICE_ID' } ] - ) - ); - }, - ], - ]; - const fetchSuccessConditions = [ - [ - 'yields action for updating the post via the api', - () => { - apiFetchDoActual(); - rewind( isAutosave, isEditedPostNew ); - const { value } = isAutosave ? - fulfillment.next( autoSavePost ) : - fulfillment.next(); - const data = isAutosave ? - autoSavePostToSend() : - editPostToSendOptimistic(); - const path = isAutosave ? '/autosaves' : ''; - expect( value ).toEqual( - apiFetch( - { - path: `/wp/v2/${ postType.rest_base }/${ editPostToSendOptimistic().id }${ path }`, - method: isAutosave ? 'POST' : 'PUT', - data, - } - ) - ); - }, - ], - [ - 'yields action for dispatch the appropriate reset action', - () => { - const { value } = fulfillment.next( savedPost() ); - expect( value ).toEqual( - dispatch( - MODULE_KEY, - isAutosave ? 'resetAutosave' : 'resetPost', - savedPost() - ) - ); - }, - ], - [ - 'yields action for dispatching the post update success', - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - dispatch( - MODULE_KEY, - '__experimentalRequestPostUpdateSuccess', - { - previousPost: currentPost(), - post: savedPost(), - options: { isAutosave }, - postType, - isRevision: false, - } - ) - ); - }, - ], - [ - 'yields dispatch action for success notification', - () => { - const { value } = fulfillment.next( [ 'foo', 'bar' ] ); - const expected = isAutosave ? - undefined : - dispatch( - 'core/notices', - 'createSuccessNotice', - ...[ - savedPostMessage, - { actions: [], id: 'SAVE_POST_NOTICE_ID' }, - ] - ); - expect( value ).toEqual( expected ); - }, - ], - ]; - - const conditionalRunTestRoutine = ( isAutosaving ) => ( [ - testDescription, - shouldRun, - testRoutine, - ] ) => { - if ( shouldRun( isAutosaving ) ) { - it( testDescription, () => { - testRoutine(); - } ); - } - }; - - const testRunRoutine = ( [ testDescription, testRoutine ] ) => { - it( testDescription, () => { - testRoutine(); - } ); - }; - - describe( 'yields with expected responses when edited post is not ' + - 'saveable', () => { - it( 'yields action for selecting if edited post is saveable', () => { - reset( false ); - const { value } = fulfillment.next(); - expect( value ).toEqual( - select( MODULE_KEY, 'isEditedPostSaveable' ) - ); - } ); - it( 'if edited post is not saveable then bails', () => { - const { value, done } = fulfillment.next( false ); - expect( done ).toBe( true ); - expect( value ).toBeUndefined(); - } ); - } ); - describe( 'yields with expected responses for when not autosaving ' + - 'and edited post is new', () => { - beforeEach( () => { - isAutosave = false; - isEditedPostNew = true; - savedPostStatus = 'publish'; - currentPostStatus = 'draft'; - savedPostMessage = 'Post published'; - } ); - initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); - describe( 'fetch action throwing an error', () => { - fetchErrorConditions.forEach( testRunRoutine ); - } ); - describe( 'fetch action not throwing an error', () => { - fetchSuccessConditions.forEach( testRunRoutine ); - } ); - } ); - - describe( 'yields with expected responses for when not autosaving ' + - 'and edited post is not new', () => { - beforeEach( () => { - isAutosave = false; - isEditedPostNew = false; - currentPostStatus = 'publish'; - savedPostStatus = 'publish'; - savedPostMessage = 'Updated Post'; - } ); - initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); - describe( 'fetch action throwing error', () => { - fetchErrorConditions.forEach( testRunRoutine ); - } ); - describe( 'fetch action not throwing error', () => { - fetchSuccessConditions.forEach( testRunRoutine ); - } ); - } ); - describe( 'yields with expected responses for when autosaving is true ' + - 'and edited post is not new', () => { - beforeEach( () => { - isAutosave = true; - isEditedPostNew = false; - currentPostStatus = 'autosave'; - savedPostStatus = 'publish'; - savedPostMessage = 'Post published'; - } ); - initialTestConditions.forEach( conditionalRunTestRoutine( true ) ); - describe( 'fetch action throwing error', () => { - fetchErrorConditions.forEach( testRunRoutine ); - } ); - describe( 'fetch action not throwing error', () => { - fetchSuccessConditions.forEach( testRunRoutine ); - } ); - } ); - } ); -} ); -describe( 'autosave()', () => { - let savePostSpy; - beforeAll( () => savePostSpy = jest.spyOn( actions, 'savePost' ) ); - afterAll( () => savePostSpy.mockRestore() ); - // autosave is mostly covered by `savePost` tests so just test the correct call - it( 'calls savePost with the correct arguments', () => { - const fulfillment = autosave(); - fulfillment.next(); - expect( savePostSpy ).toHaveBeenCalled(); - expect( savePostSpy ).toHaveBeenCalledWith( { isAutosave: true } ); - } ); -} ); -describe( 'trashPost()', () => { - let fulfillment; - const currentPost = { id: 10, content: 'foo', status: 'publish' }; - const reset = () => fulfillment = trashPost(); - const rewind = () => { - reset(); - fulfillment.next(); - fulfillment.next( postTypeSlug ); - fulfillment.next( postType ); - fulfillment.next(); - }; - it( 'yields expected action for selecting the current post type slug', () => { - reset(); - const { value } = fulfillment.next(); - expect( value ).toEqual( select( - MODULE_KEY, - 'getCurrentPostType', - ) ); - } ); - it( 'yields expected action for selecting the post type object', () => { - const { value } = fulfillment.next( postTypeSlug ); - expect( value ).toEqual( resolveSelect( - 'core', - 'getPostType', - postTypeSlug - ) ); - } ); - it( 'yields expected action for dispatching removing the trash notice ' + - 'for the post', () => { - const { value } = fulfillment.next( postType ); - expect( value ).toEqual( dispatch( - 'core/notices', - 'removeNotice', - TRASH_POST_NOTICE_ID - ) ); - } ); - it( 'yields expected action for selecting the currentPost', () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( select( - MODULE_KEY, - 'getCurrentPost' - ) ); - } ); - describe( 'expected yields when fetch throws an error', () => { - it( 'yields expected action for dispatching an error notice', () => { - const error = { foo: 'bar', code: 'fail' }; - apiFetchThrowError( error ); - const { value } = fulfillment.next( currentPost ); - expect( value ).toEqual( dispatch( - 'core/notices', - 'createErrorNotice', - 'Trashing failed', - { id: TRASH_POST_NOTICE_ID }, - ) ); - } ); - } ); - describe( 'expected yields when fetch does not throw an error', () => { - it( 'yields expected action object for the api fetch', () => { - apiFetchDoActual(); - rewind(); - const { value } = fulfillment.next( currentPost ); - expect( value ).toEqual( apiFetch( - { - path: `/wp/v2/${ postType.rest_base }/${ currentPost.id }`, - method: 'DELETE', - } - ) ); - } ); - it( 'yields expected dispatch action for resetting the post', () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( dispatch( - MODULE_KEY, - 'resetPost', - { ...currentPost, status: 'trash' } - ) ); - } ); - } ); -} ); -describe( 'refreshPost()', () => { - let fulfillment; - const currentPost = { id: 10, content: 'foo' }; - const reset = () => fulfillment = refreshPost(); - it( 'yields expected action for selecting the currentPost', () => { - reset(); - const { value } = fulfillment.next(); - expect( value ).toEqual( select( - MODULE_KEY, - 'getCurrentPost', - ) ); - } ); - it( 'yields expected action for selecting the current post type', () => { - const { value } = fulfillment.next( currentPost ); - expect( value ).toEqual( select( - MODULE_KEY, - 'getCurrentPostType' - ) ); - } ); - it( 'yields expected action for selecting the post type object', () => { - const { value } = fulfillment.next( postTypeSlug ); - expect( value ).toEqual( resolveSelect( - 'core', - 'getPostType', - postTypeSlug - ) ); - } ); - it( 'yields expected action for the api fetch call', () => { - const { value } = fulfillment.next( postType ); - apiFetchDoActual(); - // since the timestamp is a computed value we can't do a direct comparison. - // so we'll just see if the path has most of the value. - expect( value.request.path ).toEqual( expect.stringContaining( - `/wp/v2/${ postType.rest_base }/${ currentPost.id }?context=edit&_timestamp=` - ) ); - } ); - it( 'yields expected action for dispatching the reset of the post', () => { - const { value } = fulfillment.next( currentPost ); - expect( value ).toEqual( dispatch( - MODULE_KEY, - 'resetPost', - currentPost - ) ); - } ); -} ); diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index de77fbc45aab59..84f51151137667 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -26,28 +26,8 @@ import { convertBlockToStatic, receiveReusableBlocks, } from './effects/reusable-blocks'; -import { - requestPostUpdate, - requestPostUpdateSuccess, - requestPostUpdateFailure, - trashPost, - trashPostFailure, - refreshPost, -} from './effects/posts'; export default { - REQUEST_POST_UPDATE: ( action, store ) => { - requestPostUpdate( action, store ); - }, - REQUEST_POST_UPDATE_SUCCESS: requestPostUpdateSuccess, - REQUEST_POST_UPDATE_FAILURE: requestPostUpdateFailure, - TRASH_POST: ( action, store ) => { - trashPost( action, store ); - }, - TRASH_POST_FAILURE: trashPostFailure, - REFRESH_POST: ( action, store ) => { - refreshPost( action, store ); - }, SETUP_EDITOR( action ) { const { post, edits, template } = action; diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index 6d6c6fafc83316..a8a9ef08177e10 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -9,16 +9,14 @@ import { registerStore } from '@wordpress/data'; import reducer from './reducer'; import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; -import * as generalActions from './actions.js'; -import * as actions from './actions/index.js'; +import * as actions from './actions'; import controls from './controls'; import { MODULE_KEY } from './constants'; const store = registerStore( MODULE_KEY, { reducer, - controls, selectors, - actions: { ...actions, ...generalActions }, + actions, controls, persist: [ 'preferences' ], } ); diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 993a86d1441709..3e1b39f83196c0 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -1,20 +1,656 @@ +/** + * External dependencies + */ +import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; + /** * Internal dependencies */ -import { +import generatorActions, { __experimentalFetchReusableBlocks as fetchReusableBlocks, __experimentalSaveReusableBlock as saveReusableBlock, __experimentalDeleteReusableBlock as deleteReusableBlock, __experimentalConvertBlockToStatic as convertBlockToStatic, __experimentalConvertBlockToReusable as convertBlockToReusable, + __experimentalRequestPostUpdateStart as requestPostUpdateStart, + __experimentalRequestPostUpdateSuccess as requestPostUpdateSuccess, + __experimentalRequestPostUpdateFailure as requestPostUpdateFailure, + __experimentalOptimisticUpdatePost as optimisticUpdatePost, setupEditor, resetPost, + resetAutosave, + updatePost, + lockPostSaving, + unlockPostSaving, editPost, savePost, + autosave, + refreshPost, trashPost, redo, undo, } from '../actions.js'; +import { select, dispatch, apiFetch, resolveSelect } from '../controls'; +import { + MODULE_KEY, + SAVE_POST_NOTICE_ID, + TRASH_POST_NOTICE_ID, + POST_UPDATE_TRANSACTION_ID, +} from '../constants'; + +jest.mock( '../controls' ); + +select.mockImplementation( ( ...args ) => { + const { select: actualSelect } = jest.requireActual( '../controls' ); + return actualSelect( ...args ); +} ); + +dispatch.mockImplementation( ( ...args ) => { + const { dispatch: actualDispatch } = jest.requireActual( '../controls' ); + return actualDispatch( ...args ); +} ); + +resolveSelect.mockImplementation( ( ...args ) => { + const { resolveSelect: selectResolver } = jest + .requireActual( '../controls' ); + return selectResolver( ...args ); +} ); + +const apiFetchThrowError = ( error ) => { + apiFetch.mockClear(); + apiFetch.mockImplementation( () => { + throw error; + } ); +}; + +const apiFetchDoActual = () => { + apiFetch.mockClear(); + apiFetch.mockImplementation( ( ...args ) => { + const { apiFetch: fetch } = jest.requireActual( '../controls' ); + return fetch( ...args ); + } ); +}; + +const postType = { + rest_base: 'posts', + labels: { + item_updated: 'Updated Post', + item_published: 'Post published', + }, +}; +const postTypeSlug = 'post'; + +describe( 'Post generator actions', () => { + describe( 'savePost()', () => { + let fulfillment, + edits, + currentPost, + currentPostStatus, + editPostToSendOptimistic, + autoSavePost, + autoSavePostToSend, + savedPost, + savedPostStatus, + isAutosave, + isEditedPostNew, + savedPostMessage; + beforeEach( () => { + edits = ( defaultStatus = null ) => { + const postObject = { + title: 'foo', + content: 'bar', + excerpt: 'cheese', + foo: 'bar', + }; + if ( defaultStatus !== null ) { + postObject.status = defaultStatus; + } + return postObject; + }; + currentPost = () => ( { + id: 44, + title: 'bar', + content: 'bar', + excerpt: 'crackers', + status: currentPostStatus, + } ); + editPostToSendOptimistic = () => { + const postObject = { + ...edits(), + content: editedPostContent, + id: currentPost().id, + }; + if ( ! postObject.status && isEditedPostNew ) { + postObject.status = 'draft'; + } + if ( isAutosave ) { + delete postObject.foo; + } + return postObject; + }; + autoSavePost = { status: 'autosave', bar: 'foo' }; + autoSavePostToSend = () => ( + { + ...editPostToSendOptimistic(), + bar: 'foo', + status: 'autosave', + } + ); + savedPost = () => ( + { + ...currentPost(), + ...editPostToSendOptimistic(), + content: editedPostContent, + status: savedPostStatus, + } + ); + } ); + const editedPostContent = 'to infinity and beyond'; + const reset = ( isAutosaving ) => fulfillment = savePost( + { isAutosave: isAutosaving } + ); + const rewind = ( isAutosaving, isNewPost ) => { + reset( isAutosaving ); + fulfillment.next(); + fulfillment.next( true ); + fulfillment.next( edits() ); + fulfillment.next( isNewPost ); + fulfillment.next( currentPost() ); + fulfillment.next( editedPostContent ); + fulfillment.next( postTypeSlug ); + fulfillment.next( postType ); + fulfillment.next(); + if ( isAutosaving ) { + fulfillment.next(); + } else { + fulfillment.next(); + fulfillment.next(); + } + }; + const initialTestConditions = [ + [ + 'yields action for selecting if edited post is saveable', + () => true, + () => { + reset( isAutosave ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( MODULE_KEY, 'isEditedPostSaveable' ) + ); + }, + ], + [ + 'yields action for selecting the post edits done', + () => true, + () => { + const { value } = fulfillment.next( true ); + expect( value ).toEqual( + select( MODULE_KEY, 'getPostEdits' ) + ); + }, + ], + [ + 'yields action for selecting whether the edited post is new', + () => true, + () => { + const { value } = fulfillment.next( edits() ); + expect( value ).toEqual( + select( MODULE_KEY, 'isEditedPostNew' ) + ); + }, + ], + [ + 'yields action for selecting the current post', + () => true, + () => { + const { value } = fulfillment.next( isEditedPostNew ); + expect( value ).toEqual( + select( MODULE_KEY, 'getCurrentPost' ) + ); + }, + ], + [ + 'yields action for selecting the edited post content', + () => true, + () => { + const { value } = fulfillment.next( currentPost() ); + expect( value ).toEqual( + select( MODULE_KEY, 'getEditedPostContent' ) + ); + }, + ], + [ + 'yields action for selecting current post type slug', + () => true, + () => { + const { value } = fulfillment.next( editedPostContent ); + expect( value ).toEqual( + select( MODULE_KEY, 'getCurrentPostType' ) + ); + }, + ], + [ + 'yields action for selecting the post type object', + () => true, + () => { + const { value } = fulfillment.next( postTypeSlug ); + expect( value ).toEqual( + resolveSelect( 'core', 'getPostType', postTypeSlug ) + ); + }, + ], + [ + 'yields action for dispatching request post update start', + () => true, + () => { + const { value } = fulfillment.next( postType ); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateStart', + { isAutosave } + ) + ); + }, + ], + [ + 'yields action for dispatching optimistic update of post', + () => true, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalOptimisticUpdatePost', + editPostToSendOptimistic() + ) + ); + }, + ], + [ + 'yields action for dispatching the removal of save post notice', + ( isAutosaving ) => ! isAutosaving, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'removeNotice', + SAVE_POST_NOTICE_ID, + ) + ); + }, + ], + [ + 'yields action for dispatching the removal of autosave notice', + ( isAutosaving ) => ! isAutosaving, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'removeNotice', + 'autosave-exists' + ) + ); + }, + ], + [ + 'yield action for selecting the autoSavePost', + ( isAutosaving ) => isAutosaving, + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( + MODULE_KEY, + 'getAutosave' + ) + ); + }, + ], + ]; + const fetchErrorConditions = [ + [ + 'yields action for dispatching post update failure', + () => { + const error = { foo: 'bar', code: 'fail' }; + apiFetchThrowError( error ); + const editsObject = edits(); + const { value } = isAutosave ? + fulfillment.next( autoSavePost ) : + fulfillment.next(); + if ( isAutosave ) { + delete editsObject.foo; + } + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateFailure', + { + post: currentPost(), + edits: isEditedPostNew ? + { ...editsObject, status: 'draft' } : + editsObject, + error, + options: { isAutosave }, + } + ) + ); + }, + ], + [ + 'yields action for dispatching an appropriate error notice', + () => { + const { value } = fulfillment.next( [ 'foo', 'bar' ] ); + expect( value ).toEqual( + dispatch( + 'core/notices', + 'createErrorNotice', + ...[ 'Updating failed', { id: 'SAVE_POST_NOTICE_ID' } ] + ) + ); + }, + ], + ]; + const fetchSuccessConditions = [ + [ + 'yields action for updating the post via the api', + () => { + apiFetchDoActual(); + rewind( isAutosave, isEditedPostNew ); + const { value } = isAutosave ? + fulfillment.next( autoSavePost ) : + fulfillment.next(); + const data = isAutosave ? + autoSavePostToSend() : + editPostToSendOptimistic(); + const path = isAutosave ? '/autosaves' : ''; + expect( value ).toEqual( + apiFetch( + { + path: `/wp/v2/${ postType.rest_base }/${ editPostToSendOptimistic().id }${ path }`, + method: isAutosave ? 'POST' : 'PUT', + data, + } + ) + ); + }, + ], + [ + 'yields action for dispatch the appropriate reset action', + () => { + const { value } = fulfillment.next( savedPost() ); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + isAutosave ? 'resetAutosave' : 'resetPost', + savedPost() + ) + ); + }, + ], + [ + 'yields action for dispatching the post update success', + () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( + dispatch( + MODULE_KEY, + '__experimentalRequestPostUpdateSuccess', + { + previousPost: currentPost(), + post: savedPost(), + options: { isAutosave }, + postType, + isRevision: false, + } + ) + ); + }, + ], + [ + 'yields dispatch action for success notification', + () => { + const { value } = fulfillment.next( [ 'foo', 'bar' ] ); + const expected = isAutosave ? + undefined : + dispatch( + 'core/notices', + 'createSuccessNotice', + ...[ + savedPostMessage, + { actions: [], id: 'SAVE_POST_NOTICE_ID' }, + ] + ); + expect( value ).toEqual( expected ); + }, + ], + ]; + + const conditionalRunTestRoutine = ( isAutosaving ) => ( [ + testDescription, + shouldRun, + testRoutine, + ] ) => { + if ( shouldRun( isAutosaving ) ) { + it( testDescription, () => { + testRoutine(); + } ); + } + }; + + const testRunRoutine = ( [ testDescription, testRoutine ] ) => { + it( testDescription, () => { + testRoutine(); + } ); + }; + + describe( 'yields with expected responses when edited post is not ' + + 'saveable', () => { + it( 'yields action for selecting if edited post is saveable', () => { + reset( false ); + const { value } = fulfillment.next(); + expect( value ).toEqual( + select( MODULE_KEY, 'isEditedPostSaveable' ) + ); + } ); + it( 'if edited post is not saveable then bails', () => { + const { value, done } = fulfillment.next( false ); + expect( done ).toBe( true ); + expect( value ).toBeUndefined(); + } ); + } ); + describe( 'yields with expected responses for when not autosaving ' + + 'and edited post is new', () => { + beforeEach( () => { + isAutosave = false; + isEditedPostNew = true; + savedPostStatus = 'publish'; + currentPostStatus = 'draft'; + savedPostMessage = 'Post published'; + } ); + initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); + describe( 'fetch action throwing an error', () => { + fetchErrorConditions.forEach( testRunRoutine ); + } ); + describe( 'fetch action not throwing an error', () => { + fetchSuccessConditions.forEach( testRunRoutine ); + } ); + } ); + + describe( 'yields with expected responses for when not autosaving ' + + 'and edited post is not new', () => { + beforeEach( () => { + isAutosave = false; + isEditedPostNew = false; + currentPostStatus = 'publish'; + savedPostStatus = 'publish'; + savedPostMessage = 'Updated Post'; + } ); + initialTestConditions.forEach( conditionalRunTestRoutine( false ) ); + describe( 'fetch action throwing error', () => { + fetchErrorConditions.forEach( testRunRoutine ); + } ); + describe( 'fetch action not throwing error', () => { + fetchSuccessConditions.forEach( testRunRoutine ); + } ); + } ); + describe( 'yields with expected responses for when autosaving is true ' + + 'and edited post is not new', () => { + beforeEach( () => { + isAutosave = true; + isEditedPostNew = false; + currentPostStatus = 'autosave'; + savedPostStatus = 'publish'; + savedPostMessage = 'Post published'; + } ); + initialTestConditions.forEach( conditionalRunTestRoutine( true ) ); + describe( 'fetch action throwing error', () => { + fetchErrorConditions.forEach( testRunRoutine ); + } ); + describe( 'fetch action not throwing error', () => { + fetchSuccessConditions.forEach( testRunRoutine ); + } ); + } ); + } ); + describe( 'autosave()', () => { + let savePostSpy; + beforeAll( () => savePostSpy = jest.spyOn( generatorActions, 'savePost' ) ); + afterAll( () => savePostSpy.mockRestore() ); + // autosave is mostly covered by `savePost` tests so just test the correct call + it( 'calls savePost with the correct arguments', () => { + const fulfillment = autosave(); + fulfillment.next(); + expect( savePostSpy ).toHaveBeenCalled(); + expect( savePostSpy ).toHaveBeenCalledWith( { isAutosave: true } ); + } ); + } ); + describe( 'trashPost()', () => { + let fulfillment; + const currentPost = { id: 10, content: 'foo', status: 'publish' }; + const reset = () => fulfillment = trashPost(); + const rewind = () => { + reset(); + fulfillment.next(); + fulfillment.next( postTypeSlug ); + fulfillment.next( postType ); + fulfillment.next(); + }; + it( 'yields expected action for selecting the current post type slug', + () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPostType', + ) ); + } + ); + it( 'yields expected action for selecting the post type object', () => { + const { value } = fulfillment.next( postTypeSlug ); + expect( value ).toEqual( resolveSelect( + 'core', + 'getPostType', + postTypeSlug + ) ); + } ); + it( 'yields expected action for dispatching removing the trash notice ' + + 'for the post', () => { + const { value } = fulfillment.next( postType ); + expect( value ).toEqual( dispatch( + 'core/notices', + 'removeNotice', + TRASH_POST_NOTICE_ID + ) ); + } ); + it( 'yields expected action for selecting the currentPost', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPost' + ) ); + } ); + describe( 'expected yields when fetch throws an error', () => { + it( 'yields expected action for dispatching an error notice', () => { + const error = { foo: 'bar', code: 'fail' }; + apiFetchThrowError( error ); + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( dispatch( + 'core/notices', + 'createErrorNotice', + 'Trashing failed', + { id: TRASH_POST_NOTICE_ID }, + ) ); + } ); + } ); + describe( 'expected yields when fetch does not throw an error', () => { + it( 'yields expected action object for the api fetch', () => { + apiFetchDoActual(); + rewind(); + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( apiFetch( + { + path: `/wp/v2/${ postType.rest_base }/${ currentPost.id }`, + method: 'DELETE', + } + ) ); + } ); + it( 'yields expected dispatch action for resetting the post', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( dispatch( + MODULE_KEY, + 'resetPost', + { ...currentPost, status: 'trash' } + ) ); + } ); + } ); + } ); + describe( 'refreshPost()', () => { + let fulfillment; + const currentPost = { id: 10, content: 'foo' }; + const reset = () => fulfillment = refreshPost(); + it( 'yields expected action for selecting the currentPost', () => { + reset(); + const { value } = fulfillment.next(); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPost', + ) ); + } ); + it( 'yields expected action for selecting the current post type', () => { + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( select( + MODULE_KEY, + 'getCurrentPostType' + ) ); + } ); + it( 'yields expected action for selecting the post type object', () => { + const { value } = fulfillment.next( postTypeSlug ); + expect( value ).toEqual( resolveSelect( + 'core', + 'getPostType', + postTypeSlug + ) ); + } ); + it( 'yields expected action for the api fetch call', () => { + const { value } = fulfillment.next( postType ); + apiFetchDoActual(); + // since the timestamp is a computed value we can't do a direct comparison. + // so we'll just see if the path has most of the value. + expect( value.request.path ).toEqual( expect.stringContaining( + `/wp/v2/${ postType.rest_base }/${ currentPost.id }?context=edit&_timestamp=` + ) ); + } ); + it( 'yields expected action for dispatching the reset of the post', () => { + const { value } = fulfillment.next( currentPost ); + expect( value ).toEqual( dispatch( + MODULE_KEY, + 'resetPost', + currentPost + ) ); + } ); + } ); +} ); describe( 'actions', () => { describe( 'setupEditor', () => { @@ -39,40 +675,94 @@ describe( 'actions', () => { } ); } ); - describe( 'editPost', () => { - it( 'should return EDIT_POST action', () => { - const edits = { format: 'sample' }; - expect( editPost( edits ) ).toEqual( { - type: 'EDIT_POST', - edits, + describe( 'resetAutosave', () => { + it( 'should return the RESET_AUTOSAVE action', () => { + const post = {}; + const result = resetAutosave( post ); + expect( result ).toEqual( { + type: 'RESET_AUTOSAVE', + post, } ); } ); } ); - describe( 'savePost', () => { - it( 'should return REQUEST_POST_UPDATE action', () => { - expect( savePost() ).toEqual( { - type: 'REQUEST_POST_UPDATE', + describe( 'requestPostUpdateStart', () => { + it( 'should return the REQUEST_POST_UPDATE_START action', () => { + const result = requestPostUpdateStart(); + expect( result ).toEqual( { + type: 'REQUEST_POST_UPDATE_START', + optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, options: {}, } ); } ); + } ); - it( 'should pass through options argument', () => { - expect( savePost( { autosave: true } ) ).toEqual( { - type: 'REQUEST_POST_UPDATE', - options: { autosave: true }, + describe( 'requestPostUpdateSuccess', () => { + it( 'should return the REQUEST_POST_UPDATE_SUCCESS action', () => { + const testActionData = { + previousPost: {}, + post: {}, + options: {}, + postType: 'post', + }; + const result = requestPostUpdateSuccess( { + ...testActionData, + isRevision: false, + } ); + expect( result ).toEqual( { + ...testActionData, + type: 'REQUEST_POST_UPDATE_SUCCESS', + optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID }, } ); } ); } ); - describe( 'trashPost', () => { - it( 'should return TRASH_POST action', () => { - const postId = 1; - const postType = 'post'; - expect( trashPost( postId, postType ) ).toEqual( { - type: 'TRASH_POST', - postId, - postType, + describe( 'requestPostUpdateFailure', () => { + it( 'should return the REQUEST_POST_UPDATE_FAILURE action', () => { + const testActionData = { + post: {}, + options: {}, + edits: {}, + error: {}, + }; + const result = requestPostUpdateFailure( testActionData ); + expect( result ).toEqual( { + ...testActionData, + type: 'REQUEST_POST_UPDATE_FAILURE', + optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID }, + } ); + } ); + } ); + + describe( 'updatePost', () => { + it( 'should return the UPDATE_POST action', () => { + const edits = {}; + const result = updatePost( edits ); + expect( result ).toEqual( { + type: 'UPDATE_POST', + edits, + } ); + } ); + } ); + + describe( 'editPost', () => { + it( 'should return EDIT_POST action', () => { + const edits = { format: 'sample' }; + expect( editPost( edits ) ).toEqual( { + type: 'EDIT_POST', + edits, + } ); + } ); + } ); + + describe( 'optimisticUpdatePost', () => { + it( 'should return the UPDATE_POST action with optimist property', () => { + const edits = {}; + const result = optimisticUpdatePost( edits ); + expect( result ).toEqual( { + type: 'UPDATE_POST', + edits, + optimist: { id: POST_UPDATE_TRANSACTION_ID }, } ); } ); } ); @@ -145,4 +835,24 @@ describe( 'actions', () => { } ); } ); } ); + + describe( 'lockPostSaving', () => { + it( 'should return the LOCK_POST_SAVING action', () => { + const result = lockPostSaving( 'test' ); + expect( result ).toEqual( { + type: 'LOCK_POST_SAVING', + lockName: 'test', + } ); + } ); + } ); + + describe( 'unlockPostSaving', () => { + it( 'should return the UNLOCK_POST_SAVING action', () => { + const result = unlockPostSaving( 'test' ); + expect( result ).toEqual( { + type: 'UNLOCK_POST_SAVING', + lockName: 'test', + } ); + } ); + } ); } ); diff --git a/packages/editor/src/store/test/effects.js b/packages/editor/src/store/test/effects.js index 2e9170d2175a0a..cdc7223858e0fe 100644 --- a/packages/editor/src/store/test/effects.js +++ b/packages/editor/src/store/test/effects.js @@ -6,214 +6,17 @@ import { unregisterBlockType, registerBlockType, } from '@wordpress/blocks'; -import { dispatch as dataDispatch } from '@wordpress/data'; /** * Internal dependencies */ import { setupEditorState, resetEditorBlocks } from '../actions'; import effects from '../effects'; -import { SAVE_POST_NOTICE_ID } from '../effects/posts'; import '../../'; describe( 'effects', () => { - beforeAll( () => { - jest.spyOn( dataDispatch( 'core/notices' ), 'createErrorNotice' ); - jest.spyOn( dataDispatch( 'core/notices' ), 'createSuccessNotice' ); - } ); - - beforeEach( () => { - dataDispatch( 'core/notices' ).createErrorNotice.mockReset(); - dataDispatch( 'core/notices' ).createSuccessNotice.mockReset(); - } ); - const defaultBlockSettings = { save: () => 'Saved', category: 'common', title: 'block title' }; - describe( '.REQUEST_POST_UPDATE_SUCCESS', () => { - const handler = effects.REQUEST_POST_UPDATE_SUCCESS; - - const defaultPost = { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: '', - }, - }; - const getDraftPost = () => ( { - ...defaultPost, - status: 'draft', - } ); - const getPublishedPost = () => ( { - ...defaultPost, - status: 'publish', - } ); - const getPostType = () => ( { - labels: { - view_item: 'View post', - item_published: 'Post published.', - item_reverted_to_draft: 'Post reverted to draft.', - item_updated: 'Post updated.', - }, - viewable: true, - } ); - - it( 'should dispatch notices when publishing or scheduling a post', () => { - const previousPost = getDraftPost(); - const post = getPublishedPost(); - const postType = getPostType(); - - handler( { post, previousPost, postType } ); - - expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( - 'Post published.', - { - id: SAVE_POST_NOTICE_ID, - actions: [ - { label: 'View post', url: undefined }, - ], - } - ); - } ); - - it( 'should dispatch notices when publishing or scheduling an unviewable post', () => { - const previousPost = getDraftPost(); - const post = getPublishedPost(); - const postType = { ...getPostType(), viewable: false }; - - handler( { post, previousPost, postType } ); - - expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( - 'Post published.', - { - id: SAVE_POST_NOTICE_ID, - actions: [], - } - ); - } ); - - it( 'should dispatch notices when reverting a published post to a draft', () => { - const previousPost = getPublishedPost(); - const post = getDraftPost(); - const postType = getPostType(); - - handler( { post, previousPost, postType } ); - - expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( - 'Post reverted to draft.', - { - id: SAVE_POST_NOTICE_ID, - actions: [], - } - ); - } ); - - it( 'should dispatch notices when just updating a published post again', () => { - const previousPost = getPublishedPost(); - const post = getPublishedPost(); - const postType = getPostType(); - - handler( { post, previousPost, postType } ); - - expect( dataDispatch( 'core/notices' ).createSuccessNotice ).toHaveBeenCalledWith( - 'Post updated.', - { - id: SAVE_POST_NOTICE_ID, - actions: [ - { label: 'View post', url: undefined }, - ], - } - ); - } ); - - it( 'should do nothing if the updated post was autosaved', () => { - const previousPost = getPublishedPost(); - const post = { ...getPublishedPost(), id: defaultPost.id + 1 }; - - handler( { post, previousPost, options: { isAutosave: true } } ); - - expect( dataDispatch( 'core/notices' ).createSuccessNotice ).not.toHaveBeenCalled(); - } ); - } ); - - describe( '.REQUEST_POST_UPDATE_FAILURE', () => { - it( 'should dispatch a notice on failure when publishing a draft fails.', () => { - const handler = effects.REQUEST_POST_UPDATE_FAILURE; - - const action = { - post: { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: '', - }, - status: 'draft', - }, - edits: { - status: 'publish', - }, - }; - - handler( action ); - - expect( dataDispatch( 'core/notices' ).createErrorNotice ).toHaveBeenCalledWith( 'Publishing failed', { id: SAVE_POST_NOTICE_ID } ); - } ); - - it( 'should not dispatch a notice when there were no changes for autosave to save.', () => { - const handler = effects.REQUEST_POST_UPDATE_FAILURE; - - const action = { - post: { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: '', - }, - status: 'draft', - }, - edits: { - status: 'publish', - }, - error: { - code: 'rest_autosave_no_changes', - }, - }; - - handler( action ); - - expect( dataDispatch( 'core/notices' ).createErrorNotice ).not.toHaveBeenCalled(); - } ); - - it( 'should dispatch a notice on failure when trying to update a draft.', () => { - const handler = effects.REQUEST_POST_UPDATE_FAILURE; - - const action = { - post: { - id: 1, - title: { - raw: 'A History of Pork', - }, - content: { - raw: '', - }, - status: 'draft', - }, - edits: { - status: 'draft', - }, - }; - - handler( action ); - - expect( dataDispatch( 'core/notices' ).createErrorNotice ).toHaveBeenCalledWith( 'Updating failed', { id: SAVE_POST_NOTICE_ID } ); - } ); - } ); - describe( '.SETUP_EDITOR', () => { const handler = effects.SETUP_EDITOR; diff --git a/packages/editor/src/store/actions/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js similarity index 97% rename from packages/editor/src/store/actions/utils/notice-builder.js rename to packages/editor/src/store/utils/notice-builder.js index 075541b118d348..0919188ed1a1a1 100644 --- a/packages/editor/src/store/actions/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; +import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../constants'; /** * External dependencies diff --git a/packages/editor/src/store/actions/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js similarity index 99% rename from packages/editor/src/store/actions/utils/test/notice-builder.js rename to packages/editor/src/store/utils/test/notice-builder.js index bd3b8c19ff8e71..a78d03f81fad79 100644 --- a/packages/editor/src/store/actions/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -9,7 +9,7 @@ import { import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, -} from '../../../constants'; +} from '../../constants'; describe( 'getNotificationArgumentsForSaveSuccess()', () => { const postType = { From b3beb7fccbb2fdea3ff33ff5259385f09cb78d03 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Fri, 22 Feb 2019 16:53:15 -0500 Subject: [PATCH 18/32] update docs --- .../developers/data/data-core-editor.md | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 7e95832dc5bdbe..e5bfcf0d0f75f4 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -749,6 +749,41 @@ post has been received, by initialization or autosave. * post: Autosave post object. +### __experimentalRequestPostUpdateStart + +Optimistic action for dispatching that a post update request has started. + +*Parameters* + + * options: null + +### __experimentalRequestPostUpdateSuccess + +Optimistic action for indicating that the request post update has completed +successfully. + +*Parameters* + + * previousPost: The previous post prior to update. + * post: The new post after update + * isRevision: Whether the post is a revision or not. + * options: Options passed through from the original action +dispatch. + * postType: The post type object. + +### __experimentalRequestPostUpdateFailure + +Optimistic action for indicating that the request post update has completed +with a failure. + +*Parameters* + + * post: The post that failed updating. + * edits: The fields that were being updated. + * error: The error from the failed call. + * options: Options passed through from the original action +dispatch. + ### updatePost Returns an action object used in signalling that a patch of updates for the @@ -775,18 +810,34 @@ been edited. * edits: Post attributes to edit. +### __experimentalOptimisticUpdatePost + +Returns action object produced by the updatePost creator augmented by +an optimist option that signals optimistically applying updates. + +*Parameters* + + * edits: Updated post fields. + ### savePost -Returns an action object to save the post. +Action generator for saving the current post in the editor. *Parameters* - * options: Options for the save. - * options.isAutosave: Perform an autosave if true. + * options: null + +### refreshPost + +Action generator for handling refreshing the current post. + +### trashPost + +Action generator for trashing the current post in the editor. ### autosave -Returns an action object used in signalling that the post should autosave. +Action generator used in signalling that the post should autosave. *Parameters* From 651216d96b3f3f98a47bd6e45f0b15d41617bc73 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 07:10:56 -0500 Subject: [PATCH 19/32] fix doc blocks --- packages/editor/src/store/actions.js | 44 ++++++++++++------- .../editor/src/store/utils/notice-builder.js | 11 +++-- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index a0525a39c85e16..fae65c92e675fc 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -64,6 +64,7 @@ export function resetPost( post ) { * post has been received, by initialization or autosave. * * @param {Object} post Autosave post object. + * * @return {Object} Action object. */ export function resetAutosave( post ) { @@ -77,6 +78,7 @@ export function resetAutosave( post ) { * Optimistic action for dispatching that a post update request has started. * * @param {Object} options + * * @return {Object} An action object */ export function __experimentalRequestPostUpdateStart( options = {} ) { @@ -91,12 +93,13 @@ export function __experimentalRequestPostUpdateStart( options = {} ) { * Optimistic action for indicating that the request post update has completed * successfully. * - * @param {Object} previousPost The previous post prior to update. - * @param {Object} post The new post after update - * @param {boolean} isRevision Whether the post is a revision or not. - * @param {Object} options Options passed through from the original action - * dispatch. - * @param {Object} postType The post type object. + * @param {Object} previousPost The previous post prior to update. + * @param {Object} post The new post after update + * @param {boolean} isRevision Whether the post is a revision or not. + * @param {Object} options Options passed through from the original + * action dispatch. + * @param {Object} postType The post type object. + * * @return {Object} Action object. */ export function __experimentalRequestPostUpdateSuccess( { @@ -127,11 +130,11 @@ export function __experimentalRequestPostUpdateSuccess( { * Optimistic action for indicating that the request post update has completed * with a failure. * - * @param {Object} post The post that failed updating. - * @param {Object} edits The fields that were being updated. - * @param {*} error The error from the failed call. - * @param {Object} options Options passed through from the original action - * dispatch. + * @param {Object} post The post that failed updating. + * @param {Object} edits The fields that were being updated. + * @param {*} error The error from the failed call. + * @param {Object} options Options passed through from the original action + * dispatch. * @return {Object} An action object */ export function __experimentalRequestPostUpdateFailure( { @@ -166,7 +169,8 @@ export function updatePost( edits ) { } /** - * Returns an action object used to setup the editor state when first opening an editor. + * Returns an action object used to setup the editor state when first opening + * an editor. * * @param {Object} post Post object. * @@ -199,6 +203,7 @@ export function editPost( edits ) { * an optimist option that signals optimistically applying updates. * * @param {Object} edits Updated post fields. + * * @return {Object} Action object. */ export function __experimentalOptimisticUpdatePost( edits ) { @@ -402,7 +407,8 @@ export function* refreshPost() { { // Timestamp arg allows caller to bypass browser caching, which is // expected for this specific function. - path: `/wp/v2/${ postType.rest_base }/${ post.id }?context=edit&_timestamp=${ Date.now() }`, + path: `/wp/v2/${ postType.rest_base }/${ post.id }` + + `?context=edit&_timestamp=${ Date.now() }`, } ); yield dispatch( @@ -590,7 +596,8 @@ export function __experimentalUpdateReusableBlockTitle( id, title ) { } /** - * Returns an action object used to convert a reusable block into a static block. + * Returns an action object used to convert a reusable block into a static + * block. * * @param {string} clientId The client ID of the block to attach. * @@ -604,7 +611,8 @@ export function __experimentalConvertBlockToStatic( clientId ) { } /** - * Returns an action object used to convert a static block into a reusable block. + * Returns an action object used to convert a static block into a reusable + * block. * * @param {string} clientIds The client IDs of the block to detach. * @@ -618,7 +626,8 @@ export function __experimentalConvertBlockToReusable( clientIds ) { } /** - * Returns an action object used in signalling that the user has enabled the publish sidebar. + * Returns an action object used in signalling that the user has enabled the + * publish sidebar. * * @return {Object} Action object */ @@ -629,7 +638,8 @@ export function enablePublishSidebar() { } /** - * Returns an action object used in signalling that the user has disabled the publish sidebar. + * Returns an action object used in signalling that the user has disabled the + * publish sidebar. * * @return {Object} Action object */ diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 0919188ed1a1a1..71caf693a2f60a 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -17,8 +17,9 @@ import { get, includes } from 'lodash'; * Builds the arguments for a success notification dispatch. * * @param {Object} data Incoming data to build the arguments from. - * @return {Array} Arguments for dispatch. An empty array signals no - * notification should be sent. + * + * @return {Array} Arguments for dispatch. An empty array signals no + * notification should be sent. */ export function getNotificationArgumentsForSaveSuccess( data ) { const { previousPost, post, postType } = data; @@ -77,8 +78,9 @@ export function getNotificationArgumentsForSaveSuccess( data ) { * Builds the fail notification arguments for dispatch. * * @param {Object} data Incoming data to build the arguments with. + * * @return {Array} Arguments for dispatch. An empty array signals no - * notification should be sent. + * notification should be sent. */ export function getNotificationArgumentsForSaveFail( data ) { const { post, edits, error } = data; @@ -105,9 +107,10 @@ export function getNotificationArgumentsForSaveFail( data ) { } /** - * Builds the trash fail notifiation arguments for dispatch. + * Builds the trash fail notification arguments for dispatch. * * @param {Object} data + * * @return {Array} Arguments for dispatch. */ export function getNotificationArgumentsForTrashFail( data ) { From a455781366fb186c07c0a2098c5915e3d1501e47 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 07:52:23 -0500 Subject: [PATCH 20/32] fix jsdocs for param object properties --- packages/editor/src/store/actions.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index fae65c92e675fc..ff5f49534bc18c 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -92,13 +92,13 @@ export function __experimentalRequestPostUpdateStart( options = {} ) { /** * Optimistic action for indicating that the request post update has completed * successfully. - * - * @param {Object} previousPost The previous post prior to update. - * @param {Object} post The new post after update - * @param {boolean} isRevision Whether the post is a revision or not. - * @param {Object} options Options passed through from the original - * action dispatch. - * @param {Object} postType The post type object. + * @param {Object} data The data for the action. + * @param {Object} data.previousPost The previous post prior to update. + * @param {Object} data.post The new post after update + * @param {boolean} data.isRevision Whether the post is a revision or not. + * @param {Object} data.options Options passed through from the original + * action dispatch. + * @param {Object} data.postType The post type object. * * @return {Object} Action object. */ @@ -130,11 +130,12 @@ export function __experimentalRequestPostUpdateSuccess( { * Optimistic action for indicating that the request post update has completed * with a failure. * - * @param {Object} post The post that failed updating. - * @param {Object} edits The fields that were being updated. - * @param {*} error The error from the failed call. - * @param {Object} options Options passed through from the original action - * dispatch. + * @param {Object} data The data for the action + * @param {Object} data.post The post that failed updating. + * @param {Object} data.edits The fields that were being updated. + * @param {*} data.error The error from the failed call. + * @param {Object} data.options Options passed through from the original + * action dispatch. * @return {Object} An action object */ export function __experimentalRequestPostUpdateFailure( { From 1a4fc0cf1538597a643d21aef6b42fec9711502b Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 07:54:22 -0500 Subject: [PATCH 21/32] simplify import statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - this also initially experimented with just mocking `actions.savePost` for the autosave test but that didn’t work. Needed to revert to the usage of a generatorActions default export for that. --- packages/editor/src/store/test/actions.js | 73 ++++++++--------------- 1 file changed, 26 insertions(+), 47 deletions(-) diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 3e1b39f83196c0..4056a9fd4230ed 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -6,30 +6,7 @@ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** * Internal dependencies */ -import generatorActions, { - __experimentalFetchReusableBlocks as fetchReusableBlocks, - __experimentalSaveReusableBlock as saveReusableBlock, - __experimentalDeleteReusableBlock as deleteReusableBlock, - __experimentalConvertBlockToStatic as convertBlockToStatic, - __experimentalConvertBlockToReusable as convertBlockToReusable, - __experimentalRequestPostUpdateStart as requestPostUpdateStart, - __experimentalRequestPostUpdateSuccess as requestPostUpdateSuccess, - __experimentalRequestPostUpdateFailure as requestPostUpdateFailure, - __experimentalOptimisticUpdatePost as optimisticUpdatePost, - setupEditor, - resetPost, - resetAutosave, - updatePost, - lockPostSaving, - unlockPostSaving, - editPost, - savePost, - autosave, - refreshPost, - trashPost, - redo, - undo, -} from '../actions.js'; +import generatorActions, * as actions from '../actions'; import { select, dispatch, apiFetch, resolveSelect } from '../controls'; import { MODULE_KEY, @@ -146,7 +123,7 @@ describe( 'Post generator actions', () => { ); } ); const editedPostContent = 'to infinity and beyond'; - const reset = ( isAutosaving ) => fulfillment = savePost( + const reset = ( isAutosaving ) => fulfillment = actions.savePost( { isAutosave: isAutosaving } ); const rewind = ( isAutosaving, isNewPost ) => { @@ -519,7 +496,7 @@ describe( 'Post generator actions', () => { afterAll( () => savePostSpy.mockRestore() ); // autosave is mostly covered by `savePost` tests so just test the correct call it( 'calls savePost with the correct arguments', () => { - const fulfillment = autosave(); + const fulfillment = actions.autosave(); fulfillment.next(); expect( savePostSpy ).toHaveBeenCalled(); expect( savePostSpy ).toHaveBeenCalledWith( { isAutosave: true } ); @@ -528,7 +505,7 @@ describe( 'Post generator actions', () => { describe( 'trashPost()', () => { let fulfillment; const currentPost = { id: 10, content: 'foo', status: 'publish' }; - const reset = () => fulfillment = trashPost(); + const reset = () => fulfillment = actions.trashPost(); const rewind = () => { reset(); fulfillment.next(); @@ -608,7 +585,7 @@ describe( 'Post generator actions', () => { describe( 'refreshPost()', () => { let fulfillment; const currentPost = { id: 10, content: 'foo' }; - const reset = () => fulfillment = refreshPost(); + const reset = () => fulfillment = actions.refreshPost(); it( 'yields expected action for selecting the currentPost', () => { reset(); const { value } = fulfillment.next(); @@ -656,7 +633,7 @@ describe( 'actions', () => { describe( 'setupEditor', () => { it( 'should return the SETUP_EDITOR action', () => { const post = {}; - const result = setupEditor( post ); + const result = actions.setupEditor( post ); expect( result ).toEqual( { type: 'SETUP_EDITOR', post, @@ -667,7 +644,7 @@ describe( 'actions', () => { describe( 'resetPost', () => { it( 'should return the RESET_POST action', () => { const post = {}; - const result = resetPost( post ); + const result = actions.resetPost( post ); expect( result ).toEqual( { type: 'RESET_POST', post, @@ -678,7 +655,7 @@ describe( 'actions', () => { describe( 'resetAutosave', () => { it( 'should return the RESET_AUTOSAVE action', () => { const post = {}; - const result = resetAutosave( post ); + const result = actions.resetAutosave( post ); expect( result ).toEqual( { type: 'RESET_AUTOSAVE', post, @@ -688,7 +665,7 @@ describe( 'actions', () => { describe( 'requestPostUpdateStart', () => { it( 'should return the REQUEST_POST_UPDATE_START action', () => { - const result = requestPostUpdateStart(); + const result = actions.__experimentalRequestPostUpdateStart(); expect( result ).toEqual( { type: 'REQUEST_POST_UPDATE_START', optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID }, @@ -705,7 +682,7 @@ describe( 'actions', () => { options: {}, postType: 'post', }; - const result = requestPostUpdateSuccess( { + const result = actions.__experimentalRequestPostUpdateSuccess( { ...testActionData, isRevision: false, } ); @@ -725,7 +702,9 @@ describe( 'actions', () => { edits: {}, error: {}, }; - const result = requestPostUpdateFailure( testActionData ); + const result = actions.__experimentalRequestPostUpdateFailure( + testActionData + ); expect( result ).toEqual( { ...testActionData, type: 'REQUEST_POST_UPDATE_FAILURE', @@ -737,7 +716,7 @@ describe( 'actions', () => { describe( 'updatePost', () => { it( 'should return the UPDATE_POST action', () => { const edits = {}; - const result = updatePost( edits ); + const result = actions.updatePost( edits ); expect( result ).toEqual( { type: 'UPDATE_POST', edits, @@ -748,7 +727,7 @@ describe( 'actions', () => { describe( 'editPost', () => { it( 'should return EDIT_POST action', () => { const edits = { format: 'sample' }; - expect( editPost( edits ) ).toEqual( { + expect( actions.editPost( edits ) ).toEqual( { type: 'EDIT_POST', edits, } ); @@ -758,7 +737,7 @@ describe( 'actions', () => { describe( 'optimisticUpdatePost', () => { it( 'should return the UPDATE_POST action with optimist property', () => { const edits = {}; - const result = optimisticUpdatePost( edits ); + const result = actions.__experimentalOptimisticUpdatePost( edits ); expect( result ).toEqual( { type: 'UPDATE_POST', edits, @@ -769,7 +748,7 @@ describe( 'actions', () => { describe( 'redo', () => { it( 'should return REDO action', () => { - expect( redo() ).toEqual( { + expect( actions.redo() ).toEqual( { type: 'REDO', } ); } ); @@ -777,7 +756,7 @@ describe( 'actions', () => { describe( 'undo', () => { it( 'should return UNDO action', () => { - expect( undo() ).toEqual( { + expect( actions.undo() ).toEqual( { type: 'UNDO', } ); } ); @@ -785,13 +764,13 @@ describe( 'actions', () => { describe( 'fetchReusableBlocks', () => { it( 'should return the FETCH_REUSABLE_BLOCKS action', () => { - expect( fetchReusableBlocks() ).toEqual( { + expect( actions.__experimentalFetchReusableBlocks() ).toEqual( { type: 'FETCH_REUSABLE_BLOCKS', } ); } ); it( 'should take an optional id argument', () => { - expect( fetchReusableBlocks( 123 ) ).toEqual( { + expect( actions.__experimentalFetchReusableBlocks( 123 ) ).toEqual( { type: 'FETCH_REUSABLE_BLOCKS', id: 123, } ); @@ -800,7 +779,7 @@ describe( 'actions', () => { describe( 'saveReusableBlock', () => { it( 'should return the SAVE_REUSABLE_BLOCK action', () => { - expect( saveReusableBlock( 123 ) ).toEqual( { + expect( actions.__experimentalSaveReusableBlock( 123 ) ).toEqual( { type: 'SAVE_REUSABLE_BLOCK', id: 123, } ); @@ -809,7 +788,7 @@ describe( 'actions', () => { describe( 'deleteReusableBlock', () => { it( 'should return the DELETE_REUSABLE_BLOCK action', () => { - expect( deleteReusableBlock( 123 ) ).toEqual( { + expect( actions.__experimentalDeleteReusableBlock( 123 ) ).toEqual( { type: 'DELETE_REUSABLE_BLOCK', id: 123, } ); @@ -819,7 +798,7 @@ describe( 'actions', () => { describe( 'convertBlockToStatic', () => { it( 'should return the CONVERT_BLOCK_TO_STATIC action', () => { const clientId = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( convertBlockToStatic( clientId ) ).toEqual( { + expect( actions.__experimentalConvertBlockToStatic( clientId ) ).toEqual( { type: 'CONVERT_BLOCK_TO_STATIC', clientId, } ); @@ -829,7 +808,7 @@ describe( 'actions', () => { describe( 'convertBlockToReusable', () => { it( 'should return the CONVERT_BLOCK_TO_REUSABLE action', () => { const clientId = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; - expect( convertBlockToReusable( clientId ) ).toEqual( { + expect( actions.__experimentalConvertBlockToReusable( clientId ) ).toEqual( { type: 'CONVERT_BLOCK_TO_REUSABLE', clientIds: [ clientId ], } ); @@ -838,7 +817,7 @@ describe( 'actions', () => { describe( 'lockPostSaving', () => { it( 'should return the LOCK_POST_SAVING action', () => { - const result = lockPostSaving( 'test' ); + const result = actions.lockPostSaving( 'test' ); expect( result ).toEqual( { type: 'LOCK_POST_SAVING', lockName: 'test', @@ -848,7 +827,7 @@ describe( 'actions', () => { describe( 'unlockPostSaving', () => { it( 'should return the UNLOCK_POST_SAVING action', () => { - const result = unlockPostSaving( 'test' ); + const result = actions.unlockPostSaving( 'test' ); expect( result ).toEqual( { type: 'UNLOCK_POST_SAVING', lockName: 'test', From 49f5af6e3aa5a7982868a089f34025c8fec70526 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 08:01:16 -0500 Subject: [PATCH 22/32] utilize constants from constants file --- packages/editor/src/store/selectors.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index c3147abda70631..0930f128f4a561 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -27,18 +27,12 @@ import { createRegistrySelector } from '@wordpress/data'; * Internal dependencies */ import { PREFERENCES_DEFAULTS } from './defaults'; -import { EDIT_MERGE_PROPERTIES } from './constants'; - -/*** - * Module constants - */ -export const POST_UPDATE_TRANSACTION_ID = 'post-update'; -const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; -export const INSERTER_UTILITY_HIGH = 3; -export const INSERTER_UTILITY_MEDIUM = 2; -export const INSERTER_UTILITY_LOW = 1; -export const INSERTER_UTILITY_NONE = 0; -const ONE_MINUTE_IN_MS = 60 * 1000; +import { + EDIT_MERGE_PROPERTIES, + POST_UPDATE_TRANSACTION_ID, + PERMALINK_POSTNAME_REGEX, + ONE_MINUTE_IN_MS, +} from './constants'; /** * Shared reference to an empty object for cases where it is important to avoid From a41039ce583729c21a16b60aded296609227455b Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 08:01:38 -0500 Subject: [PATCH 23/32] remove unused constants (now in block-editor) --- packages/editor/src/store/constants.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index 6872def82c08a8..420becc6c14b39 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -18,11 +18,4 @@ export const POST_UPDATE_TRANSACTION_ID = 'post-update'; export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; export const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; -export const INSERTER_UTILITY_HIGH = 3; -export const INSERTER_UTILITY_MEDIUM = 2; -export const INSERTER_UTILITY_LOW = 1; -export const INSERTER_UTILITY_NONE = 0; -export const MILLISECONDS_PER_HOUR = 3600 * 1000; -export const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; -export const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; export const ONE_MINUTE_IN_MS = 60 * 1000; From 2c14df414c8a5da7a90965b747a0c39bd8a999ea Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 08:05:02 -0500 Subject: [PATCH 24/32] simplify import --- packages/editor/src/store/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index e19de8aee105c5..ee30633d1edd10 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { default as triggerFetch } from '@wordpress/api-fetch'; +import triggerFetch from '@wordpress/api-fetch'; import { createRegistryControl } from '@wordpress/data'; export function apiFetch( request ) { From f66af801535ab395d553344f79a195938004e27a Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 08:16:15 -0500 Subject: [PATCH 25/32] add doc blocks for control actions --- packages/editor/src/store/controls.js | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index ee30633d1edd10..8690e67dd784dd 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -4,6 +4,13 @@ import triggerFetch from '@wordpress/api-fetch'; import { createRegistryControl } from '@wordpress/data'; +/** + * Dispatches a control action for triggering an api fetch call. + * + * @param {Object} request Arguments for the fetch request. + * + * @return {Object} control descriptor. + */ export function apiFetch( request ) { return { type: 'API_FETCH', @@ -11,6 +18,15 @@ export function apiFetch( request ) { }; } +/** + * Dispatches a control action for triggering a registry select. + * + * @param {string} storeKey + * @param {string} selectorName + * @param {Array} args Arguments for the select. + * + * @return {Object} control descriptor. + */ export function select( storeKey, selectorName, ...args ) { return { type: 'SELECT', @@ -20,6 +36,16 @@ export function select( storeKey, selectorName, ...args ) { }; } +/** + * Dispatches a control action for triggering a registry select that has a + * resolver. + * + * @param {string} storeKey + * @param {string} selectorName + * @param {Array} args Arguments for the select. + * + * @return {Object} control descriptor. + */ export function resolveSelect( storeKey, selectorName, ...args ) { return { type: 'RESOLVE_SELECT', @@ -29,6 +55,15 @@ export function resolveSelect( storeKey, selectorName, ...args ) { }; } +/** + * Dispatches a control action for triggering a registry dispatch. + * + * @param {string} storeKey + * @param {string} actionName + * @param {Array} args Arguments for the dispatch action. + * + * @return {Object} control descriptor. + */ export function dispatch( storeKey, actionName, ...args ) { return { type: 'DISPATCH', From 5568ac37d404201729303acfb5465c38631d23ac Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 08:36:41 -0500 Subject: [PATCH 26/32] update docs --- .../developers/data/data-core-editor.md | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index e5bfcf0d0f75f4..9d3e1fb8476d1b 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -764,12 +764,13 @@ successfully. *Parameters* - * previousPost: The previous post prior to update. - * post: The new post after update - * isRevision: Whether the post is a revision or not. - * options: Options passed through from the original action -dispatch. - * postType: The post type object. + * data: The data for the action. + * data.previousPost: The previous post prior to update. + * data.post: The new post after update + * data.isRevision: Whether the post is a revision or not. + * data.options: Options passed through from the original + action dispatch. + * data.postType: The post type object. ### __experimentalRequestPostUpdateFailure @@ -778,11 +779,12 @@ with a failure. *Parameters* - * post: The post that failed updating. - * edits: The fields that were being updated. - * error: The error from the failed call. - * options: Options passed through from the original action -dispatch. + * data: The data for the action + * data.post: The post that failed updating. + * data.edits: The fields that were being updated. + * data.error: The error from the failed call. + * data.options: Options passed through from the original + action dispatch. ### updatePost @@ -795,7 +797,8 @@ latest version of the post have been received. ### setupEditorState -Returns an action object used to setup the editor state when first opening an editor. +Returns an action object used to setup the editor state when first opening +an editor. *Parameters* @@ -915,7 +918,8 @@ to be updated. ### __experimentalConvertBlockToStatic -Returns an action object used to convert a reusable block into a static block. +Returns an action object used to convert a reusable block into a static +block. *Parameters* @@ -923,7 +927,8 @@ Returns an action object used to convert a reusable block into a static block. ### __experimentalConvertBlockToReusable -Returns an action object used to convert a static block into a reusable block. +Returns an action object used to convert a static block into a reusable +block. *Parameters* @@ -931,11 +936,13 @@ Returns an action object used to convert a static block into a reusable block. ### enablePublishSidebar -Returns an action object used in signalling that the user has enabled the publish sidebar. +Returns an action object used in signalling that the user has enabled the +publish sidebar. ### disablePublishSidebar -Returns an action object used in signalling that the user has disabled the publish sidebar. +Returns an action object used in signalling that the user has disabled the +publish sidebar. ### lockPostSaving From 6d74643bcfea22604b41d797ad5b221a7db4c11b Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Wed, 27 Feb 2019 08:52:20 -0500 Subject: [PATCH 27/32] make sure test is importing used constant --- packages/editor/src/store/test/selectors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 459161ee038014..01f2d09199e52b 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -22,6 +22,7 @@ import { RawHTML } from '@wordpress/element'; */ import * as selectors from '../selectors'; import { PREFERENCES_DEFAULTS } from '../defaults'; +import { POST_UPDATE_TRANSACTION_ID } from '../constants'; const { hasEditorUndo, @@ -64,7 +65,6 @@ const { getStateBeforeOptimisticTransaction, isPublishingPost, isPublishSidebarEnabled, - POST_UPDATE_TRANSACTION_ID, isPermalinkEditable, getPermalink, getPermalinkParts, From ff70f2b9697a10896e76ebde23c3f2043525e2c1 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Thu, 28 Feb 2019 08:27:06 -0500 Subject: [PATCH 28/32] fix doc block spacing issues --- packages/editor/src/store/actions.js | 29 ++++++++++--------- packages/editor/src/store/controls.js | 6 ++-- packages/editor/src/store/selectors.js | 4 +-- .../editor/src/store/utils/notice-builder.js | 4 +-- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index ff5f49534bc18c..7df56df5dbcea2 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -92,15 +92,16 @@ export function __experimentalRequestPostUpdateStart( options = {} ) { /** * Optimistic action for indicating that the request post update has completed * successfully. - * @param {Object} data The data for the action. - * @param {Object} data.previousPost The previous post prior to update. - * @param {Object} data.post The new post after update - * @param {boolean} data.isRevision Whether the post is a revision or not. - * @param {Object} data.options Options passed through from the original - * action dispatch. - * @param {Object} data.postType The post type object. * - * @return {Object} Action object. + * @param {Object} data The data for the action. + * @param {Object} data.previousPost The previous post prior to update. + * @param {Object} data.post The new post after update + * @param {boolean} data.isRevision Whether the post is a revision or not. + * @param {Object} data.options Options passed through from the original + * action dispatch. + * @param {Object} data.postType The post type object. + * + * @return {Object} Action object. */ export function __experimentalRequestPostUpdateSuccess( { previousPost, @@ -130,12 +131,12 @@ export function __experimentalRequestPostUpdateSuccess( { * Optimistic action for indicating that the request post update has completed * with a failure. * - * @param {Object} data The data for the action - * @param {Object} data.post The post that failed updating. - * @param {Object} data.edits The fields that were being updated. - * @param {*} data.error The error from the failed call. - * @param {Object} data.options Options passed through from the original - * action dispatch. + * @param {Object} data The data for the action + * @param {Object} data.post The post that failed updating. + * @param {Object} data.edits The fields that were being updated. + * @param {*} data.error The error from the failed call. + * @param {Object} data.options Options passed through from the original + * action dispatch. * @return {Object} An action object */ export function __experimentalRequestPostUpdateFailure( { diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index 8690e67dd784dd..597a5f726145b5 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -40,9 +40,9 @@ export function select( storeKey, selectorName, ...args ) { * Dispatches a control action for triggering a registry select that has a * resolver. * - * @param {string} storeKey - * @param {string} selectorName - * @param {Array} args Arguments for the select. + * @param {string} storeKey + * @param {string} selectorName + * @param {Array} args Arguments for the select. * * @return {Object} control descriptor. */ diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 0930f128f4a561..c4d97cbd741baf 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -118,7 +118,7 @@ export function isEditedPostDirty( state ) { return true; } - // Edits and change detectiona are reset at the start of a save, but a post + // Edits and change detection are reset at the start of a save, but a post // is still considered dirty until the point at which the save completes. // Because the save is performed optimistically, the prior states are held // until committed. These can be referenced to determine whether there's a @@ -258,7 +258,7 @@ export function getCurrentPostAttribute( state, attributeName ) { /** * Returns a single attribute of the post being edited, preferring the unsaved - * edit if one exists, but mergiging with the attribute value for the last known + * edit if one exists, but merging with the attribute value for the last known * saved state of the post (this is needed for some nested attributes like meta). * * @param {Object} state Global application state. diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 71caf693a2f60a..4ef98c74e3a548 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -19,7 +19,7 @@ import { get, includes } from 'lodash'; * @param {Object} data Incoming data to build the arguments from. * * @return {Array} Arguments for dispatch. An empty array signals no - * notification should be sent. + * notification should be sent. */ export function getNotificationArgumentsForSaveSuccess( data ) { const { previousPost, post, postType } = data; @@ -80,7 +80,7 @@ export function getNotificationArgumentsForSaveSuccess( data ) { * @param {Object} data Incoming data to build the arguments with. * * @return {Array} Arguments for dispatch. An empty array signals no - * notification should be sent. + * notification should be sent. */ export function getNotificationArgumentsForSaveFail( data ) { const { post, edits, error } = data; From 1c11a5b9c277936e479922fbfe788f8fdfa47865 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Thu, 28 Feb 2019 08:45:30 -0500 Subject: [PATCH 29/32] Swith constant name from `MODULE_KEY` to `STORE_KEY` --- packages/editor/src/store/actions.js | 38 ++++++++++----------- packages/editor/src/store/constants.js | 2 +- packages/editor/src/store/index.js | 4 +-- packages/editor/src/store/test/actions.js | 40 +++++++++++------------ 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 7df56df5dbcea2..d02bbd35a6c176 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -14,7 +14,7 @@ import { apiFetch, } from './controls'; import { - MODULE_KEY, + STORE_KEY, POST_UPDATE_TRANSACTION_ID, SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, @@ -222,14 +222,14 @@ export function __experimentalOptimisticUpdatePost( edits ) { */ export function* savePost( options = {} ) { const isEditedPostSaveable = yield select( - MODULE_KEY, + STORE_KEY, 'isEditedPostSaveable' ); if ( ! isEditedPostSaveable ) { return; } let edits = yield select( - MODULE_KEY, + STORE_KEY, 'getPostEdits' ); const isAutosave = !! options.isAutosave; @@ -239,7 +239,7 @@ export function* savePost( options = {} ) { } const isEditedPostNew = yield select( - MODULE_KEY, + STORE_KEY, 'isEditedPostNew', ); @@ -259,12 +259,12 @@ export function* savePost( options = {} ) { } const post = yield select( - MODULE_KEY, + STORE_KEY, 'getCurrentPost' ); const editedPostContent = yield select( - MODULE_KEY, + STORE_KEY, 'getEditedPostContent' ); @@ -275,7 +275,7 @@ export function* savePost( options = {} ) { }; const currentPostType = yield select( - MODULE_KEY, + STORE_KEY, 'getCurrentPostType' ); @@ -286,7 +286,7 @@ export function* savePost( options = {} ) { ); yield dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalRequestPostUpdateStart', options, ); @@ -295,7 +295,7 @@ export function* savePost( options = {} ) { // will be updated. See below logic in success resolution for revert // if the autosave is applied as a revision. yield dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalOptimisticUpdatePost', toSend ); @@ -304,7 +304,7 @@ export function* savePost( options = {} ) { let method = 'PUT'; if ( isAutosave ) { const autoSavePost = yield select( - MODULE_KEY, + STORE_KEY, 'getAutosave', ); // Ensure autosaves contain all expected fields, using autosave or @@ -337,10 +337,10 @@ export function* savePost( options = {} ) { } ); const resetAction = isAutosave ? 'resetAutosave' : 'resetPost'; - yield dispatch( MODULE_KEY, resetAction, newPost ); + yield dispatch( STORE_KEY, resetAction, newPost ); yield dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalRequestPostUpdateSuccess', { previousPost: post, @@ -369,7 +369,7 @@ export function* savePost( options = {} ) { } } catch ( error ) { yield dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalRequestPostUpdateFailure', { post, edits, error, options } ); @@ -393,11 +393,11 @@ export function* savePost( options = {} ) { */ export function* refreshPost() { const post = yield select( - MODULE_KEY, + STORE_KEY, 'getCurrentPost' ); const postTypeSlug = yield select( - MODULE_KEY, + STORE_KEY, 'getCurrentPostType' ); const postType = yield resolveSelect( @@ -414,7 +414,7 @@ export function* refreshPost() { } ); yield dispatch( - MODULE_KEY, + STORE_KEY, 'resetPost', newPost ); @@ -425,7 +425,7 @@ export function* refreshPost() { */ export function* trashPost() { const postTypeSlug = yield select( - MODULE_KEY, + STORE_KEY, 'getCurrentPostType' ); const postType = yield resolveSelect( @@ -440,7 +440,7 @@ export function* trashPost() { ); try { const post = yield select( - MODULE_KEY, + STORE_KEY, 'getCurrentPost' ); yield apiFetch( @@ -453,7 +453,7 @@ export function* trashPost() { // TODO: This should be an updatePost action (updating subsets of post // properties), but right now editPost is tied with change detection. yield dispatch( - MODULE_KEY, + STORE_KEY, 'resetPost', { ...post, status: 'trash' } ); diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index 420becc6c14b39..8f8f1bd0afcef6 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -12,7 +12,7 @@ export const EDIT_MERGE_PROPERTIES = new Set( [ * Constant for the store module (or reducer) key. * @type {string} */ -export const MODULE_KEY = 'core/editor'; +export const STORE_KEY = 'core/editor'; export const POST_UPDATE_TRANSACTION_ID = 'post-update'; export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index a8a9ef08177e10..42af629bcce0d3 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -11,9 +11,9 @@ import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; import * as actions from './actions'; import controls from './controls'; -import { MODULE_KEY } from './constants'; +import { STORE_KEY } from './constants'; -const store = registerStore( MODULE_KEY, { +const store = registerStore( STORE_KEY, { reducer, selectors, actions, diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 4056a9fd4230ed..7f32988e87280e 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -9,7 +9,7 @@ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; import generatorActions, * as actions from '../actions'; import { select, dispatch, apiFetch, resolveSelect } from '../controls'; import { - MODULE_KEY, + STORE_KEY, SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, POST_UPDATE_TRANSACTION_ID, @@ -152,7 +152,7 @@ describe( 'Post generator actions', () => { reset( isAutosave ); const { value } = fulfillment.next(); expect( value ).toEqual( - select( MODULE_KEY, 'isEditedPostSaveable' ) + select( STORE_KEY, 'isEditedPostSaveable' ) ); }, ], @@ -162,7 +162,7 @@ describe( 'Post generator actions', () => { () => { const { value } = fulfillment.next( true ); expect( value ).toEqual( - select( MODULE_KEY, 'getPostEdits' ) + select( STORE_KEY, 'getPostEdits' ) ); }, ], @@ -172,7 +172,7 @@ describe( 'Post generator actions', () => { () => { const { value } = fulfillment.next( edits() ); expect( value ).toEqual( - select( MODULE_KEY, 'isEditedPostNew' ) + select( STORE_KEY, 'isEditedPostNew' ) ); }, ], @@ -182,7 +182,7 @@ describe( 'Post generator actions', () => { () => { const { value } = fulfillment.next( isEditedPostNew ); expect( value ).toEqual( - select( MODULE_KEY, 'getCurrentPost' ) + select( STORE_KEY, 'getCurrentPost' ) ); }, ], @@ -192,7 +192,7 @@ describe( 'Post generator actions', () => { () => { const { value } = fulfillment.next( currentPost() ); expect( value ).toEqual( - select( MODULE_KEY, 'getEditedPostContent' ) + select( STORE_KEY, 'getEditedPostContent' ) ); }, ], @@ -202,7 +202,7 @@ describe( 'Post generator actions', () => { () => { const { value } = fulfillment.next( editedPostContent ); expect( value ).toEqual( - select( MODULE_KEY, 'getCurrentPostType' ) + select( STORE_KEY, 'getCurrentPostType' ) ); }, ], @@ -223,7 +223,7 @@ describe( 'Post generator actions', () => { const { value } = fulfillment.next( postType ); expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalRequestPostUpdateStart', { isAutosave } ) @@ -237,7 +237,7 @@ describe( 'Post generator actions', () => { const { value } = fulfillment.next(); expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalOptimisticUpdatePost', editPostToSendOptimistic() ) @@ -279,7 +279,7 @@ describe( 'Post generator actions', () => { const { value } = fulfillment.next(); expect( value ).toEqual( select( - MODULE_KEY, + STORE_KEY, 'getAutosave' ) ); @@ -301,7 +301,7 @@ describe( 'Post generator actions', () => { } expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalRequestPostUpdateFailure', { post: currentPost(), @@ -359,7 +359,7 @@ describe( 'Post generator actions', () => { const { value } = fulfillment.next( savedPost() ); expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, isAutosave ? 'resetAutosave' : 'resetPost', savedPost() ) @@ -372,7 +372,7 @@ describe( 'Post generator actions', () => { const { value } = fulfillment.next(); expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, '__experimentalRequestPostUpdateSuccess', { previousPost: currentPost(), @@ -428,7 +428,7 @@ describe( 'Post generator actions', () => { reset( false ); const { value } = fulfillment.next(); expect( value ).toEqual( - select( MODULE_KEY, 'isEditedPostSaveable' ) + select( STORE_KEY, 'isEditedPostSaveable' ) ); } ); it( 'if edited post is not saveable then bails', () => { @@ -518,7 +518,7 @@ describe( 'Post generator actions', () => { reset(); const { value } = fulfillment.next(); expect( value ).toEqual( select( - MODULE_KEY, + STORE_KEY, 'getCurrentPostType', ) ); } @@ -543,7 +543,7 @@ describe( 'Post generator actions', () => { it( 'yields expected action for selecting the currentPost', () => { const { value } = fulfillment.next(); expect( value ).toEqual( select( - MODULE_KEY, + STORE_KEY, 'getCurrentPost' ) ); } ); @@ -575,7 +575,7 @@ describe( 'Post generator actions', () => { it( 'yields expected dispatch action for resetting the post', () => { const { value } = fulfillment.next(); expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, 'resetPost', { ...currentPost, status: 'trash' } ) ); @@ -590,14 +590,14 @@ describe( 'Post generator actions', () => { reset(); const { value } = fulfillment.next(); expect( value ).toEqual( select( - MODULE_KEY, + STORE_KEY, 'getCurrentPost', ) ); } ); it( 'yields expected action for selecting the current post type', () => { const { value } = fulfillment.next( currentPost ); expect( value ).toEqual( select( - MODULE_KEY, + STORE_KEY, 'getCurrentPostType' ) ); } ); @@ -621,7 +621,7 @@ describe( 'Post generator actions', () => { it( 'yields expected action for dispatching the reset of the post', () => { const { value } = fulfillment.next( currentPost ); expect( value ).toEqual( dispatch( - MODULE_KEY, + STORE_KEY, 'resetPost', currentPost ) ); From 4cc4015b16e21d896a492c954f29aefcf31db659 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Thu, 28 Feb 2019 08:45:55 -0500 Subject: [PATCH 30/32] remove use of generatorActions default import ans switch to dispatch control for autosave --- packages/editor/src/store/actions.js | 15 +++++---------- packages/editor/src/store/test/actions.js | 14 +++++--------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index d02bbd35a6c176..ac4f9f7a1185f6 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -472,7 +472,11 @@ export function* trashPost() { * @param {Object?} options Extra flags to identify the autosave. */ export function* autosave( options ) { - yield* generatorActions.savePost( { isAutosave: true, ...options } ); + yield dispatch( + STORE_KEY, + 'savePost', + { isAutosave: true, ...options } + ); } /** @@ -734,12 +738,3 @@ export const exitFormattedText = getBlockEditorAction( 'exitFormattedText' ); export const insertDefaultBlock = getBlockEditorAction( 'insertDefaultBlock' ); export const updateBlockListSettings = getBlockEditorAction( 'updateBlockListSettings' ); export const updateEditorSettings = getBlockEditorAction( 'updateEditorSettings' ); - -// default export of generator actions. -const generatorActions = { - savePost, - autosave, - trashPost, - refreshPost, -}; -export default generatorActions; diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 7f32988e87280e..296a365fe4c490 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -6,7 +6,7 @@ import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** * Internal dependencies */ -import generatorActions, * as actions from '../actions'; +import * as actions from '../actions'; import { select, dispatch, apiFetch, resolveSelect } from '../controls'; import { STORE_KEY, @@ -491,15 +491,11 @@ describe( 'Post generator actions', () => { } ); } ); describe( 'autosave()', () => { - let savePostSpy; - beforeAll( () => savePostSpy = jest.spyOn( generatorActions, 'savePost' ) ); - afterAll( () => savePostSpy.mockRestore() ); - // autosave is mostly covered by `savePost` tests so just test the correct call - it( 'calls savePost with the correct arguments', () => { + it( 'dispatches savePost with the correct arguments', () => { const fulfillment = actions.autosave(); - fulfillment.next(); - expect( savePostSpy ).toHaveBeenCalled(); - expect( savePostSpy ).toHaveBeenCalledWith( { isAutosave: true } ); + const { value } = fulfillment.next(); + expect( value.actionName ).toBe( 'savePost' ); + expect( value.args ).toEqual( [ { isAutosave: true } ] ); } ); } ); describe( 'trashPost()', () => { From 0ba7d0ed4adbbd2df449d008409aa94415cb8c84 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Thu, 28 Feb 2019 10:04:28 -0500 Subject: [PATCH 31/32] doc changes --- docs/designers-developers/developers/data/data-core-editor.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 9d3e1fb8476d1b..3a22e11c901214 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -769,7 +769,7 @@ successfully. * data.post: The new post after update * data.isRevision: Whether the post is a revision or not. * data.options: Options passed through from the original - action dispatch. + action dispatch. * data.postType: The post type object. ### __experimentalRequestPostUpdateFailure @@ -784,7 +784,7 @@ with a failure. * data.edits: The fields that were being updated. * data.error: The error from the failed call. * data.options: Options passed through from the original - action dispatch. + action dispatch. ### updatePost From c17179e6fe5a719c975277c841107f4323218b64 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 28 Feb 2019 14:07:33 -0500 Subject: [PATCH 32/32] Fix docs spacing Co-Authored-By: nerrad --- packages/editor/src/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index ac4f9f7a1185f6..f1271ed3d96d14 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -131,7 +131,7 @@ export function __experimentalRequestPostUpdateSuccess( { * Optimistic action for indicating that the request post update has completed * with a failure. * - * @param {Object} data The data for the action + * @param {Object} data The data for the action * @param {Object} data.post The post that failed updating. * @param {Object} data.edits The fields that were being updated. * @param {*} data.error The error from the failed call.