diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 31163cae4dcbf4..03a675531cbc04 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -501,20 +501,15 @@ export function* saveEntityRecord( yield receiveAutosaves( persistedRecord.id, updatedRecord ); } } else { - // Auto drafts should be converted to drafts on explicit saves and we should not respect their default title, - // but some plugins break with this behavior so we can't filter it on the server. - let data = record; - if ( - kind === 'postType' && - persistedRecord && - persistedRecord.status === 'auto-draft' - ) { - if ( ! data.status ) { - data = { ...data, status: 'draft' }; - } - if ( ! data.title || data.title === 'Auto Draft' ) { - data = { ...data, title: '' }; - } + let edits = record; + if ( entity.__unstablePrePersist ) { + edits = { + ...edits, + ...entity.__unstablePrePersist( + persistedRecord, + edits + ), + }; } // Get the full local version of the record before the update, @@ -536,7 +531,7 @@ export function* saveEntityRecord( yield receiveEntityRecords( kind, name, - { ...persistedEntity, ...data }, + { ...persistedEntity, ...edits }, undefined, // This must be false or it will trigger a GET request in parallel to the PUT/POST below false @@ -545,7 +540,7 @@ export function* saveEntityRecord( updatedRecord = yield apiFetch( { path, method: recordId ? 'PUT' : 'POST', - data, + data: edits, } ); yield receiveEntityRecords( kind, diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 8e249afae3ff1c..c3c81b8dbfee03 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -114,6 +114,69 @@ export const kinds = [ { name: 'taxonomy', loadEntities: loadTaxonomyEntities }, ]; +/** + * Returns a function to be used to retrieve the title of a given post type record. + * + * @param {string} postTypeName PostType name. + * @return {Function} getTitle. + */ +export const getPostTypeTitle = ( postTypeName ) => ( record ) => { + if ( [ 'wp_template_part', 'wp_template' ].includes( postTypeName ) ) { + return ( + record?.title?.rendered || record?.title || startCase( record.slug ) + ); + } + return record?.title?.rendered || record?.title || String( record.id ); +}; + +/** + * Returns a function to be used to retrieve extra edits to apply before persisting a post type. + * + * @param {string} postTypeName PostType name. + * @return {Function} prePersistHandler. + */ +export const getPostTypePrePersistHandler = ( postTypeName ) => ( + persistedRecord, + edits +) => { + const newEdits = {}; + + // Fix template titles. + if ( + [ 'wp_template', 'wp_template_part' ].includes( postTypeName ) && + ! edits.title && + ! persistedRecord.title + ) { + newEdits.title = persistedRecord + ? getPostTypeTitle( postTypeName )( persistedRecord ) + : edits.slug; + } + + // Templates and template parts can only be published. + if ( [ 'wp_template', 'wp_template_part' ].includes( postTypeName ) ) { + newEdits.status = 'publish'; + } + + if ( persistedRecord?.status === 'auto-draft' ) { + // Saving an auto-draft should create a draft by default. + if ( ! edits.status && ! newEdits.status ) { + newEdits.status = 'draft'; + } + + // Fix the auto-draft default title. + if ( + ( ! edits.title || edits.title === 'Auto Draft' ) && + ! newEdits.title && + ( ! persistedRecord?.title || + persistedRecord?.title === 'Auto Draft' ) + ) { + newEdits.title = ''; + } + } + + return newEdits; +}; + /** * Returns the list of post type entities. * @@ -133,16 +196,8 @@ function* loadPostTypeEntities() { selectionEnd: true, }, mergedEdits: { meta: true }, - getTitle( record ) { - if ( [ 'wp_template_part', 'wp_template' ].includes( name ) ) { - return ( - record?.title?.rendered || - record?.title || - startCase( record.slug ) - ); - } - return record?.title?.rendered || record?.title || record.id; - }, + getTitle: getPostTypeTitle( name ), + __unstablePrePersist: getPostTypePrePersistHandler( name ), }; } ); } diff --git a/packages/core-data/src/test/entities.js b/packages/core-data/src/test/entities.js index e84e99ffb191f1..7d2bf1fecf161e 100644 --- a/packages/core-data/src/test/entities.js +++ b/packages/core-data/src/test/entities.js @@ -1,7 +1,13 @@ /** * Internal dependencies */ -import { getMethodName, defaultEntities, getKindEntities } from '../entities'; +import { + getMethodName, + defaultEntities, + getKindEntities, + getPostTypeTitle, + getPostTypePrePersistHandler, +} from '../entities'; import { addEntities } from '../actions'; describe( 'getMethodName', () => { @@ -79,3 +85,137 @@ describe( 'getKindEntities', () => { expect( end ).toEqual( { done: true, value: fetchedEntities } ); } ); } ); + +describe( 'getPostTypeTitle', () => { + it( 'should prefer the rendered value for titles for regular post types', async () => { + const record = { + id: 10, + title: { + rendered: 'My Title', + }, + }; + expect( getPostTypeTitle( 'post' )( record ) ).toBe( 'My Title' ); + } ); + + it( "should fallback to the title if it's a string", async () => { + const record = { + id: 10, + title: 'My Title', + }; + expect( getPostTypeTitle( 'post' )( record ) ).toBe( 'My Title' ); + } ); + + it( 'should fallback to the id if no title provided', async () => { + const record = { + id: 10, + }; + expect( getPostTypeTitle( 'post' )( record ) ).toBe( '10' ); + } ); + + it( 'should prefer the rendered value for titles for templates', async () => { + const record = { + slug: 'single', + title: { + rendered: 'My Template', + }, + }; + expect( getPostTypeTitle( 'wp_template' )( record ) ).toBe( + 'My Template' + ); + } ); + + it( "should fallback to the title if it's a string for templates", async () => { + const record = { + slug: 'single', + title: 'My Template', + }; + expect( getPostTypeTitle( 'wp_template' )( record ) ).toBe( + 'My Template' + ); + } ); + + it( 'should fallback to the slug if no title provided', async () => { + const record = { + slug: 'single', + }; + expect( getPostTypeTitle( 'wp_template' )( record ) ).toBe( 'Single' ); + } ); +} ); + +describe( 'getPostTypePrePersistHandler', () => { + it( 'set the status to draft and empty the title when saving auto-draft posts', () => { + let record = { + status: 'auto-draft', + }; + const edits = {}; + expect( + getPostTypePrePersistHandler( 'post' )( record, edits ) + ).toEqual( { + status: 'draft', + title: '', + } ); + + record = { + status: 'publish', + }; + expect( + getPostTypePrePersistHandler( 'post' )( record, edits ) + ).toEqual( {} ); + + record = { + status: 'auto-draft', + title: 'Auto Draft', + }; + expect( + getPostTypePrePersistHandler( 'post' )( record, edits ) + ).toEqual( { + status: 'draft', + title: '', + } ); + + record = { + status: 'publish', + title: 'My Title', + }; + expect( + getPostTypePrePersistHandler( 'post' )( record, edits ) + ).toEqual( {} ); + } ); + + it( 'should set the status of templates to publish and fix the title', () => { + let record = { + status: 'auto-draft', + slug: 'single', + }; + const edits = {}; + expect( + getPostTypePrePersistHandler( 'wp_template' )( record, edits ) + ).toEqual( { + status: 'publish', + title: 'Single', + } ); + + record = { + status: 'auto-draft', + }; + expect( + getPostTypePrePersistHandler( 'wp_template_part' )( record, edits ) + ).toEqual( { + status: 'publish', + title: '', + } ); + + record = { + status: 'auto-draft', + slug: 'single', + title: { + rendered: 'My title', + }, + }; + expect( + getPostTypePrePersistHandler( 'wp_template' )( record, edits ) + ).toEqual( { + status: 'publish', + } ); + } ); +} ); diff --git a/packages/edit-post/src/components/header/template-save-button/index.js b/packages/edit-post/src/components/header/template-save-button/index.js index 57c660d65ef167..48089eb55df50d 100644 --- a/packages/edit-post/src/components/header/template-save-button/index.js +++ b/packages/edit-post/src/components/header/template-save-button/index.js @@ -1,12 +1,11 @@ /** * WordPress dependencies */ -import { EntitiesSavedStates, store as editorStore } from '@wordpress/editor'; +import { EntitiesSavedStates } from '@wordpress/editor'; import { Button } from '@wordpress/components'; -import { store as coreStore } from '@wordpress/core-data'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -20,18 +19,6 @@ function TemplateSaveButton() { setIsEntitiesReviewPanelOpen, ] = useState( false ); const { setIsEditingTemplate } = useDispatch( editPostStore ); - const { editEntityRecord } = useDispatch( coreStore ); - const { getTemplateInfo, getEditedEntityRecord } = useSelect( - ( select ) => { - return { - getTemplateInfo: select( editorStore ) - .__experimentalGetTemplateInfo, - getEditedEntityRecord: select( coreStore ) - .getEditedEntityRecord, - }; - }, - [] - ); return ( <>