diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index f54e541122e150..da5dd9af47e122 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -561,12 +561,13 @@ Action triggered to delete an entity record. _Parameters_ -- _kind_ `string`: Kind of the deleted entity record. -- _name_ `string`: Name of the deleted entity record. -- _recordId_ `string`: Record ID of the deleted entity record. +- _kind_ `string`: Kind of the deleted entity. +- _name_ `string`: Name of the deleted entity. +- _recordId_ `string`: Record ID of the deleted entity. - _query_ `?Object`: Special query parameters for the DELETE API call. - _options_ `[Object]`: Delete options. - _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. +- _options.throwOnError_ `[boolean]`: If false, this action suppresses all the exceptions. Defaults to false. ### editEntityRecord @@ -734,6 +735,7 @@ _Parameters_ - _options_ `Object`: Saving options. - _options.isAutosave_ `[boolean]`: Whether this is an autosave. - _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. +- _options.throwOnError_ `[boolean]`: If false, this action suppresses all the exceptions. Defaults to false. ### undo diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index 1ac48df5af1631..c8a308c5d03087 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### New Features +– The saveEntityRecord, saveEditedEntityRecord, and deleteEntityRecord actions now accept an optional throwOnError option (defaults to false). When set to true, any exceptions occurring when the action was executing are re-thrown, causing dispatch().saveEntityRecord() to reject with an error. ([#39258](https://github.com/WordPress/gutenberg/pull/39258)) + ## 4.2.0 (2022-03-11) ## 4.1.2 (2022-02-23) diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 4b9d4536764fa3..d19c0a11ad0b01 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -64,12 +64,13 @@ Action triggered to delete an entity record. _Parameters_ -- _kind_ `string`: Kind of the deleted entity record. -- _name_ `string`: Name of the deleted entity record. -- _recordId_ `string`: Record ID of the deleted entity record. +- _kind_ `string`: Kind of the deleted entity. +- _name_ `string`: Name of the deleted entity. +- _recordId_ `string`: Record ID of the deleted entity. - _query_ `?Object`: Special query parameters for the DELETE API call. - _options_ `[Object]`: Delete options. - _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. +- _options.throwOnError_ `[boolean]`: If false, this action suppresses all the exceptions. Defaults to false. ### editEntityRecord @@ -237,6 +238,7 @@ _Parameters_ - _options_ `Object`: Saving options. - _options.isAutosave_ `[boolean]`: Whether this is an autosave. - _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. +- _options.throwOnError_ `[boolean]`: If false, this action suppresses all the exceptions. Defaults to false. ### undo diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index cd8fed52937c03..a87b82c4cb5152 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -209,22 +209,24 @@ export function receiveEmbedPreview( url, preview ) { /** * Action triggered to delete an entity record. * - * @param {string} kind Kind of the deleted entity record. - * @param {string} name Name of the deleted entity record. - * @param {string} recordId Record ID of the deleted entity record. - * @param {?Object} query Special query parameters for the - * DELETE API call. - * @param {Object} [options] Delete options. - * @param {Function} [options.__unstableFetch] Internal use only. Function to - * call instead of `apiFetch()`. - * Must return a promise. + * @param {string} kind Kind of the deleted entity. + * @param {string} name Name of the deleted entity. + * @param {string} recordId Record ID of the deleted entity. + * @param {?Object} query Special query parameters for the + * DELETE API call. + * @param {Object} [options] Delete options. + * @param {Function} [options.__unstableFetch] Internal use only. Function to + * call instead of `apiFetch()`. + * Must return a promise. + * @param {boolean} [options.throwOnError=false] If false, this action suppresses all + * the exceptions. Defaults to false. */ export const deleteEntityRecord = ( kind, name, recordId, query, - { __unstableFetch = apiFetch } = {} + { __unstableFetch = apiFetch, throwOnError = false } = {} ) => async ( { dispatch } ) => { const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); const entityConfig = find( configs, { kind, name } ); @@ -248,6 +250,7 @@ export const deleteEntityRecord = ( recordId, } ); + let hasError = false; try { let path = `${ entityConfig.baseURL }/${ recordId }`; @@ -262,6 +265,7 @@ export const deleteEntityRecord = ( await dispatch( removeItems( kind, name, recordId, true ) ); } catch ( _error ) { + hasError = true; error = _error; } @@ -273,6 +277,10 @@ export const deleteEntityRecord = ( error, } ); + if ( hasError && throwOnError ) { + throw error; + } + return deletedRecord; } finally { dispatch.__unstableReleaseStoreLock( lock ); @@ -386,20 +394,26 @@ export function __unstableCreateUndoLevel() { /** * Action triggered to save an entity record. * - * @param {string} kind Kind of the received entity. - * @param {string} name Name of the received entity. - * @param {Object} record Record to be saved. - * @param {Object} options Saving options. - * @param {boolean} [options.isAutosave=false] Whether this is an autosave. - * @param {Function} [options.__unstableFetch] Internal use only. Function to - * call instead of `apiFetch()`. - * Must return a promise. + * @param {string} kind Kind of the received entity. + * @param {string} name Name of the received entity. + * @param {Object} record Record to be saved. + * @param {Object} options Saving options. + * @param {boolean} [options.isAutosave=false] Whether this is an autosave. + * @param {Function} [options.__unstableFetch] Internal use only. Function to + * call instead of `apiFetch()`. + * Must return a promise. + * @param {boolean} [options.throwOnError=false] If false, this action suppresses all + * the exceptions. Defaults to false. */ export const saveEntityRecord = ( kind, name, record, - { isAutosave = false, __unstableFetch = apiFetch } = {} + { + isAutosave = false, + __unstableFetch = apiFetch, + throwOnError = false, + } = {} ) => async ( { select, resolveSelect, dispatch } ) => { const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); const entityConfig = find( configs, { kind, name } ); @@ -445,6 +459,7 @@ export const saveEntityRecord = ( } ); let updatedRecord; let error; + let hasError = false; try { const path = `${ entityConfig.baseURL }${ recordId ? '/' + recordId : '' @@ -567,6 +582,7 @@ export const saveEntityRecord = ( ); } } catch ( _error ) { + hasError = true; error = _error; } dispatch( { @@ -578,6 +594,10 @@ export const saveEntityRecord = ( isAutosave, } ); + if ( hasError && throwOnError ) { + throw error; + } + return updatedRecord; } finally { dispatch.__unstableReleaseStoreLock( lock ); diff --git a/packages/core-data/src/test/actions.js b/packages/core-data/src/test/actions.js index e902c532a6193c..8a73a3b3040b59 100644 --- a/packages/core-data/src/test/actions.js +++ b/packages/core-data/src/test/actions.js @@ -110,6 +110,68 @@ describe( 'deleteEntityRecord', () => { expect( result ).toBe( deletedRecord ); } ); + + it( 'throws on error when throwOnError is true', async () => { + const entities = [ + { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, + ]; + + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( entities ); + + // Provide response + apiFetch.mockImplementation( () => { + throw new Error( 'API error' ); + } ); + + await expect( + deleteEntityRecord( + 'postType', + 'post', + 10, + {}, + { + throwOnError: true, + } + )( { dispatch } ) + ).rejects.toEqual( new Error( 'API error' ) ); + } ); + + it( 'resolves on error when throwOnError is false', async () => { + const entities = [ + { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, + ]; + + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( entities ); + + // Provide response + apiFetch.mockImplementation( () => { + throw new Error( 'API error' ); + } ); + + await expect( + deleteEntityRecord( + 'postType', + 'post', + 10, + {}, + { + throwOnError: false, + } + )( { dispatch } ) + ).resolves.toBe( false ); + } ); } ); describe( 'saveEditedEntityRecord', () => { @@ -201,9 +263,15 @@ describe( 'saveEditedEntityRecord', () => { } ); describe( 'saveEntityRecord', () => { + let dispatch; beforeEach( async () => { apiFetch.mockReset(); jest.useFakeTimers(); + dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); } ); it( 'triggers a POST request for a new record', async () => { @@ -215,11 +283,6 @@ describe( 'saveEntityRecord', () => { getRawEntityRecord: () => post, }; - const dispatch = Object.assign( jest.fn(), { - receiveEntityRecords: jest.fn(), - __unstableAcquireStoreLock: jest.fn(), - __unstableReleaseStoreLock: jest.fn(), - } ); // Provide entities dispatch.mockReturnValueOnce( configs ); @@ -278,6 +341,54 @@ describe( 'saveEntityRecord', () => { expect( result ).toBe( updatedRecord ); } ); + it( 'throws on error when throwOnError is true', async () => { + const post = { title: 'new post' }; + const entities = [ + { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, + ]; + const select = { + getRawEntityRecord: () => post, + }; + + // Provide entities + dispatch.mockReturnValueOnce( entities ); + + // Provide response + apiFetch.mockImplementation( () => { + throw new Error( 'API error' ); + } ); + + await expect( + saveEntityRecord( 'postType', 'post', post, { + throwOnError: true, + } )( { select, dispatch } ) + ).rejects.toEqual( new Error( 'API error' ) ); + } ); + + it( 'resolves on error when throwOnError is false', async () => { + const post = { title: 'new post' }; + const entities = [ + { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, + ]; + const select = { + getRawEntityRecord: () => post, + }; + + // Provide entities + dispatch.mockReturnValueOnce( entities ); + + // Provide response + apiFetch.mockImplementation( () => { + throw new Error( 'API error' ); + } ); + + await expect( + saveEntityRecord( 'postType', 'post', post, { + throwOnError: false, + } )( { select, dispatch } ) + ).resolves.toEqual( undefined ); + } ); + it( 'triggers a PUT request for an existing record', async () => { const post = { id: 10, title: 'new post' }; const configs = [ @@ -287,11 +398,6 @@ describe( 'saveEntityRecord', () => { getRawEntityRecord: () => post, }; - const dispatch = Object.assign( jest.fn(), { - receiveEntityRecords: jest.fn(), - __unstableAcquireStoreLock: jest.fn(), - __unstableReleaseStoreLock: jest.fn(), - } ); // Provide entities dispatch.mockReturnValueOnce( configs ); @@ -364,11 +470,6 @@ describe( 'saveEntityRecord', () => { getRawEntityRecord: () => ( {} ), }; - const dispatch = Object.assign( jest.fn(), { - receiveEntityRecords: jest.fn(), - __unstableAcquireStoreLock: jest.fn(), - __unstableReleaseStoreLock: jest.fn(), - } ); // Provide entities dispatch.mockReturnValueOnce( configs ); diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index beed4b75d50ef4..d811ce2564c6e9 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -56,15 +56,9 @@ export const saveNavigationPost = ( post ) => async ( { // Save menu. await registry .dispatch( coreDataStore ) - .saveEditedEntityRecord( 'root', 'menu', menuId ); - - const error = registry - .select( coreDataStore ) - .getLastEntitySaveError( 'root', 'menu', menuId ); - - if ( error ) { - throw new Error( error.message ); - } + .saveEditedEntityRecord( 'root', 'menu', menuId, { + throwOnError: true, + } ); // Save menu items. const updatedBlocks = await dispatch( diff --git a/packages/edit-post/src/components/header/template-title/delete-template.js b/packages/edit-post/src/components/header/template-title/delete-template.js index 3d09c4e34caf34..c550b552bbd7e7 100644 --- a/packages/edit-post/src/components/header/template-title/delete-template.js +++ b/packages/edit-post/src/components/header/template-title/delete-template.js @@ -67,7 +67,9 @@ export default function DeleteTemplate() { ...settings, availableTemplates: newAvailableTemplates, } ); - deleteEntityRecord( 'postType', 'wp_template', template.id ); + deleteEntityRecord( 'postType', 'wp_template', template.id, { + throwOnError: true, + } ); }; return ( diff --git a/packages/edit-site/src/components/add-new-template/new-template-part.js b/packages/edit-site/src/components/add-new-template/new-template-part.js index 379ae5f8854a41..d2e396f6580dbb 100644 --- a/packages/edit-site/src/components/add-new-template/new-template-part.js +++ b/packages/edit-site/src/components/add-new-template/new-template-part.js @@ -7,7 +7,7 @@ import { kebabCase } from 'lodash'; * WordPress dependencies */ import { useState } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; @@ -24,7 +24,6 @@ export default function NewTemplatePart( { postType } ) { const [ isModalOpen, setIsModalOpen ] = useState( false ); const { createErrorNotice } = useDispatch( noticesStore ); const { saveEntityRecord } = useDispatch( coreStore ); - const { getLastEntitySaveError } = useSelect( coreStore ); async function createTemplatePart( { title, area } ) { if ( ! title ) { @@ -49,18 +48,10 @@ export default function NewTemplatePart( { postType } ) { title, content: '', area, - } + }, + { throwOnError: true } ); - const lastEntitySaveError = getLastEntitySaveError( - 'postType', - 'wp_template_part', - templatePart.id - ); - if ( lastEntitySaveError ) { - throw lastEntitySaveError; - } - setIsModalOpen( false ); // Navigate to the created template part editor. diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js index 9f3328472eb7e6..3340d37fb21397 100644 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -86,7 +86,6 @@ export default function NewTemplate( { postType } ) { ); const { saveEntityRecord } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { getLastEntitySaveError } = useSelect( coreStore ); async function createTemplate( { slug } ) { try { @@ -103,18 +102,10 @@ export default function NewTemplate( { postType } ) { slug: slug.toString(), status: 'publish', title, - } + }, + { throwOnError: true } ); - const lastEntitySaveError = getLastEntitySaveError( - 'postType', - 'wp_template', - template.id - ); - if ( lastEntitySaveError ) { - throw lastEntitySaveError; - } - // Navigate to the created template editor. history.push( { postId: template.id, diff --git a/packages/edit-site/src/components/list/actions/rename-menu-item.js b/packages/edit-site/src/components/list/actions/rename-menu-item.js index 4eb7d1691ad709..1ce4cc843035be 100644 --- a/packages/edit-site/src/components/list/actions/rename-menu-item.js +++ b/packages/edit-site/src/components/list/actions/rename-menu-item.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { Button, Flex, @@ -19,7 +19,6 @@ export default function RenameMenuItem( { template, onClose } ) { const [ title, setTitle ] = useState( () => template.title.rendered ); const [ isModalOpen, setIsModalOpen ] = useState( false ); - const { getLastEntitySaveError } = useSelect( coreStore ); const { editEntityRecord, saveEditedEntityRecord } = useDispatch( coreStore ); @@ -48,19 +47,10 @@ export default function RenameMenuItem( { template, onClose } ) { await saveEditedEntityRecord( 'postType', template.type, - template.id + template.id, + { throwOnError: true } ); - const lastError = getLastEntitySaveError( - 'postType', - template.type, - template.id - ); - - if ( lastError ) { - throw lastError; - } - createSuccessNotice( __( 'Entity renamed.' ), { type: 'snackbar', } ); diff --git a/packages/edit-widgets/src/store/actions.js b/packages/edit-widgets/src/store/actions.js index 1f57b042a8932c..392b552c6ad57f 100644 --- a/packages/edit-widgets/src/store/actions.js +++ b/packages/edit-widgets/src/store/actions.js @@ -282,18 +282,11 @@ export const saveWidgetArea = ( widgetAreaId ) => async ( { }; const trySaveWidgetArea = ( widgetAreaId ) => ( { registry } ) => { - const saveErrorBefore = registry - .select( coreStore ) - .getLastEntitySaveError( KIND, WIDGET_AREA_ENTITY_TYPE, widgetAreaId ); registry .dispatch( coreStore ) - .saveEditedEntityRecord( KIND, WIDGET_AREA_ENTITY_TYPE, widgetAreaId ); - const saveErrorAfter = registry - .select( coreStore ) - .getLastEntitySaveError( KIND, WIDGET_AREA_ENTITY_TYPE, widgetAreaId ); - if ( saveErrorAfter && saveErrorBefore !== saveErrorAfter ) { - throw new Error( saveErrorAfter ); - } + .saveEditedEntityRecord( KIND, WIDGET_AREA_ENTITY_TYPE, widgetAreaId, { + throwOnError: true, + } ); }; /**