Skip to content
Closed
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