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

Add Reusable Block deletion #4139

Merged
merged 9 commits into from
Jan 4, 2018
Merged

Add Reusable Block deletion #4139

merged 9 commits into from
Jan 4, 2018

Conversation

noisysocks
Copy link
Member

@noisysocks noisysocks commented Dec 22, 2017

✨ What this is

Special 2-for-1 offer: closes #3792 and closes #4041.

Note that I've set add/reusable-block-deletion-to-api as the base branch so that the diff is bearable. The intention is to merge this into master after #4137 lands.

🗑 Adds the ability to delete a Reusable Block

This PR uses the new API introduced in #4137 to add support for deleting Reusable Blocks.

With the new API, this is pretty straightforward:

  1. There's a new DELETE_REUSABLE_BLOCK effect which calls the destroy() method that the WP API Backbone library gives us for free.
  2. This effect optimistically calls REMOVE_REUSABLE_BLOCK which removes the Reusable Block and any blocks that reference it from local editor state. If the delete fails, REMOVE_REUSABLE_BLOCK is reverted.

💆‍♀️ Improves the Reusable Block flow

I implemented the changes suggested by @karmatosed in #4041 by moving 'Detach Reusable Block' into a seperate settings menu section alongside our new 'Delete Reusable Block' button.

Here's how the settings menu now looks for a static block:

screen shot 2017-12-21 at 12 29 19

And here's how it now looks for a reusable block:

screen shot 2017-12-21 at 12 29 46

📋 How to test

  1. Create a block
  2. Convert it to a reusable block
  3. Rename the reusable block
  4. Edit the reusable block
  5. Use the Inserter to insert a copy of the reusable block
  6. Remove a block
  7. Delete the reusable block

@noisysocks noisysocks added [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) [Type] Enhancement A suggestion for improvement. labels Dec 22, 2017
@noisysocks
Copy link
Member Author

cc. @youknowriad @aduth @pento since they know this code
cc. @karmatosed @jasmussen for UX feedback

@noisysocks noisysocks changed the base branch from add/reusable-block-deletion-to-api to master December 27, 2017 23:19
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

An edge case, but one that could be potentially destructive: We allow the user to delete a reusable block while it's still in its "temporary" unsaved state, meaning that it will use the value produced by uniqueId. Since a block could exist with an ID matching the temporary ID, the user could inadvertently delete the wrong reusable block.

Luckily I didn't have a block with ID 2:

image

I'd suggest:

  • Prevent deleting reusable block if it hasn't been saved yet.
  • Maybe pass the argument to uniqueId, which acts as a prefix. Arguably this shouldn't be necessary, but eliminates any chance that the temproary ID overlaps with a real block ID.

onDelete( id ) {
// TODO: Make this a <Confirm /> component or similar
// eslint-disable-next-line no-alert
if ( window.confirm( __( 'Are you sure you want to permanently delete this Reusable Block?' ) ) ) {
Copy link
Member

Choose a reason for hiding this comment

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

You might be interested in a related discussion on "Scariness" of such prompts:

Automattic/wp-calypso#10982

In other words, should we be proactive about messaging here to clarify that deleting a block can impact other posts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea! Made the copy scarier in 6c0e185.

case 'FETCH_REUSABLE_BLOCKS_FAILURE': {
const { id } = action;
if ( ! id ) {
return state;
Copy link
Member

Choose a reason for hiding this comment

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

In what situations would you expect this code path to be hit? Even if it was falsey, would there be harm in allowing omit to proceed? Aside from returning a new reference: But there's also an issue that if id is truthy but doesn't exist in the current set, we still return a new reference.

omit( state, 'some-id-that-doesnt-exist' ) !== state

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right, this check isn't necessary. I've removed it in 92b6a18.

@@ -471,6 +474,8 @@ describe( 'effects', () => {

describe( 'reusable block effects', () => {
beforeAll( () => {
lodash.uniqueId = jest.spyOn( lodash, 'uniqueId' );
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we could achieve this more concisely with a combination of jest.mock and require.requireActual.

But then, looking at how you're asserting on optimist.id, do we really need this at all? Since you're using expect.any( Object ), and not asserting against the actual value produced by uniqueId.

Copy link
Member Author

@noisysocks noisysocks Jan 4, 2018

Choose a reason for hiding this comment

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

You're right, mocking this isn't giving us much benefit considering the effort involved. I've removed the mock by making our assertions less strict in 78a9bb7.


const allBlocks = getBlocks( getState() );
const associatedBlocks = allBlocks.filter( block => isReusableBlock( block ) && block.attributes.ref === id );
const associatedBlockUids = associatedBlocks.map( block => block.uid );
Copy link
Contributor

Choose a reason for hiding this comment

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

Might a reducer here (i.e. allBlocks.reduce...) help avoid passing through the blocks list twice? Or is that a heavier operation? Just a tiny thought on optimisation I guess :-)

Copy link
Member Author

@noisysocks noisysocks Jan 4, 2018

Choose a reason for hiding this comment

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

You're absolutely right that using reduce would likely be faster!

Still, I prefer to optimise for readability over performance unless it's an operation that's performance sensitive. Deleting a reusable block is a fairly uncommon operation, so I'm not too worried.

Adds an effect (DELETE_REUSABLE_BLOCK) which triggers deletes a reusable
block via the API. The action that triggers this effect can be created
with the deleteReusableBlock() action creator.

The reusable block is optimistically removed from local redux state by
the reducer handling the REMOVE_REUSABLE_BLOCK action.
Moves 'Detach from Reusable Block' and adds 'Delete Reusable Block'
buttons to a <ReusableBlockSettings> component that lives inside
<BlockSettingsMenu>.
This helps reduce the ambiguity between removing a block and deleting a
Reusable Block.
Makes the REMOVE_REUSABLE_BLOCK action also remove any blocks that are
using the removed Reusable Block from the editor state.
Don't allow DELETE_REUSABLE_BLOCK to delete a reusable block that is
marked temporary.
There's no need to check for the existence of `id` before `omit`ing it.
If `id` is falsey then `omit` will do nothing.
We can avoid mocking `uniqueId` by being less pedantic about what our
tests expect.
@noisysocks
Copy link
Member Author

Thanks for reviewing, @aduth! I've addressed your comments and made it so that reusable blocks with temporary IDs cannot be deleted in 1f9e5c2.

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Nice work 👍

Question: Should the deleted block be removed from state.preferences.blockUsage as well? I've noticed there's some issues with Reusable Blocks in recent and frequently used blocks, so wasn't sure if you planned to address this separately. I'd guess that the reusable block of state.preferences.blockUsage is not storing the ref anyways, so not tracking details of the block being removed.

@noisysocks
Copy link
Member Author

Question: Should the deleted block be removed from state.preferences.blockUsage as well?

That's a good point. I'll do this as part of #4224 since, as you say, we aren't currently tracking reusable blocks in the frequent/recent list of blocks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggested improvements to reusable block flow Reusable Blocks: Add ability to delete a Reusable Block
3 participants