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

Multi-entity save UI: Add Discard Changes panel #36185

Draft
wants to merge 25 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,76 @@ export const __experimentalSaveSpecifiedEntityEdits = (
return await dispatch.saveEntityRecord( kind, name, editsToSave, options );
};

/**
* Action triggered to reset an entity record's edits.
*
* @param {string} kind Kind of the entity.
* @param {string} name Name of the entity.
* @param {Object} recordId ID of the record.
*/
export const __experimentalResetEditedEntityRecord = (
kind,
name,
recordId
) => async ( { select, dispatch } ) => {
if ( ! select.hasEditsForEntityRecord( kind, name, recordId ) ) {
return;
}
const edits = select.getEntityRecordEdits( kind, name, recordId );
const editsToDiscard = {};
for ( const edit in edits ) {
editsToDiscard[ edit ] = undefined;
}
return await dispatch( {
type: 'EDIT_ENTITY_RECORD',
kind,
name,
recordId,
edits: editsToDiscard,
transientEdits: {},
meta: { undo: undefined },
} );
};

/**
* Action triggered to reset 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} itemsToReset List of entity properties to reset.
*/
export const __experimentalResetSpecifiedEntityEdits = (
kind,
name,
recordId,
itemsToReset
) => async ( { select, dispatch } ) => {
if ( ! select.hasEditsForEntityRecord( kind, name, recordId ) ) {
return;
}
const edits = select.getEntityRecordNonTransientEdits(
kind,
name,
recordId
);
const editsToDiscard = {};
for ( const edit in edits ) {
if ( itemsToReset.some( ( item ) => item === edit ) ) {
editsToDiscard[ edit ] = undefined;
}
}
return await dispatch( {
type: 'EDIT_ENTITY_RECORD',
kind,
name,
recordId,
edits: editsToDiscard,
transientEdits: {},
meta: { undo: undefined }, // Don't add this to the undo stack.
} );
};

/**
* Returns an action object used in signalling that Upload permissions have been received.
*
Expand Down
64 changes: 64 additions & 0 deletions packages/core-data/src/test/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
receiveAutosaves,
receiveCurrentUser,
__experimentalBatch,
__experimentalResetEditedEntityRecord,
__experimentalResetSpecifiedEntityEdits,
} from '../actions';

jest.mock( '../batch', () => {
Expand Down Expand Up @@ -196,6 +198,68 @@ describe( 'saveEditedEntityRecord', () => {
} );
} );

describe( '__experimentalResetEditedEntityRecord', () => {
it( "triggers an EDIT_ENTITY_RECORD action to set all the selected entity record's edits to undefined", async () => {
const select = {
getEntityRecordEdits: () => ( { description: {}, title: {} } ),
hasEditsForEntityRecord: () => true,
};

const dispatch = Object.assign( jest.fn() );

await __experimentalResetEditedEntityRecord(
'root',
'site',
undefined
)( { dispatch, select } );

expect( dispatch ).toHaveBeenCalledTimes( 1 );
expect( dispatch ).toHaveBeenCalledWith( {
type: 'EDIT_ENTITY_RECORD',
kind: 'root',
name: 'site',
recordId: undefined,
edits: { description: undefined, title: undefined },
transientEdits: {},
meta: { undo: undefined },
} );
} );
} );

describe( '__experimentalResetSpecifiedEntityEdits', () => {
it( 'triggers an EDIT_ENTITY_RECORD action to set the selected entity record edits to undefined', async () => {
const itemsToDiscard = [ 'title' ];

const select = {
getEntityRecordNonTransientEdits: () => ( {
title: {},
description: {},
} ),
hasEditsForEntityRecord: () => true,
};

const dispatch = Object.assign( jest.fn() );

await __experimentalResetSpecifiedEntityEdits(
'root',
'site',
undefined,
itemsToDiscard
)( { dispatch, select } );

expect( dispatch ).toHaveBeenCalledTimes( 1 );
expect( dispatch ).toHaveBeenCalledWith( {
type: 'EDIT_ENTITY_RECORD',
kind: 'root',
name: 'site',
recordId: undefined,
edits: { title: undefined },
transientEdits: {},
meta: { undo: undefined },
} );
} );
} );

describe( 'saveEntityRecord', () => {
beforeEach( async () => {
apiFetch.mockReset();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* External dependencies
*/
import { some } from 'lodash';

