-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Conversation
cc. @youknowriad @aduth @pento since they know this code |
There was a problem hiding this 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:
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?' ) ) ) { |
There was a problem hiding this comment.
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:
In other words, should we be proactive about messaging here to clarify that deleting a block can impact other posts?
There was a problem hiding this comment.
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.
editor/store/reducer.js
Outdated
case 'FETCH_REUSABLE_BLOCKS_FAILURE': { | ||
const { id } = action; | ||
if ( ! id ) { | ||
return state; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
editor/store/test/effects.js
Outdated
@@ -471,6 +474,8 @@ describe( 'effects', () => { | |||
|
|||
describe( 'reusable block effects', () => { | |||
beforeAll( () => { | |||
lodash.uniqueId = jest.spyOn( lodash, 'uniqueId' ); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
editor/store/effects.js
Outdated
|
||
const allBlocks = getBlocks( getState() ); | ||
const associatedBlocks = allBlocks.filter( block => isReusableBlock( block ) && block.attributes.ref === id ); | ||
const associatedBlockUids = associatedBlocks.map( block => block.uid ); |
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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.
e81ad82
to
78a9bb7
Compare
There was a problem hiding this 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.
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. |
✨ What this is
Special 2-for-1 offer: closes #3792 and closes #4041.
Note that I've setadd/reusable-block-deletion-to-api
as the base branch so that the diff is bearable. The intention is to merge this intomaster
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:
DELETE_REUSABLE_BLOCK
effect which calls thedestroy()
method that the WP API Backbone library gives us for free.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:
And here's how it now looks for a reusable block:
📋 How to test