From f4b3c729cb836cff081d3af096b9ac340640d484 Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Fri, 23 Apr 2021 09:42:49 -0400 Subject: [PATCH] Save panel - decouple Site Entity items for individual saving. (#30816) * decouple edited site props in checkbox list * move to original useSelect to update dirtyEntityRecords * modularize site entity saving * make new action not site entity specific * add basic e2e test * try waitForXPath before typing --- packages/core-data/src/actions.js | 43 ++++++++++++ .../experiments/multi-entity-saving.test.js | 28 ++++++++ .../entities-saved-states/entity-type-list.js | 8 ++- .../components/entities-saved-states/index.js | 70 ++++++++++++++++--- 4 files changed, 135 insertions(+), 14 deletions(-) diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index c7454f51e28f79..8bf6d40b8c5c74 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -680,6 +680,49 @@ export function* saveEditedEntityRecord( kind, name, recordId, options ) { return yield* saveEntityRecord( kind, name, record, options ); } +/** + * Action triggered to save only specified properties for the entity. + * + * @param {string} kind Kind of the entity. + * @param {string} name Name of the entity. + * @param {Object} recordId ID of the record. + * @param {Array} itemsToSave List of entity properties to save. + * @param {Object} options Saving options. + */ +export function* __experimentalSaveSpecifiedEntityEdits( + kind, + name, + recordId, + itemsToSave, + options +) { + if ( + ! ( yield controls.select( + 'core', + 'hasEditsForEntityRecord', + kind, + name, + recordId + ) ) + ) { + return; + } + const edits = yield controls.select( + 'core', + 'getEntityRecordNonTransientEdits', + kind, + name, + recordId + ); + const editsToSave = {}; + for ( const edit in edits ) { + if ( itemsToSave.some( ( item ) => item === edit ) ) { + editsToSave[ edit ] = edits[ edit ]; + } + } + return yield* saveEntityRecord( kind, name, editsToSave, options ); +} + /** * Returns an action object used in signalling that Upload permissions have been received. * diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js index 0380184033a113..d5643c341b1711 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js @@ -3,6 +3,7 @@ */ import { createNewPost, + disablePrePublishChecks, insertBlock, publishPost, trashAllPosts, @@ -168,6 +169,33 @@ describe( 'Multi-entity save flow', () => { await assertMultiSaveEnabled(); await assertExistance( saveA11ySelector, true ); } ); + + it( 'Site blocks should save individually', async () => { + await createNewPost(); + await disablePrePublishChecks(); + + await insertBlock( 'Site Title' ); + await page.waitForXPath( '//a[contains(text(), "gutenberg")]' ); // Ensure title is retrieved before typing. + await page.keyboard.type( '...' ); + await insertBlock( 'Site Tagline' ); + await page.waitForXPath( + '//p[contains(text(), "Just another WordPress site")]' + ); // Esnure tagline is retrieved before typing. + await page.keyboard.type( '...' ); + + await page.click( savePostSelector ); + await page.waitForSelector( savePanelSelector ); + let checkboxInputs = await page.$$( checkboxInputSelector ); + expect( checkboxInputs ).toHaveLength( 3 ); + + await checkboxInputs[ 1 ].click(); + await page.click( entitiesSaveSelector ); + + await page.click( savePostSelector ); + await page.waitForSelector( savePanelSelector ); + checkboxInputs = await page.$$( checkboxInputSelector ); + expect( checkboxInputs ).toHaveLength( 1 ); + } ); } ); describe( 'Site Editor', () => { diff --git a/packages/editor/src/components/entities-saved-states/entity-type-list.js b/packages/editor/src/components/entities-saved-states/entity-type-list.js index a022e324928115..fc8d9922beca11 100644 --- a/packages/editor/src/components/entities-saved-states/entity-type-list.js +++ b/packages/editor/src/components/entities-saved-states/entity-type-list.js @@ -9,6 +9,7 @@ import { some } from 'lodash'; import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; import { page, layout } from '@wordpress/icons'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -29,7 +30,7 @@ export default function EntityTypeList( { const firstRecord = list[ 0 ]; const entity = useSelect( ( select ) => - select( 'core' ).getEntity( firstRecord.kind, firstRecord.name ), + select( coreStore ).getEntity( firstRecord.kind, firstRecord.name ), [ firstRecord.kind, firstRecord.name ] ); @@ -42,7 +43,7 @@ export default function EntityTypeList( { { list.map( ( record ) => { return ( elt.kind === record.kind && elt.name === record.name && - elt.key === record.key + elt.key === record.key && + elt.property === record.property ) } onChange={ ( value ) => diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 5e4a334e9db36f..fee90b62272a48 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -18,6 +18,14 @@ import { store as coreStore } from '@wordpress/core-data'; */ import EntityTypeList from './entity-type-list'; +const TRANSLATED_SITE_PROTPERTIES = { + title: __( 'Title' ), + description: __( 'Tagline' ), + sitelogo: __( 'Logo' ), + show_on_front: __( 'Show on front' ), + page_on_front: __( 'Page on front' ), +}; + function EntitiesSavedStates( { isOpen, close } ) { const saveButtonRef = useRef(); useEffect( () => { @@ -27,13 +35,42 @@ function EntitiesSavedStates( { isOpen, close } ) { } }, [ isOpen ] ); const { dirtyEntityRecords } = useSelect( ( select ) => { + const dirtyRecords = select( + coreStore + ).__experimentalGetDirtyEntityRecords(); + + // Remove site object and decouple into its edited pieces. + const dirtyRecordsWithoutSite = dirtyRecords.filter( + ( record ) => ! ( record.kind === 'root' && record.name === 'site' ) + ); + + const siteEdits = select( coreStore ).getEntityRecordEdits( + 'root', + 'site' + ); + + const siteEditsAsEntities = []; + for ( const property in siteEdits ) { + siteEditsAsEntities.push( { + kind: 'root', + name: 'site', + title: TRANSLATED_SITE_PROTPERTIES[ property ] || property, + property, + } ); + } + const dirtyRecordsWithSiteItems = [ + ...dirtyRecordsWithoutSite, + ...siteEditsAsEntities, + ]; + return { - dirtyEntityRecords: select( - coreStore - ).__experimentalGetDirtyEntityRecords(), + dirtyEntityRecords: dirtyRecordsWithSiteItems, }; }, [] ); - const { saveEditedEntityRecord } = useDispatch( coreStore ); + const { + saveEditedEntityRecord, + __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits, + } = useDispatch( coreStore ); // To group entities by type. const partitionedSavables = Object.values( @@ -43,42 +80,53 @@ function EntitiesSavedStates( { isOpen, close } ) { // Unchecked entities to be ignored by save function. const [ unselectedEntities, _setUnselectedEntities ] = useState( [] ); - const setUnselectedEntities = ( { kind, name, key }, checked ) => { + const setUnselectedEntities = ( + { kind, name, key, property }, + checked + ) => { if ( checked ) { _setUnselectedEntities( unselectedEntities.filter( ( elt ) => elt.kind !== kind || elt.name !== name || - elt.key !== key + elt.key !== key || + elt.property !== property ) ); } else { _setUnselectedEntities( [ ...unselectedEntities, - { kind, name, key }, + { kind, name, key, property }, ] ); } }; const saveCheckedEntities = () => { const entitiesToSave = dirtyEntityRecords.filter( - ( { kind, name, key } ) => { + ( { kind, name, key, property } ) => { return ! some( unselectedEntities, ( elt ) => elt.kind === kind && elt.name === name && - elt.key === key + elt.key === key && + elt.property === property ); } ); close( entitiesToSave ); - entitiesToSave.forEach( ( { kind, name, key } ) => { - saveEditedEntityRecord( kind, name, key ); + const siteItemsToSave = []; + entitiesToSave.forEach( ( { kind, name, key, property } ) => { + if ( 'root' === kind && 'site' === name ) { + siteItemsToSave.push( property ); + } else { + saveEditedEntityRecord( kind, name, key ); + } } ); + saveSpecifiedEntityEdits( 'root', 'site', undefined, siteItemsToSave ); }; // Explicitly define this with no argument passed. Using `close` on