/**
* WordPress dependencies
*/
import { Button, PanelBody, PanelRow } from '@wordpress/components';
import { __, _n } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { Fragment, useState } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import EntityRecordItem from './entity-record-item';

export default function DiscardEntityChangesPanel( { closePanel, savables } ) {
const isSaving = useSelect( ( select ) =>
savables.some( ( { kind, name, key } ) =>
select( coreStore ).isSavingEntityRecord( kind, name, key )
)
);

const {
__experimentalResetEditedEntityRecord: resetEditedEntityRecord,
__experimentalResetSpecifiedEntityEdits: resetSpecifiedEntityEdits,
} = useDispatch( coreStore );

const { createSuccessNotice } = useDispatch( noticesStore );

// Selected entities to be discarded.
const [ selectedEntities, _setSelectedEntities ] = useState( [] );

const setSelectedEntities = ( { kind, name, key, property }, checked ) => {
if ( ! checked ) {
_setSelectedEntities(
selectedEntities.filter(
( elt ) =>
elt.kind !== kind ||
elt.name !== name ||
elt.key !== key ||
elt.property !== property
)
);
} else {
_setSelectedEntities( [
...selectedEntities,
{ kind, name, key, property },
] );
}
};

const discardCheckedEntities = () => {
closePanel();

const numberOfSelectedEntities = selectedEntities.length;

const siteItemsToDiscard = [];
selectedEntities.forEach( ( { kind, name, key, property } ) => {
if ( 'root' === kind && 'site' === name ) {
siteItemsToDiscard.push( property );
} else {
resetEditedEntityRecord( kind, name, key );
}
} );
resetSpecifiedEntityEdits(
'root',
'site',
undefined,
siteItemsToDiscard
);

if ( numberOfSelectedEntities === savables.length ) {
createSuccessNotice( __( 'All changes discarded.' ), {
type: 'snackbar',
} );
} else {
createSuccessNotice(
_n(
'Change discarded.',
'Some changes discarded.',
numberOfSelectedEntities
),
{
type: 'snackbar',
}
);
}
};

return (
<Fragment>
<div>
<div className="entities-saved-states__text-prompt">
<strong>{ __( 'Changes saved!' ) }</strong>
</div>
<div className="entities-saved-states__text-prompt">
<strong>{ __( "What's next?" ) }</strong>
<p>
{ __(
'Your template still has some unsaved changes.'
) }
</p>
<p>
{ __(
'You can select and discard them now, or close the panel and deal with them later.'
) }
</p>
</div>

<PanelBody initialOpen={ true }>
{ isSaving && (
<ul className="entities-saved-states__discard-changes__saving">
<li className="entities-saved-states__discard-changes-item">
&#8203;
</li>
<li className="entities-saved-states__discard-changes-item">
&#8203;
</li>
<li className="entities-saved-states__discard-changes-item">
&#8203;
</li>
</ul>
) }
{ ! isSaving &&
savables.map( ( record ) => (
<EntityRecordItem
key={ record.key || record.property }
record={ record }
checked={ some(
selectedEntities,
( elt ) =>
elt.kind === record.kind &&
elt.name === record.name &&
elt.key === record.key &&
elt.property === record.property
) }
onChange={ ( value ) =>
setSelectedEntities( record, value )
}
/>
) ) }
<PanelRow>
<Button
disabled={
selectedEntities.length === 0 || isSaving
}
isDestructive
onClick={ discardCheckedEntities }
>
{ __( 'Discard changes' ) }
</Button>
</PanelRow>
</PanelBody>
</div>
</Fragment>
);
}
Loading