From 0bad51e52cd32c75420d9241444d0ea18109e5a4 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Mon, 14 Feb 2022 12:47:18 +0100 Subject: [PATCH] Migrate editor store to thunks (#35929) * Core data: add missing await in autosave() * Editor store: no notice after trashing a post * Migrate editor store to thunks --- .../reference-guides/data/data-core-editor.md | 27 +- package-lock.json | 1 - packages/core-data/src/actions.js | 2 +- packages/editor/package.json | 1 - .../local-autosave-monitor/index.js | 5 +- packages/editor/src/store/actions.js | 363 ++++------ packages/editor/src/store/actions.native.js | 6 +- packages/editor/src/store/index.js | 7 +- .../store/{controls.js => local-autosave.js} | 8 - packages/editor/src/store/test/actions.js | 644 +++++++----------- .../editor/src/store/utils/notice-builder.js | 5 + .../src/store/utils/test/notice-builder.js | 1 + 12 files changed, 401 insertions(+), 669 deletions(-) rename packages/editor/src/store/{controls.js => local-autosave.js} (85%) diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index c0b01321867d21..e1e212e1019d14 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -1026,7 +1026,7 @@ _Related_ ### autosave -Action generator used in signalling that the post should autosave. This +Action that autosaves the current post. This includes server-side autosaving (default) and client-side (a.k.a. local) autosaving (e.g. on the Web, the post might be committed to Session Storage). @@ -1049,8 +1049,7 @@ Action that creates an undo history record. ### disablePublishSidebar -Returns an action object used in signalling that the user has disabled the -publish sidebar. +Action that disables the publish sidebar. _Returns_ @@ -1068,8 +1067,7 @@ _Parameters_ ### enablePublishSidebar -Returns an action object used in signalling that the user has enabled the -publish sidebar. +Action that enables the publish sidebar. _Returns_ @@ -1113,7 +1111,7 @@ _Related_ ### lockPostAutosaving -Returns an action object used to signal that post autosaving is locked. +Action that locks post autosaving. _Usage_ @@ -1130,7 +1128,7 @@ _Returns_ ### lockPostSaving -Returns an action object used to signal that post saving is locked. +Action that locks post saving. _Usage_ @@ -1213,8 +1211,7 @@ _Related_ ### redo -Returns an action object used in signalling that undo history should -restore last popped state. +Action that restores last popped state in undo history. ### refreshPost @@ -1270,7 +1267,7 @@ post has been received, either by initialization or save. ### savePost -Action generator for saving the current post in the editor. +Action for saving the current post in the editor. _Parameters_ @@ -1362,15 +1359,15 @@ _Related_ ### trashPost -Action generator for trashing the current post in the editor. +Action for trashing the current post in the editor. ### undo -Returns an action object used in signalling that undo history should pop. +Action that pops a record from undo history and undoes the edit. ### unlockPostAutosaving -Returns an action object used to signal that post autosaving is unlocked. +Action that unlocks post autosaving. _Usage_ @@ -1387,7 +1384,7 @@ _Returns_ ### unlockPostSaving -Returns an action object used to signal that post saving is unlocked. +Action that unlocks post saving. _Usage_ @@ -1437,7 +1434,7 @@ _Returns_ ### updatePostLock -Returns an action object used to lock the editor. +Action that locks the editor. _Parameters_ diff --git a/package-lock.json b/package-lock.json index 00886bf6b36332..6c7806ebf85658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16267,7 +16267,6 @@ "@wordpress/compose": "file:packages/compose", "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", - "@wordpress/data-controls": "file:packages/data-controls", "@wordpress/date": "file:packages/date", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/element": "file:packages/element", diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 28e6513e4cd642..f2dd5ea8df8ba0 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -466,7 +466,7 @@ export const saveEntityRecord = ( // so the client just sends and receives objects. const currentUser = select.getCurrentUser(); const currentUserId = currentUser ? currentUser.id : undefined; - const autosavePost = resolveSelect.getAutosave( + const autosavePost = await resolveSelect.getAutosave( persistedRecord.type, persistedRecord.id, currentUserId diff --git a/packages/editor/package.json b/packages/editor/package.json index 00c9cc6c2f81bc..8e501240d0a1df 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -39,7 +39,6 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", - "@wordpress/data-controls": "file:../data-controls", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", diff --git a/packages/editor/src/components/local-autosave-monitor/index.js b/packages/editor/src/components/local-autosave-monitor/index.js index b406a2545cf395..14f1f3fb7f8673 100644 --- a/packages/editor/src/components/local-autosave-monitor/index.js +++ b/packages/editor/src/components/local-autosave-monitor/index.js @@ -17,7 +17,10 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import AutosaveMonitor from '../autosave-monitor'; -import { localAutosaveGet, localAutosaveClear } from '../../store/controls'; +import { + localAutosaveGet, + localAutosaveClear, +} from '../../store/local-autosave'; import { store as editorStore } from '../../store'; const requestIdleCallback = window.requestIdleCallback diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 7459128ef8e783..02aceef3eaf9a6 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -6,9 +6,8 @@ import { has } from 'lodash'; /** * WordPress dependencies */ +import apiFetch from '@wordpress/api-fetch'; import deprecated from '@wordpress/deprecated'; -import { controls } from '@wordpress/data'; -import { apiFetch } from '@wordpress/data-controls'; import { parse, synchronizeBlocksWithTemplate, @@ -21,7 +20,8 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { STORE_NAME, TRASH_POST_NOTICE_ID } from './constants'; +import { TRASH_POST_NOTICE_ID } from './constants'; +import { localAutosaveSet } from './local-autosave'; import { getNotificationArgumentsForSaveSuccess, getNotificationArgumentsForSaveFail, @@ -36,8 +36,8 @@ import { * @param {Object} edits Initial edited attributes object. * @param {Array?} template Block Template. */ -export function* setupEditor( post, edits, template ) { - yield setupEditorState( post ); +export const setupEditor = ( post, edits, template ) => ( { dispatch } ) => { + dispatch.setupEditorState( post ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; if ( isNewPost && template ) { @@ -52,21 +52,19 @@ export function* setupEditor( post, edits, template ) { } let blocks = parse( content ); blocks = synchronizeBlocksWithTemplate( blocks, template ); - yield resetEditorBlocks( blocks, { + dispatch.resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false, } ); } if ( edits && - Object.keys( edits ).some( - ( key ) => - edits[ key ] !== - ( has( post, [ key, 'raw' ] ) ? post[ key ].raw : post[ key ] ) + Object.values( edits ).some( + ( [ key, edit ] ) => edit !== ( post[ key ]?.raw ?? post[ key ] ) ) ) { - yield editPost( edits ); + dispatch.editPost( edits ); } -} +}; /** * Returns an action object signalling that the editor is being destroyed and @@ -93,34 +91,6 @@ export function resetPost() { return { type: 'DO_NOTHING' }; } -/** - * 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', - options, - }; -} - -/** - * Action for dispatching that a post update request has finished. - * - * @param {Object} options - * - * @return {Object} An action object - */ -export function __experimentalRequestPostUpdateFinish( options = {} ) { - return { - type: 'REQUEST_POST_UPDATE_FINISH', - options, - }; -} - /** * Returns an action object used in signalling that a patch of updates for the * latest version of the post have been received. @@ -159,73 +129,59 @@ export function setupEditorState( post ) { * * @param {Object} edits Post attributes to edit. * @param {Object} options Options for the edit. - * - * @yield {Object} Action object or control. - */ -export function* editPost( edits, options ) { - const { id, type } = yield controls.select( STORE_NAME, 'getCurrentPost' ); - yield controls.dispatch( - coreStore, - 'editEntityRecord', - 'postType', - type, - id, - edits, - options - ); -} + */ +export const editPost = ( edits, options ) => ( { select, registry } ) => { + const { id, type } = select.getCurrentPost(); + registry + .dispatch( coreStore ) + .editEntityRecord( 'postType', type, id, edits, options ); +}; /** - * Action generator for saving the current post in the editor. + * Action for saving the current post in the editor. * * @param {Object} options */ -export function* savePost( options = {} ) { - if ( ! ( yield controls.select( STORE_NAME, 'isEditedPostSaveable' ) ) ) { +export const savePost = ( options = {} ) => async ( { + select, + dispatch, + registry, +} ) => { + if ( ! select.isEditedPostSaveable() ) { return; } - let edits = { - content: yield controls.select( STORE_NAME, 'getEditedPostContent' ), - }; + + const content = select.getEditedPostContent(); + if ( ! options.isAutosave ) { - yield controls.dispatch( STORE_NAME, 'editPost', edits, { - undoIgnore: true, - } ); + dispatch.editPost( { content }, { undoIgnore: true } ); } - yield __experimentalRequestPostUpdateStart( options ); - const previousRecord = yield controls.select( - STORE_NAME, - 'getCurrentPost' - ); - edits = { + const previousRecord = select.getCurrentPost(); + const edits = { id: previousRecord.id, - ...( yield controls.select( - coreStore, - 'getEntityRecordNonTransientEdits', + ...registry + .select( coreStore ) + .getEntityRecordNonTransientEdits( + 'postType', + previousRecord.type, + previousRecord.id + ), + content, + }; + dispatch( { type: 'REQUEST_POST_UPDATE_START', options } ); + await registry + .dispatch( coreStore ) + .saveEntityRecord( 'postType', previousRecord.type, edits, options ); + dispatch( { type: 'REQUEST_POST_UPDATE_FINISH', options } ); + + const error = registry + .select( coreStore ) + .getLastEntitySaveError( 'postType', previousRecord.type, previousRecord.id - ) ), - ...edits, - }; - yield controls.dispatch( - coreStore, - 'saveEntityRecord', - 'postType', - previousRecord.type, - edits, - options - ); - yield __experimentalRequestPostUpdateFinish( options ); - - const error = yield controls.select( - coreStore, - 'getLastEntitySaveError', - 'postType', - previousRecord.type, - previousRecord.id - ); + ); if ( error ) { const args = getNotificationArgumentsForSaveFail( { post: previousRecord, @@ -233,44 +189,30 @@ export function* savePost( options = {} ) { error, } ); if ( args.length ) { - yield controls.dispatch( - noticesStore, - 'createErrorNotice', - ...args - ); + registry.dispatch( noticesStore ).createErrorNotice( ...args ); } } else { - const updatedRecord = yield controls.select( - STORE_NAME, - 'getCurrentPost' - ); + const updatedRecord = select.getCurrentPost(); const args = getNotificationArgumentsForSaveSuccess( { previousPost: previousRecord, post: updatedRecord, - postType: yield controls.resolveSelect( - coreStore, - 'getPostType', - updatedRecord.type - ), + postType: await registry + .resolveSelect( coreStore ) + .getPostType( updatedRecord.type ), options, } ); if ( args.length ) { - yield controls.dispatch( - noticesStore, - 'createSuccessNotice', - ...args - ); + registry.dispatch( noticesStore ).createSuccessNotice( ...args ); } // Make sure that any edits after saving create an undo level and are // considered for change detection. if ( ! options.isAutosave ) { - yield controls.dispatch( - blockEditorStore, - '__unstableMarkLastChangeAsPersistent' - ); + registry + .dispatch( blockEditorStore ) + .__unstableMarkLastChangeAsPersistent(); } } -} +}; /** * Action for refreshing the current post. @@ -287,104 +229,68 @@ export function refreshPost() { } /** - * Action generator for trashing the current post in the editor. - */ -export function* trashPost() { - const postTypeSlug = yield controls.select( - STORE_NAME, - 'getCurrentPostType' - ); - const postType = yield controls.resolveSelect( - coreStore, - 'getPostType', - postTypeSlug - ); - yield controls.dispatch( - noticesStore, - 'removeNotice', - TRASH_POST_NOTICE_ID - ); + * Action for trashing the current post in the editor. + */ +export const trashPost = () => async ( { select, dispatch, registry } ) => { + const postTypeSlug = select.getCurrentPostType(); + const postType = await registry + .resolveSelect( coreStore ) + .getPostType( postTypeSlug ); + registry.dispatch( noticesStore ).removeNotice( TRASH_POST_NOTICE_ID ); try { - const post = yield controls.select( STORE_NAME, 'getCurrentPost' ); - yield apiFetch( { + const post = select.getCurrentPost(); + await apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ post.id }`, method: 'DELETE', } ); - yield controls.dispatch( STORE_NAME, 'savePost' ); + await dispatch.savePost(); } catch ( error ) { - yield controls.dispatch( - noticesStore, - 'createErrorNotice', - ...getNotificationArgumentsForTrashFail( { error } ) - ); + registry + .dispatch( noticesStore ) + .createErrorNotice( + ...getNotificationArgumentsForTrashFail( { error } ) + ); } -} +}; /** - * Action generator used in signalling that the post should autosave. This + * Action that autosaves the current post. This * includes server-side autosaving (default) and client-side (a.k.a. local) * autosaving (e.g. on the Web, the post might be committed to Session * Storage). * * @param {Object?} options Extra flags to identify the autosave. */ -export function* autosave( { local = false, ...options } = {} ) { +export const autosave = ( { local = false, ...options } = {} ) => async ( { + select, + dispatch, +} ) => { if ( local ) { - const post = yield controls.select( STORE_NAME, 'getCurrentPost' ); - const isPostNew = yield controls.select( - STORE_NAME, - 'isEditedPostNew' - ); - const title = yield controls.select( - STORE_NAME, - 'getEditedPostAttribute', - 'title' - ); - const content = yield controls.select( - STORE_NAME, - 'getEditedPostAttribute', - 'content' - ); - const excerpt = yield controls.select( - STORE_NAME, - 'getEditedPostAttribute', - 'excerpt' - ); - yield { - type: 'LOCAL_AUTOSAVE_SET', - postId: post.id, - isPostNew, - title, - content, - excerpt, - }; + const post = select.getCurrentPost(); + const isPostNew = select.isEditedPostNew(); + const title = select.getEditedPostAttribute( 'title' ); + const content = select.getEditedPostAttribute( 'content' ); + const excerpt = select.getEditedPostAttribute( 'excerpt' ); + localAutosaveSet( post.id, isPostNew, title, content, excerpt ); } else { - yield controls.dispatch( STORE_NAME, 'savePost', { - isAutosave: true, - ...options, - } ); + await dispatch.savePost( { isAutosave: true, ...options } ); } -} +}; /** - * Returns an action object used in signalling that undo history should - * restore last popped state. - * - * @yield {Object} Action object. + * Action that restores last popped state in undo history. */ -export function* redo() { - yield controls.dispatch( coreStore, 'redo' ); -} +export const redo = () => ( { registry } ) => { + registry.dispatch( coreStore ).redo(); +}; /** - * Returns an action object used in signalling that undo history should pop. - * - * @yield {Object} Action object. + * Action that pops a record from undo history and undoes the edit. */ -export function* undo() { - yield controls.dispatch( coreStore, 'undo' ); -} +export const undo = () => ( { registry } ) => { + registry.dispatch( coreStore ).undo(); +}; /** * Action that creates an undo history record. @@ -401,10 +307,9 @@ export function createUndoLevel() { } /** - * Returns an action object used to lock the editor. + * Action that locks the editor. * * @param {Object} lock Details about the post lock status, user, and nonce. - * * @return {Object} Action object. */ export function updatePostLock( lock ) { @@ -415,8 +320,7 @@ export function updatePostLock( lock ) { } /** - * Returns an action object used in signalling that the user has enabled the - * publish sidebar. + * Action that enables the publish sidebar. * * @return {Object} Action object */ @@ -427,8 +331,7 @@ export function enablePublishSidebar() { } /** - * Returns an action object used in signalling that the user has disabled the - * publish sidebar. + * Action that disables the publish sidebar. * * @return {Object} Action object */ @@ -439,7 +342,7 @@ export function disablePublishSidebar() { } /** - * Returns an action object used to signal that post saving is locked. + * Action that locks post saving. * * @param {string} lockName The lock name. * @@ -489,7 +392,7 @@ export function lockPostSaving( lockName ) { } /** - * Returns an action object used to signal that post saving is unlocked. + * Action that unlocks post saving. * * @param {string} lockName The lock name. * @@ -509,7 +412,7 @@ export function unlockPostSaving( lockName ) { } /** - * Returns an action object used to signal that post autosaving is locked. + * Action that locks post autosaving. * * @param {string} lockName The lock name. * @@ -529,7 +432,7 @@ export function lockPostAutosaving( lockName ) { } /** - * Returns an action object used to signal that post autosaving is unlocked. + * Action that unlocks post autosaving. * * @param {string} lockName The lock name. * @@ -553,34 +456,27 @@ export function unlockPostAutosaving( lockName ) { * * @param {Array} blocks Block Array. * @param {?Object} options Optional options. - * - * @yield {Object} Action object */ -export function* resetEditorBlocks( blocks, options = {} ) { +export const resetEditorBlocks = ( blocks, options = {} ) => ( { + select, + dispatch, + registry, +} ) => { const { __unstableShouldCreateUndoLevel, selection } = options; const edits = { blocks, selection }; if ( __unstableShouldCreateUndoLevel !== false ) { - const { id, type } = yield controls.select( - STORE_NAME, - 'getCurrentPost' - ); + const { id, type } = select.getCurrentPost(); const noChange = - ( yield controls.select( - coreStore, - 'getEditedEntityRecord', - 'postType', - type, - id - ) ).blocks === edits.blocks; + registry + .select( coreStore ) + .getEditedEntityRecord( 'postType', type, id ).blocks === + edits.blocks; if ( noChange ) { - return yield controls.dispatch( - coreStore, - '__unstableCreateUndoLevel', - 'postType', - type, - id - ); + registry + .dispatch( coreStore ) + .__unstableCreateUndoLevel( 'postType', type, id ); + return; } // We create a new function here on every persistent edit @@ -589,8 +485,9 @@ export function* resetEditorBlocks( blocks, options = {} ) { edits.content = ( { blocks: blocksForSerialization = [] } ) => __unstableSerializeAndClean( blocksForSerialization ); } - yield* editPost( edits ); -} + + dispatch.editPost( edits ); +}; /* * Returns an action object used in signalling that the post editor settings have been updated. @@ -610,16 +507,14 @@ export function updateEditorSettings( settings ) { * Backward compatibility */ -const getBlockEditorAction = ( name ) => - function* ( ...args ) { - deprecated( "`wp.data.dispatch( 'core/editor' )." + name + '`', { - since: '5.3', - alternative: - "`wp.data.dispatch( 'core/block-editor' )." + name + '`', - version: '6.2', - } ); - yield controls.dispatch( blockEditorStore, name, ...args ); - }; +const getBlockEditorAction = ( name ) => ( ...args ) => ( { registry } ) => { + deprecated( "`wp.data.dispatch( 'core/editor' )." + name + '`', { + since: '5.3', + alternative: "`wp.data.dispatch( 'core/block-editor' )." + name + '`', + version: '6.2', + } ); + registry.dispatch( blockEditorStore )[ name ]( ...args ); +}; /** * @see resetBlocks in core/block-editor store. diff --git a/packages/editor/src/store/actions.native.js b/packages/editor/src/store/actions.native.js index 49d40749cb1c7a..d7adcccdafe3bf 100644 --- a/packages/editor/src/store/actions.native.js +++ b/packages/editor/src/store/actions.native.js @@ -20,8 +20,8 @@ export function togglePostTitleSelection( isSelected = true ) { } /** - * Action generator used in signalling that the post should autosave. + * Action that autosaves the post. */ -export function* autosave() { +export const autosave = () => () => { RNReactNativeGutenbergBridge.editorDidAutosave(); -} +}; diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index ed7cb77a5c0afb..4fb31171b19080 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { createReduxStore, registerStore } from '@wordpress/data'; -import { controls as dataControls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -10,7 +9,6 @@ import { controls as dataControls } from '@wordpress/data-controls'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; -import controls from './controls'; import { STORE_NAME } from './constants'; /** @@ -24,10 +22,7 @@ export const storeConfig = { reducer, selectors, actions, - controls: { - ...dataControls, - ...controls, - }, + __experimentalUseThunks: true, }; /** diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/local-autosave.js similarity index 85% rename from packages/editor/src/store/controls.js rename to packages/editor/src/store/local-autosave.js index c41aac33373aa7..536358655d27f3 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/local-autosave.js @@ -36,11 +36,3 @@ export function localAutosaveSet( postId, isPostNew, title, content, excerpt ) { export function localAutosaveClear( postId, isPostNew ) { window.sessionStorage.removeItem( postKey( postId, isPostNew ) ); } - -const controls = { - LOCAL_AUTOSAVE_SET( { postId, isPostNew, title, content, excerpt } ) { - localAutosaveSet( postId, isPostNew, title, content, excerpt ); - }, -}; - -export default controls; diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 2a04378d67bf43..477beddf4d85dc 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -1,443 +1,289 @@ /** * WordPress dependencies */ -import { apiFetch } from '@wordpress/data-controls'; -import { controls } from '@wordpress/data'; +import apiFetch from '@wordpress/api-fetch'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { createRegistry } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ + import * as actions from '../actions'; -import { STORE_NAME, TRASH_POST_NOTICE_ID } from '../constants'; +import { store as editorStore } from '..'; + +jest.useRealTimers(); + +const postId = 44; -const postType = { +const postTypeConfig = { + kind: 'postType', + name: 'post', + baseURL: '/wp/v2/posts', + transientEdits: { blocks: true, selection: true }, + mergedEdits: { meta: true }, + rawAttributes: [ 'title', 'excerpt', 'content' ], +}; + +const postTypeEntity = { + slug: 'post', rest_base: 'posts', labels: { item_updated: 'Updated Post', item_published: 'Post published', + item_reverted_to_draft: 'Post reverted to draft.', }, }; -const postId = 44; -const postTypeSlug = 'post'; -describe( 'Post generator actions', () => { +function createRegistryWithStores() { + // create a registry + const registry = createRegistry(); + + // register stores + registry.register( blockEditorStore ); + registry.register( coreStore ); + registry.register( editorStore ); + registry.register( noticesStore ); + + // register post type entity + registry.dispatch( coreStore ).addEntities( [ postTypeConfig ] ); + + // store post type entity + registry + .dispatch( coreStore ) + .receiveEntityRecords( 'root', 'postType', [ postTypeEntity ] ); + + return registry; +} + +const getMethod = ( options ) => + options.headers?.[ 'X-HTTP-Method-Override' ] || options.method || 'GET'; + +describe( 'Post actions', () => { describe( 'savePost()', () => { - let fulfillment, currentPost, currentPostStatus, isAutosave; - beforeEach( () => { - currentPost = () => ( { + it( 'saves a modified post', async () => { + const post = { id: postId, - type: postTypeSlug, + type: 'post', title: 'bar', content: 'bar', excerpt: 'crackers', - status: currentPostStatus, - } ); - } ); - const reset = ( isAutosaving ) => - ( fulfillment = actions.savePost( { isAutosave: isAutosaving } ) ); - const testConditions = [ - [ - 'yields an action for checking if the post is saveable', - () => true, - () => { - reset( isAutosave ); - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.select( STORE_NAME, 'isEditedPostSaveable' ) - ); - }, - ], - [ - 'yields an action for selecting the current edited post content', - () => true, - () => { - const { value } = fulfillment.next( true ); - expect( value ).toEqual( - controls.select( STORE_NAME, 'getEditedPostContent' ) - ); - }, - ], - [ - "yields an action for editing the post entity's content if not an autosave", - () => true, - () => { - if ( ! isAutosave ) { - const edits = { content: currentPost().content }; - const { value } = fulfillment.next( edits.content ); - expect( value ).toEqual( - controls.dispatch( STORE_NAME, 'editPost', edits, { - undoIgnore: true, - } ) - ); - } - }, - ], - [ - 'yields an action for signalling that an update to the post started', - () => true, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( { - type: 'REQUEST_POST_UPDATE_START', - options: { isAutosave }, - } ); - }, - ], - [ - 'yields an action for selecting the current post', - () => true, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.select( STORE_NAME, 'getCurrentPost' ) - ); - }, - ], - [ - "yields an action for selecting the post entity's non transient edits", - () => true, - () => { - const post = currentPost(); - const { value } = fulfillment.next( post ); - expect( value ).toEqual( - controls.select( - 'core', - 'getEntityRecordNonTransientEdits', - 'postType', - post.type, - post.id - ) - ); - }, - ], - [ - 'yields an action for dispatching an update to the post entity', - () => true, - () => { - const post = currentPost(); - const { value } = fulfillment.next( post ); - expect( value ).toEqual( - controls.dispatch( - 'core', - 'saveEntityRecord', - 'postType', - post.type, - isAutosave ? { ...post, content: undefined } : post, - { - isAutosave, - } - ) - ); - }, - ], - [ - 'yields an action for signalling that an update to the post finished', - () => true, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( { - type: 'REQUEST_POST_UPDATE_FINISH', - options: { isAutosave }, - } ); - }, - ], - [ - "yields an action for selecting the entity's save error", - () => true, - () => { - const post = currentPost(); - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.select( - 'core', - 'getLastEntitySaveError', - 'postType', - post.type, - post.id - ) - ); - }, - ], - [ - 'yields an action for selecting the current post', - () => true, - () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.select( STORE_NAME, 'getCurrentPost' ) - ); - }, - ], - [ - 'yields an action for selecting the current post type config', - () => true, - () => { - const post = currentPost(); - const { value } = fulfillment.next( post ); - expect( value ).toEqual( - controls.resolveSelect( - 'core', - 'getPostType', - post.type - ) - ); - }, - ], - [ - 'yields an action for dispatching a success notice', - () => true, - () => { - if ( ! isAutosave ) { - const { value } = fulfillment.next( postType ); - expect( value ).toEqual( - controls.dispatch( - noticesStore, - 'createSuccessNotice', - currentPostStatus === 'publish' - ? 'Updated Post' - : 'Draft saved', - { - actions: [], - id: 'SAVE_POST_NOTICE_ID', - type: 'snackbar', - } - ) - ); - } - }, - ], - [ - 'yields an action for marking the last change as persistent', - () => true, - () => { - if ( ! isAutosave ) { - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.dispatch( - 'core/block-editor', - '__unstableMarkLastChangeAsPersistent' - ) - ); - } - }, - ], - [ - 'implicitly returns undefined', - () => true, - () => { - expect( fulfillment.next() ).toEqual( { - done: true, - value: undefined, - } ); - }, - ], - ]; - - const conditionalRunTestRoutine = ( isAutosaving ) => ( [ - testDescription, - shouldRun, - testRoutine, - ] ) => { - if ( shouldRun( isAutosaving ) ) { - // eslint-disable-next-line jest/valid-title - it( testDescription, () => { - testRoutine(); - } ); - } - }; - - describe( 'yields with expected responses for when not autosaving and edited post is new', () => { - beforeEach( () => { - isAutosave = false; - currentPostStatus = 'draft'; - } ); - testConditions.forEach( conditionalRunTestRoutine( false ) ); - } ); + status: 'draft', + }; + + // mock apiFetch response + apiFetch.setFetchHandler( async ( options ) => { + const method = getMethod( options ); + const { path, data } = options; - describe( 'yields with expected responses for when not autosaving and edited post is not new', () => { - beforeEach( () => { - isAutosave = false; - currentPostStatus = 'publish'; + if ( + method === 'PUT' && + path.startsWith( `/wp/v2/posts/${ postId }` ) + ) { + return { ...post, ...data }; + } + + throw { + code: 'unknown_path', + message: `Unknown path: ${ method } ${ path }`, + }; } ); - testConditions.forEach( conditionalRunTestRoutine( false ) ); - } ); - describe( 'yields with expected responses for when autosaving is true and edited post is not new', () => { - beforeEach( () => { - isAutosave = true; - currentPostStatus = 'autosave'; + + // create registry + const registry = createRegistryWithStores(); + + // store post + registry + .dispatch( coreStore ) + .receiveEntityRecords( 'postType', 'post', post ); + + // setup editor with post and initial edits + registry.dispatch( editorStore ).setupEditor( post, { + content: 'new bar', } ); - testConditions.forEach( conditionalRunTestRoutine( true ) ); - } ); - } ); - describe( 'autosave()', () => { - it( 'dispatches savePost with the correct arguments', () => { - const fulfillment = actions.autosave(); - const { value } = fulfillment.next(); - expect( value.actionName ).toBe( 'savePost' ); - expect( value.args ).toEqual( [ { isAutosave: true } ] ); - } ); - } ); - describe( 'trashPost()', () => { - let fulfillment; - const currentPost = { id: 10, content: 'foo', status: 'publish' }; - const reset = () => ( fulfillment = actions.trashPost() ); - const rewind = () => { - reset(); - fulfillment.next(); - fulfillment.next( postTypeSlug ); - fulfillment.next( postType ); - fulfillment.next(); - fulfillment.next( currentPost ); - }; - it( 'yields expected action for selecting the current post type slug', () => { - reset(); - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.select( STORE_NAME, 'getCurrentPostType' ) - ); - } ); - it( 'yields expected action for selecting the post type object', () => { - const { value } = fulfillment.next( postTypeSlug ); - expect( value ).toEqual( - controls.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( - controls.dispatch( - noticesStore, - 'removeNotice', - TRASH_POST_NOTICE_ID - ) - ); - } - ); - it( 'yields expected action for selecting the currentPost', () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.select( STORE_NAME, 'getCurrentPost' ) - ); - } ); - it( 'yields expected action object for the api fetch', () => { - const { value } = fulfillment.next( currentPost ); - expect( value ).toEqual( - apiFetch( { - path: `/wp/v2/${ postType.rest_base }/${ currentPost.id }`, - method: 'DELETE', - } ) + + // check that the post is dirty + expect( registry.select( editorStore ).isEditedPostDirty() ).toBe( + true ); - } ); - describe( 'expected yields when fetch throws an error', () => { - it( 'yields expected action for dispatching an error notice', () => { - const error = { foo: 'bar', code: 'fail' }; - const { value } = fulfillment.throw( error ); - expect( value ).toEqual( - controls.dispatch( - noticesStore, - 'createErrorNotice', - 'Trashing failed', - { - id: TRASH_POST_NOTICE_ID, - } - ) - ); - } ); - } ); - describe( 'expected yields when fetch does not throw an error', () => { - it( 'yields expected dispatch action for saving the post', () => { - rewind(); - const { value } = fulfillment.next(); - expect( value ).toEqual( - controls.dispatch( STORE_NAME, 'savePost' ) - ); - } ); - } ); - } ); -} ); -describe( 'Editor actions', () => { - describe( 'setupEditor()', () => { - it( 'should yield the setup editor actions but not reset blocks when the template is empty', () => { - const post = { content: { raw: '' }, status: 'publish' }; - const fulfillment = actions.setupEditor( post ); - const value = fulfillment.next().value; - expect( value ).toEqual( - actions.setupEditorState( { - content: { raw: '' }, - status: 'publish', - } ) + // save the post + await registry.dispatch( editorStore ).savePost(); + + // check the new content + const content = registry + .select( editorStore ) + .getEditedPostContent(); + expect( content ).toBe( 'new bar' ); + + // check that the post is no longer dirty + expect( registry.select( editorStore ).isEditedPostDirty() ).toBe( + false ); - } ); - } ); - describe( 'requestPostUpdateStart', () => { - it( 'should return the REQUEST_POST_UPDATE_START action', () => { - const result = actions.__experimentalRequestPostUpdateStart(); - expect( result ).toEqual( { - type: 'REQUEST_POST_UPDATE_START', - options: {}, - } ); + // check that a success notice has been shown + const notices = registry.select( noticesStore ).getNotices(); + expect( notices ).toMatchObject( [ + { + status: 'success', + content: 'Draft saved', + }, + ] ); } ); } ); - describe( 'editPost', () => { - it( 'should edit the relevant entity record', () => { - const edits = { format: 'sample' }; - const fulfillment = actions.editPost( edits ); - expect( fulfillment.next() ).toEqual( { - done: false, - value: controls.select( STORE_NAME, 'getCurrentPost' ), - } ); - const post = { id: 1, type: 'post' }; - expect( fulfillment.next( post ) ).toEqual( { - done: false, - value: controls.dispatch( - 'core', - 'editEntityRecord', - 'postType', - post.type, - post.id, - edits, - undefined - ), + describe( 'autosave()', () => { + it( 'autosaves a modified post', async () => { + const post = { + id: postId, + type: 'post', + title: 'bar', + content: 'bar', + excerpt: 'crackers', + status: 'draft', + }; + + // mock apiFetch response + apiFetch.setFetchHandler( async ( options ) => { + const method = getMethod( options ); + const { path, data } = options; + + if ( + method === 'GET' && + path.startsWith( '/wp/v2/users/me' ) + ) { + return { id: 1 }; + } else if ( + path.startsWith( `/wp/v2/posts/${ postId }/autosaves` ) + ) { + if ( method === 'POST' ) { + return { ...post, ...data }; + } else if ( method === 'GET' ) { + return []; + } + } + + throw { + code: 'unknown_path', + message: `Unknown path: ${ method } ${ path }`, + }; } ); - expect( fulfillment.next() ).toEqual( { - done: true, - value: undefined, + + // create registry + const registry = createRegistryWithStores(); + + // set current user + registry.dispatch( coreStore ).receiveCurrentUser( { id: 1 } ); + + // store post + registry + .dispatch( coreStore ) + .receiveEntityRecords( 'postType', 'post', post ); + + // setup editor with post and initial edits + registry.dispatch( editorStore ).setupEditor( post, { + content: 'new bar', } ); + + // check that the post is dirty + expect( registry.select( editorStore ).isEditedPostDirty() ).toBe( + true + ); + + // autosave the post + await registry.dispatch( editorStore ).autosave(); + + // check the new content + const content = registry + .select( editorStore ) + .getEditedPostContent(); + expect( content ).toBe( 'new bar' ); + + // check that the post is no longer dirty + expect( registry.select( editorStore ).isEditedPostDirty() ).toBe( + false + ); + + // check that no notice has been shown on autosave + const notices = registry.select( noticesStore ).getNotices(); + expect( notices ).toMatchObject( [] ); } ); } ); - describe( 'redo', () => { - it( 'should yield the REDO action', () => { - const fulfillment = actions.redo(); - expect( fulfillment.next() ).toEqual( { - done: false, - value: controls.dispatch( 'core', 'redo' ), - } ); - expect( fulfillment.next() ).toEqual( { - done: true, - value: undefined, + describe( 'trashPost()', () => { + it( 'trashes a post', async () => { + const post = { + id: postId, + type: 'post', + content: 'foo', + status: 'publish', + }; + + let gotTrashed = false; + + // mock apiFetch response + apiFetch.setFetchHandler( async ( options ) => { + const method = getMethod( options ); + const { path, data } = options; + + if ( path.startsWith( `/wp/v2/posts/${ postId }` ) ) { + if ( method === 'DELETE' ) { + gotTrashed = true; + return { ...post, status: 'trash' }; + } else if ( method === 'PUT' ) { + return { + ...post, + ...( gotTrashed && { status: 'trash' } ), + ...data, + }; + } + } + + throw { + code: 'unknown_path', + message: `Unknown path: ${ path }`, + }; } ); + + // create registry + const registry = createRegistryWithStores(); + + // store post + registry + .dispatch( coreStore ) + .receiveEntityRecords( 'postType', 'post', post ); + + // setup editor with post + registry.dispatch( editorStore ).setupEditor( post ); + + // trash the post + await registry.dispatch( editorStore ).trashPost(); + + // check that there are no notices + const notices = registry.select( noticesStore ).getNotices(); + expect( notices ).toEqual( [] ); + + // check the new status + const { status } = registry.select( editorStore ).getCurrentPost(); + expect( status ).toBe( 'trash' ); } ); } ); +} ); - describe( 'undo', () => { - it( 'should yield the UNDO action', () => { - const fulfillment = actions.undo(); - expect( fulfillment.next() ).toEqual( { - done: false, - value: controls.dispatch( 'core', 'undo' ), - } ); - expect( fulfillment.next() ).toEqual( { - done: true, - value: undefined, - } ); +describe( 'Editor actions', () => { + describe( 'setupEditor()', () => { + it( 'should setup the editor', () => { + // create registry + const registry = createRegistryWithStores(); + + registry + .dispatch( editorStore ) + .setupEditor( { id: 10, type: 'post' } ); + expect( registry.select( editorStore ).getCurrentPostId() ).toBe( + 10 + ); } ); } ); diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 06ea3e5e4c58c5..4d33386dc7eb73 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -28,6 +28,11 @@ export function getNotificationArgumentsForSaveSuccess( data ) { return []; } + // No notice is shown after trashing a post + if ( post.status === 'trash' && previousPost.status !== 'trash' ) { + return []; + } + const publishStatus = [ 'publish', 'private', 'future' ]; const isPublished = includes( publishStatus, previousPost.status ); const willPublish = includes( publishStatus, post.status ); diff --git a/packages/editor/src/store/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js index f6e23f6a7d12d1..054b4f47fdf9a7 100644 --- a/packages/editor/src/store/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -74,6 +74,7 @@ describe( 'getNotificationArgumentsForSaveSuccess()', () => { }, ], ], + [ 'when post will be trashed', [ 'publish', 'trash', true ], [] ], ].forEach( ( [ description,