Skip to content

Commit

Permalink
Update entity saved states to be selected by default and partitioned …
Browse files Browse the repository at this point in the history
…by type. (#21159)

Co-authored-by: Riad Benguella <[email protected]>
  • Loading branch information
Addison-Stavlo and youknowriad authored Apr 2, 2020
1 parent 5da175d commit e82e819
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 66 deletions.
4 changes: 2 additions & 2 deletions packages/block-library/src/template-part/edit/placeholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ function TemplatePartPreview() {
}

export default function TemplatePartPlaceholder( { setAttributes } ) {
const [ slug, _setSlug ] = useState();
const [ theme, setTheme ] = useState();
const [ slug, _setSlug ] = useState( '' );
const [ theme, setTheme ] = useState( '' );
const [ help, setHelp ] = useState();

// Try to find an existing template part.
Expand Down
3 changes: 3 additions & 0 deletions packages/core-data/src/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const defaultEntities = [
name: 'site',
kind: 'root',
baseURL: '/wp/v2/settings',
getTitle: ( record ) => {
return get( record, [ 'title' ], __( 'Site Title' ) );
},
},
{
label: __( 'Post Type' ),
Expand Down
178 changes: 178 additions & 0 deletions packages/e2e-tests/specs/experiments/multi-entity-saving.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* WordPress dependencies
*/
import {
createNewPost,
insertBlock,
disablePrePublishChecks,
publishPostWithPrePublishChecksDisabled,
visitAdminPage,
} from '@wordpress/e2e-test-utils';
import { addQueryArgs } from '@wordpress/url';

/**
* Internal dependencies
*/
import {
enableExperimentalFeatures,
disableExperimentalFeatures,
} from '../../experimental-features';

describe( 'Multi-entity save flow', () => {
// Selectors.
const checkboxSelector = '.components-checkbox-control__checked';
const templatePartSelector = '*[data-type="core/template-part"]';
const saveButtonSelector = '.editor-post-publish-button__button';
const multisaveSelector =
'.editor-post-publish-button__button.has-changes-dot';
const entitiesSaveSelector = '.editor-entities-saved-states__save-button';
const saveSiteSelector = '.edit-site-save-button__button';

// Setup & Teardown.
const requiredExperiments = [
'#gutenberg-full-site-editing',
'#gutenberg-full-site-editing-demo',
];
beforeAll( async () => {
await enableExperimentalFeatures( requiredExperiments );
} );
afterAll( async () => {
await disableExperimentalFeatures( requiredExperiments );
} );

describe( 'Post Editor', () => {
const assertMultiSaveEnabled = async () => {
const multiSaveButton = await page.waitForSelector(
multisaveSelector
);
expect( multiSaveButton ).not.toBeNull();
};

const assertMultiSaveDisabled = async () => {
const multiSaveButton = await page.$( multisaveSelector );
expect( multiSaveButton ).toBeNull();
};

describe( 'Pre-Publish state', () => {
it( 'Should not trigger multi-entity save button with only post edited', async () => {
await createNewPost();
await disablePrePublishChecks();
// Edit the page some.
await page.click( '.editor-post-title' );
await page.keyboard.type( 'Test Post...' );
await page.keyboard.press( 'Enter' );

await assertMultiSaveDisabled();
} );

it( 'Should trigger multi-entity save button once template part edited', async () => {
// Create new template part.
await insertBlock( 'Template Part' );
await page.keyboard.type( 'test-template-part' );
await page.keyboard.press( 'Tab' );
await page.keyboard.type( 'test-theme' );
await page.keyboard.press( 'Tab' );
await page.keyboard.press( 'Enter' );

// Make some changes in new Template Part.
await page.waitForSelector(
`${ templatePartSelector } .block-editor-inner-blocks`
);
await page.click( templatePartSelector );
await page.keyboard.type( 'some words...' );

await assertMultiSaveEnabled();
} );

it( 'Clicking should open modal with boxes checked by default', async () => {
await page.click( saveButtonSelector );
const checkedBoxes = await page.$$( checkboxSelector );
expect( checkedBoxes.length ).toBe( 2 );
} );

it( 'Saving should result in items being saved', async () => {
await page.click( entitiesSaveSelector );

// Verify post is saved.
const draftSaved = await page.waitForSelector(
'.editor-post-saved-state.is-saved'
);
expect( draftSaved ).not.toBeNull();

// Verify template part is saved.
await assertMultiSaveDisabled();
} );
} );

describe( 'Published state', () => {
it( 'Update button disabled after publish', async () => {
await publishPostWithPrePublishChecksDisabled();
const disabledSaveButton = await page.$(
`${ saveButtonSelector }[aria-disabled=true]`
);
expect( disabledSaveButton ).not.toBeNull();
} );

it( 'Update button enabled after editing post', async () => {
await page.click( '.editor-post-title' );
await page.keyboard.type( '...more title!' );

// Verify update button is enabled.
const enabledSaveButton = await page.$(
`${ saveButtonSelector }[aria-disabled=false]`
);
expect( enabledSaveButton ).not.toBeNull();

// Verify is not for multi-entity saving.
await assertMultiSaveDisabled();
} );

it( 'Multi-save button triggered after editing template part.', async () => {
await page.click( templatePartSelector );
await page.keyboard.type( '...some more words...' );
await page.keyboard.press( 'Enter' );
await assertMultiSaveEnabled();
} );
} );
} );

describe.skip( 'Site Editor', () => {
async function assertSaveDisabled() {
const disabledButton = await page.waitForSelector(
`${ saveSiteSelector }[aria-disabled=true]`
);
expect( disabledButton ).not.toBeNull();
}
const activeButtonSelector = `${ saveSiteSelector }[aria-disabled=false]`;

it( 'Save button should be disabled by default', async () => {
// Navigate to site editor.
const query = addQueryArgs( '', {
page: 'gutenberg-edit-site',
} ).slice( 1 );
await visitAdminPage( 'admin.php', query );

await assertSaveDisabled();
} );

it( 'Should be enabled after edits', async () => {
await page.click( templatePartSelector );
await page.keyboard.type( 'some words...' );
const enabledButton = await page.waitForSelector(
activeButtonSelector
);
expect( enabledButton ).not.toBeNull();
} );

it( 'Clicking button should open modal with boxes checked', async () => {
await page.click( activeButtonSelector );
const checkedBoxes = await page.$$( checkboxSelector );
expect( checkedBoxes ).not.toHaveLength( 0 );
} );

it( 'Saving should result in items being saved', async () => {
await page.click( entitiesSaveSelector );
await assertSaveDisabled();
} );
} );
} );
5 changes: 3 additions & 2 deletions packages/edit-site/src/components/save-button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ export default function SaveButton() {
const disabled = ! isDirty || isSaving;

const [ isOpen, setIsOpen ] = useState( false );
const open = useCallback( setIsOpen.bind( null, true ), [] );
const close = useCallback( setIsOpen.bind( null, false ), [] );
const open = useCallback( () => setIsOpen( true ), [] );
const close = useCallback( () => setIsOpen( false ), [] );
return (
<>
<Button
isPrimary
className="edit-site-save-button__button"
aria-disabled={ disabled }
disabled={ disabled }
isBusy={ isSaving }
Expand Down
111 changes: 62 additions & 49 deletions packages/editor/src/components/entities-saved-states/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { some } from 'lodash';
import { some, groupBy } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -12,66 +12,88 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';

function EntityRecordState( { record, checked, onChange } ) {
const entity = useSelect(
( select ) => select( 'core' ).getEntity( record.kind, record.name ),
[ record.kind, record.name ]
);

return (
<CheckboxControl
label={
<>
{ entity.label }
{ !! record.title && (
<>
{ ': ' }
<strong>
{ record.title || __( 'Untitled' ) }
</strong>
</>
) }
</>
}
checked={ ! checked }
label={ <strong>{ record.title || __( 'Untitled' ) }</strong> }
checked={ checked }
onChange={ onChange }
/>
);
}

export default function EntitiesSavedStates( {
isOpen,
onRequestClose,
ignoredForSave = [],
} ) {
function EntityTypeList( { list, unselectedEntities, setUnselectedEntities } ) {
const firstRecord = list[ 0 ];
const entity = useSelect(
( select ) =>
select( 'core' ).getEntity( firstRecord.kind, firstRecord.name ),
[ firstRecord.kind, firstRecord.name ]
);

return (
<div className="editor-entities-saved-states__entity-type-list">
<h2>{ entity.label }</h2>
{ list.map( ( record ) => {
return (
<EntityRecordState
key={ record.key || 'site' }
record={ record }
checked={
! some(
unselectedEntities,
( elt ) =>
elt.kind === record.kind &&
elt.name === record.name &&
elt.key === record.key
)
}
onChange={ ( value ) =>
setUnselectedEntities( record, value )
}
/>
);
} ) }
</div>
);
}

export default function EntitiesSavedStates( { isOpen, onRequestClose } ) {
const dirtyEntityRecords = useSelect(
( select ) => select( 'core' ).__experimentalGetDirtyEntityRecords(),
[]
);
const { saveEditedEntityRecord } = useDispatch( 'core' );

const [ unsavedEntityRecords, _setUnsavedEntityRecords ] = useState( [] );
const setUnsavedEntityRecords = ( { kind, name, key }, checked ) => {
// To group entities by type.
const partitionedSavables = Object.values(
groupBy( dirtyEntityRecords, 'name' )
);

// Unchecked entities to be ignored by save function.
const [ unselectedEntities, _setUnselectedEntities ] = useState( [] );

const setUnselectedEntities = ( { kind, name, key }, checked ) => {
if ( checked ) {
_setUnsavedEntityRecords(
unsavedEntityRecords.filter(
_setUnselectedEntities(
unselectedEntities.filter(
( elt ) =>
elt.kind !== kind ||
elt.name !== name ||
elt.key !== key
)
);
} else {
_setUnsavedEntityRecords( [
...unsavedEntityRecords,
_setUnselectedEntities( [
...unselectedEntities,
{ kind, name, key },
] );
}
};

const saveCheckedEntities = () => {
const entitiesToSave = dirtyEntityRecords.filter(
( { kind, name, key } ) => {
return ! some(
ignoredForSave.concat( unsavedEntityRecords ),
unselectedEntities,
( elt ) =>
elt.kind === kind &&
elt.name === name &&
Expand All @@ -86,30 +108,21 @@ export default function EntitiesSavedStates( {

onRequestClose( entitiesToSave );
};

return (
isOpen && (
<Modal
title={ __( 'What do you want to save?' ) }
onRequestClose={ () => onRequestClose() }
contentLabel={ __( 'Select items to save.' ) }
>
{ dirtyEntityRecords.map( ( record ) => {
{ partitionedSavables.map( ( list ) => {
return (
<EntityRecordState
key={ record.key }
record={ record }
checked={
! some(
unsavedEntityRecords,
( elt ) =>
elt.kind === record.kind &&
elt.name === record.name &&
elt.key === record.key
)
}
onChange={ ( value ) =>
setUnsavedEntityRecords( record, value )
}
<EntityTypeList
key={ list[ 0 ].name }
list={ list }
unselectedEntities={ unselectedEntities }
setUnselectedEntities={ setUnselectedEntities }
/>
);
} ) }
Expand All @@ -118,7 +131,7 @@ export default function EntitiesSavedStates( {
isPrimary
disabled={
dirtyEntityRecords.length -
unsavedEntityRecords.length ===
unselectedEntities.length ===
0
}
onClick={ saveCheckedEntities }
Expand Down
Loading

0 comments on commit e82e819

Please sign in to comment.