diff --git a/projects/js-packages/publicize-components/changelog/add-use-post-pre-publish-value-hook b/projects/js-packages/publicize-components/changelog/add-use-post-pre-publish-value-hook new file mode 100644 index 0000000000000..14f97d84416bf --- /dev/null +++ b/projects/js-packages/publicize-components/changelog/add-use-post-pre-publish-value-hook @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Added usePostPrePublishValue hook diff --git a/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/index.ts b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/index.ts new file mode 100644 index 0000000000000..60d80b8b9d97f --- /dev/null +++ b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/index.ts @@ -0,0 +1,39 @@ +import { useSelect } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; +import { useEffect, useRef, useState } from '@wordpress/element'; + +/** + * Preserve a value from the post pre-publish state. + * + * The value will stop updating after the post is published. + * + * @template V + * @param {V} value - The value to preserve. + * + * @return {V} The preserved value. + */ +export function usePostPrePublishValue< V >( value: V ) { + const isPublishing = useSelect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `@wordpress/editor` is a nightmare to work with TypeScript + select => ( select( editorStore ) as any ).isPublishingPost(), + [] + ); + + const [ currentValue, setCurrentValue ] = useState( value ); + + const valueFrozen = useRef( false ); + + useEffect( () => { + // Freeze the value after publishing starts. + if ( isPublishing ) { + valueFrozen.current = true; + } + + // Since the value is not frozen yet, update the current value. + if ( ! valueFrozen.current ) { + setCurrentValue( value ); + } + }, [ isPublishing, value ] ); + + return currentValue; +} diff --git a/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/test/index.test.js b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/test/index.test.js new file mode 100644 index 0000000000000..70b59a9ebdc08 --- /dev/null +++ b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/test/index.test.js @@ -0,0 +1,79 @@ +import { act, renderHook } from '@testing-library/react'; +import { RegistryProvider } from '@wordpress/data'; +import { store as editorStore } from '@wordpress/editor'; +import { usePostPrePublishValue } from '../'; +import { + connections as connectionsList, + createRegistryWithStores, +} from '../../../utils/test-utils'; + +const connections = connectionsList.map( connection => ( { ...connection, enabled: true } ) ); + +const post = { + jetpack_publicize_connections: [ connections[ 0 ] ], +}; + +describe( 'usePostPrePublishValue', () => { + it( 'should return the value by default', async () => { + const registry = createRegistryWithStores( post ); + + const { result } = renderHook( () => usePostPrePublishValue( 'test-value' ), { + wrapper: ( { children } ) => ( + { children } + ), + } ); + + expect( result.current ).toBe( 'test-value' ); + } ); + + it( 'should return the updated value when the post is not being published', async () => { + const registry = createRegistryWithStores( post ); + + const { rerender, result } = renderHook( + ( initialValue = 'first-value' ) => usePostPrePublishValue( initialValue ), + { + wrapper: ( { children } ) => ( + { children } + ), + } + ); + + rerender( 'second-value' ); + + await act( async () => { + await registry.dispatch( editorStore ).editPost( { + status: 'draft', + content: 'Some test content', + } ); + } ); + + expect( result.current ).toBe( 'second-value' ); + } ); + + it( 'should preserve the pre-publish value', async () => { + const registry = createRegistryWithStores( post ); + + const { rerender, result } = renderHook( + ( initialValue = 'first-value' ) => usePostPrePublishValue( initialValue ), + { + wrapper: ( { children } ) => ( + { children } + ), + } + ); + + rerender( 'second-value' ); + + await act( async () => { + registry.dispatch( editorStore ).editPost( { + status: 'publish', + content: 'Some test content', + } ); + registry.dispatch( editorStore ).savePost(); + } ); + + rerender( 'third-value' ); + + expect( result.current ).toBe( 'second-value' ); + } ); +} ); diff --git a/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js b/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js index 2593baa4d1c1d..c74bf768b4b3d 100644 --- a/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js +++ b/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js @@ -7,7 +7,7 @@ import { store as socialStore } from '../../../social-store'; import { connections as connectionsList, createRegistryWithStores, - testPost, + postPublishFetchHandler, } from '../../../utils/test-utils'; const connections = connectionsList.map( connection => ( { ...connection, enabled: true } ) ); @@ -16,9 +16,6 @@ const post = { jetpack_publicize_connections: [ connections[ 0 ] ], }; -const getMethod = options => - options.headers?.[ 'X-HTTP-Method-Override' ] || options.method || 'GET'; - describe( 'useSyncPostDataToStore', () => { it( 'should do nothing by default', async () => { const registry = createRegistryWithStores( post ); @@ -67,37 +64,7 @@ describe( 'useSyncPostDataToStore', () => { await registry.resolveSelect( socialStore ).getConnections(); // Mock apiFetch response. - apiFetch.setFetchHandler( async options => { - const method = getMethod( options ); - const { path, data, parse = true } = options; - - const wrapReturn = parse - ? v => v - : v => - // Ideally we'd do `new Response( JSON.stringify( v ) )` here, but jsdom deletes that. Sigh. - // See https://github.com/jsdom/jsdom/issues/1724 - ( { - async json() { - return v; - }, - } ); - - if ( method === 'PUT' && path.startsWith( `/wp/v2/posts/${ testPost.id }` ) ) { - return wrapReturn( { ...post, ...data } ); - } else if ( - // This URL is requested by the actions dispatched in this test. - // They are safe to ignore and are only listed here to avoid triggeringan error. - method === 'GET' && - path.startsWith( '/wp/v2/types/post' ) - ) { - return wrapReturn( {} ); - } - - throw { - code: 'unknown_path', - message: `Unknown path: ${ method } ${ path }`, - }; - } ); + apiFetch.setFetchHandler( postPublishFetchHandler( post ) ); const prevConnections = registry.select( socialStore ).getConnections(); diff --git a/projects/js-packages/publicize-components/src/utils/test-utils.js b/projects/js-packages/publicize-components/src/utils/test-utils.js index 857e4ecc1585d..fa2f326be7bc6 100644 --- a/projects/js-packages/publicize-components/src/utils/test-utils.js +++ b/projects/js-packages/publicize-components/src/utils/test-utils.js @@ -118,3 +118,54 @@ export function createActiveConnections( count ) { }, ]; } + +const getMethod = options => + options.headers?.[ 'X-HTTP-Method-Override' ] || options.method || 'GET'; + +/** + * Get the mocked fetch handler for post publish fetch requests. + * + * @param {Record} postData - Data to be used in the fetch request. + * + * @return {(options: import('@wordpress/api-fetch/build-types/types').APIFetchOptions) => Promise} Promise resolving to the fetch response + */ +export function postPublishFetchHandler( postData ) { + /** + * The mocked fetch handler for post publish fetch requests. + * + * @param {import('@wordpress/api-fetch/build-types/types').APIFetchOptions} options - Fetch options. + * + * @return {Promise} Promise resolving to the fetch response + */ + return async function ( options ) { + const method = getMethod( options ); + const { path, data, parse = true } = options; + + const wrapReturn = parse + ? v => v + : v => + // Ideally we'd do `new Response( JSON.stringify( v ) )` here, but jsdom deletes that. Sigh. + // See https://github.com/jsdom/jsdom/issues/1724 + ( { + async json() { + return v; + }, + } ); + + if ( method === 'PUT' && path.startsWith( `/wp/v2/posts/${ testPost.id }` ) ) { + return wrapReturn( { ...postData, ...data } ); + } else if ( + // This URL is requested by the actions dispatched in this test. + // They are safe to ignore and are only listed here to avoid triggeringan error. + method === 'GET' && + path.startsWith( '/wp/v2/types/post' ) + ) { + return wrapReturn( {} ); + } + + throw { + code: 'unknown_path', + message: `Unknown path: ${ method } ${ path }`, + }; + }; +}