Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update entity saved states to be selected by default and partitioned by type. #21159

Merged
merged 28 commits into from
Apr 2, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e327d5e
filter ignoredForSave out of dirtyEntityRecords
Addison-Stavlo Mar 25, 2020
81010ad
rename unsavedEntityRecords, too confusing and non-representative
Addison-Stavlo Mar 25, 2020
548a3d1
removed temporary commenting
Addison-Stavlo Mar 25, 2020
55e1330
partitioned entities by type
Addison-Stavlo Mar 25, 2020
085025a
renaming
Addison-Stavlo Mar 25, 2020
8df4aae
updated h# for entity type
Addison-Stavlo Mar 26, 2020
e555fe0
updated title to show Untitled if not present
Addison-Stavlo Mar 26, 2020
b615acd
changed prop names for interior component
Addison-Stavlo Mar 26, 2020
461a008
removed ignoredForSave prop
Addison-Stavlo Mar 27, 2020
0bafed2
started e2e test
Addison-Stavlo Mar 31, 2020
e79de94
inserted new template
Addison-Stavlo Mar 31, 2020
e397ccb
added empty string default for slug/theme name
Addison-Stavlo Mar 31, 2020
87889a5
tests for button dot state added
Addison-Stavlo Mar 31, 2020
9dceb5d
refactored test names/describes
Addison-Stavlo Mar 31, 2020
166ccc4
fixed failing test after openning button
Addison-Stavlo Mar 31, 2020
18771c6
started published checks
Addison-Stavlo Mar 31, 2020
dd35a6f
refactored reusable assertions
Addison-Stavlo Mar 31, 2020
f60e1fd
fixed last post editor test
Addison-Stavlo Mar 31, 2020
0d89553
fixed useCallback warning
Addison-Stavlo Mar 31, 2020
012702d
updated save button classname
Addison-Stavlo Apr 1, 2020
380c292
renamed test file
Addison-Stavlo Apr 1, 2020
c4c7f7d
refactored selectors etc.
Addison-Stavlo Apr 1, 2020
6cfd317
fixed undefined key in site editor
Addison-Stavlo Apr 1, 2020
18447e2
removed comment
Addison-Stavlo Apr 1, 2020
fd4da45
moved test file
Addison-Stavlo Apr 1, 2020
7c52693
added title to dirty entity selector
Addison-Stavlo Apr 1, 2020
20a4606
added getTitle to site entity
Addison-Stavlo Apr 1, 2020
9bac85a
Skip site editor tests
youknowriad Apr 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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( '' );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was necessary to edit these to stop a react warning from breaking the e2e tests. Without initializing as an empty string, react would complain about turning an uncontrolled component into a controlled component once the input was used.

const [ help, setHelp ] = useState();

// Try to find an existing template part.
Expand Down
179 changes: 179 additions & 0 deletions packages/e2e-tests/specs/editor/various/multi-entity-saving.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* 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' );

// Button should not have has-changes-dot class.
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( '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 ), [] );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were also necessary to edit due to reacts warnings breaking e2e tests. Without this change, react would throw a warning when the button was used about useState update functions not supporting the second callback argument.

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 }
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
margin-left: auto;
margin-right: 0;
}
.editor-entities-saved-states__entity-type-list {
h2 {
font-size: 18px;
margin: 20px 0 10px;
}
}
Loading