Skip to content

Commit

Permalink
Block Directory: Uninstall unused block types (#22918)
Browse files Browse the repository at this point in the history
* Add unused block uninstall code into its own component.

* Tweak saving logic

* Fix mocked API response in test

* Add new selectors to get used & unused block types

This pulls `hasBlockType` out of the block uninstaller, and moves the logic into selectors so we can re-use it for the "blocks to install" list in pre-publish panel.

* Update variable name to reflect nested structure

Co-authored-by: dufresnesteven <[email protected]>
  • Loading branch information
ryelle and StevenDufresne authored Jun 11, 2020
1 parent a63375c commit a0112fd
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* WordPress dependencies
*/
import { unregisterBlockType } from '@wordpress/blocks';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';

export default function AutoBlockUninstaller() {
const { uninstallBlockType } = useDispatch( 'core/block-directory' );

const shouldRemoveBlockTypes = useSelect( ( select ) => {
const { isAutosavingPost, isSavingPost } = select( 'core/editor' );
return isSavingPost() && ! isAutosavingPost();
} );

const unusedBlockTypes = useSelect( ( select ) =>
select( 'core/block-directory' ).getUnusedBlockTypes()
);

useEffect( () => {
if ( shouldRemoveBlockTypes && unusedBlockTypes.length ) {
unusedBlockTypes.forEach( ( blockType ) => {
uninstallBlockType( blockType );
unregisterBlockType( blockType.name );
} );
}
}, [ shouldRemoveBlockTypes ] );

return null;
}
2 changes: 2 additions & 0 deletions packages/block-directory/src/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { registerPlugin } from '@wordpress/plugins';
/**
* Internal dependencies
*/
import AutoBlockUninstaller from '../components/auto-block-uninstaller';
import InserterMenuDownloadableBlocksPanel from './inserter-menu-downloadable-blocks-panel';
import InstalledBlocksPrePublishPanel from './installed-blocks-pre-publish-panel';

registerPlugin( 'block-directory', {
render() {
return (
<>
<AutoBlockUninstaller />
<InserterMenuDownloadableBlocksPanel />
<InstalledBlocksPrePublishPanel />
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import CompactList from '../../components/compact-list';

export default function InstalledBlocksPrePublishPanel() {
const newBlockTypes = useSelect( ( select ) =>
select( 'core/block-directory' ).getInstalledBlockTypes()
select( 'core/block-directory' ).getNewBlockTypes()
);

if ( ! newBlockTypes.length ) {
Expand Down
44 changes: 43 additions & 1 deletion packages/block-directory/src/store/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { apiFetch, select } from '@wordpress/data-controls';
import { apiFetch, dispatch, select } from '@wordpress/data-controls';

/**
* Internal dependencies
Expand Down Expand Up @@ -91,6 +91,34 @@ export function* installBlockType( block ) {
return success;
}

/**
* Action triggered to uninstall a block plugin.
*
* @param {Object} block The blockType object.
*/
export function* uninstallBlockType( block ) {
const { id } = block;
try {
const response = yield apiFetch( {
path: '__experimental/block-directory/uninstall',
data: {
slug: id,
},
method: 'DELETE',
} );
if ( response !== true ) {
throw new Error( __( 'Unable to uninstall this block.' ) );
}
yield removeInstalledBlockType( block );
} catch ( error ) {
yield dispatch(
'core/notices',
'createErrorNotice',
error.message || __( 'An error occurred.' )
);
}
}

/**
* Returns an action object used to add a newly installed block type.
*
Expand All @@ -105,6 +133,20 @@ export function addInstalledBlockType( item ) {
};
}

/**
* Returns an action object used to remove a newly installed block type.
*
* @param {string} item The block item with the block id and name.
*
* @return {Object} Action object.
*/
export function removeInstalledBlockType( item ) {
return {
type: 'REMOVE_INSTALLED_BLOCK_TYPE',
item,
};
}

/**
* Returns an action object used to indicate install in progress
*
Expand Down
58 changes: 58 additions & 0 deletions packages/block-directory/src/store/selectors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
/**
* WordPress dependencies
*/
import { createRegistrySelector } from '@wordpress/data';

/**
* Internal dependencies
*/
import hasBlockType from './utils/has-block-type';

/**
* Returns true if application is requesting for downloadable blocks.
*
Expand Down Expand Up @@ -46,6 +56,54 @@ export function getInstalledBlockTypes( state ) {
return state.blockManagement.installedBlockTypes;
}

/**
* Returns block types that have been installed on the server and used in the
* current post.
*
* @param {Object} state Global application state.
*
* @return {Array} Block type items.
*/
export const getNewBlockTypes = createRegistrySelector(
( select ) => ( state ) => {
const usedBlockTree = select( 'core/block-editor' ).getBlocks();
const installedBlockTypes = getInstalledBlockTypes( state );

const newBlockTypes = [];
installedBlockTypes.forEach( ( blockType ) => {
if ( hasBlockType( blockType, usedBlockTree ) ) {
newBlockTypes.push( blockType );
}
} );

return newBlockTypes;
}
);

/**
* Returns the block types that have been installed on the server but are not
* used in the current post.
*
* @param {Object} state Global application state.
*
* @return {Array} Block type items.
*/
export const getUnusedBlockTypes = createRegistrySelector(
( select ) => ( state ) => {
const usedBlockTree = select( 'core/block-editor' ).getBlocks();
const installedBlockTypes = getInstalledBlockTypes( state );

const newBlockTypes = [];
installedBlockTypes.forEach( ( blockType ) => {
if ( ! hasBlockType( blockType, usedBlockTree ) ) {
newBlockTypes.push( blockType );
}
} );

return newBlockTypes;
}
);

/**
* Returns true if application is calling install endpoint.
*
Expand Down
46 changes: 45 additions & 1 deletion packages/block-directory/src/store/test/actions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { installBlockType } from '../actions';
import { installBlockType, uninstallBlockType } from '../actions';

describe( 'actions', () => {
const item = {
Expand Down Expand Up @@ -126,4 +126,48 @@ describe( 'actions', () => {
} );
} );
} );

describe( 'uninstallBlockType', () => {
it( 'should uninstall a block successfully', () => {
const generator = uninstallBlockType( item );

expect( generator.next().value ).toMatchObject( {
type: 'API_FETCH',
} );

expect( generator.next( true ).value ).toEqual( {
type: 'REMOVE_INSTALLED_BLOCK_TYPE',
item,
} );

expect( generator.next() ).toEqual( {
value: undefined,
done: true,
} );
} );

it( "should set a global notice if the plugin can't uninstall", () => {
const generator = uninstallBlockType( item );

expect( generator.next().value ).toMatchObject( {
type: 'API_FETCH',
} );

const apiError = {
code: 'could_not_remove_plugin',
message: 'Could not fully remove the plugin .',
data: null,
};
expect( generator.next( apiError ).value ).toMatchObject( {
type: 'DISPATCH',
actionName: 'createErrorNotice',
storeKey: 'core/notices',
} );

expect( generator.next() ).toEqual( {
value: undefined,
done: true,
} );
} );
} );
} );
28 changes: 27 additions & 1 deletion packages/block-directory/src/store/test/fixtures/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,33 @@ export const downloadableBlock = {
humanizedUpdated: '3 months ago',
};

export const installedItem = {
export const blockTypeInstalled = {
id: 'boxer-block',
name: 'boxer/boxer',
};

export const blockTypeUnused = {
id: 'example-block',
name: 'fake/unused',
};

export const blockList = [
{
clientId: 1,
name: 'core/paragraph',
attributes: {},
innerBlocks: [],
},
{
clientId: 2,
name: 'boxer/boxer',
attributes: {},
innerBlocks: [],
},
{
clientId: 3,
name: 'core/heading',
attributes: {},
innerBlocks: [],
},
];
8 changes: 4 additions & 4 deletions packages/block-directory/src/store/test/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
errorNotices,
hasPermission,
} from '../reducer';
import { installedItem, downloadableBlock } from './fixtures';
import { blockTypeInstalled, downloadableBlock } from './fixtures';

describe( 'state', () => {
describe( 'downloadableBlocks()', () => {
Expand Down Expand Up @@ -72,19 +72,19 @@ describe( 'state', () => {
const initialState = deepFreeze( { installedBlockTypes: [] } );
const state = blockManagement( initialState, {
type: 'ADD_INSTALLED_BLOCK_TYPE',
item: installedItem,
item: blockTypeInstalled,
} );

expect( state.installedBlockTypes ).toHaveLength( 1 );
} );

it( 'should remove item from the installedBlockTypesList', () => {
const initialState = deepFreeze( {
installedBlockTypes: [ installedItem ],
installedBlockTypes: [ blockTypeInstalled ],
} );
const state = blockManagement( initialState, {
type: 'REMOVE_INSTALLED_BLOCK_TYPE',
item: installedItem,
item: blockTypeInstalled,
} );

expect( state.installedBlockTypes ).toHaveLength( 0 );
Expand Down
Loading

0 comments on commit a0112fd

Please sign in to comment.