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 new block commands #52509

Merged
merged 11 commits into from Aug 14, 2023
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/block-editor/README.md
Expand Up @@ -680,6 +680,10 @@ _Related_

Private @wordpress/block-editor APIs.

### ReusableBlocksRenameHint

Undocumented declaration.

### RichText

_Related_
Expand Down Expand Up @@ -789,6 +793,10 @@ _Related_

- <https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/url-popover/README.md>

### useBlockCommands

Undocumented declaration.

### useBlockDisplayInformation

Hook used to try to find a matching block variation and return the appropriate information for display reasons. In order to to try to find a match we need to things: 1. Block's client id to extract it's current attributes. 2. A block variation should have set `isActive` prop to a proper function.
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/package.json
Expand Up @@ -39,6 +39,7 @@
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/commands": "file:../commands",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/data": "file:../data",
Expand Down
6 changes: 6 additions & 0 deletions packages/block-editor/src/components/index.js
Expand Up @@ -166,3 +166,9 @@ export { useBlockEditingMode } from './block-editing-mode';

export { default as BlockEditorProvider } from './provider';
export { default as useSetting } from './use-setting';
export { useBlockCommands } from './use-block-commands';
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need the Block in the name of this hook. It's already part of block editor so maybe it can be just useCommands which would stand for (use all block editor commands)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's fine to be granular? Is there any downside?


/*
* The following rename hint component can be removed in 6.4.
*/
export { default as ReusableBlocksRenameHint } from './inserter/reusable-block-rename-hint';
284 changes: 284 additions & 0 deletions packages/block-editor/src/components/use-block-commands/index.js
@@ -0,0 +1,284 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import {
hasBlockSupport,
store as blocksStore,
switchToBlockType,
isTemplatePart,
} from '@wordpress/blocks';
import { useSelect, useDispatch } from '@wordpress/data';
import { useCommandLoader } from '@wordpress/commands';
import {
copy,
edit as remove,
create as add,
group,
ungroup,
moveTo as move,
} from '@wordpress/icons';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';

export const useTransformCommands = () => {
const { clientIds } = useSelect( ( select ) => {
const { getSelectedBlockClientIds } = select( blockEditorStore );
const selectedBlockClientIds = getSelectedBlockClientIds();

return {
clientIds: selectedBlockClientIds,
};
}, [] );
const blocks = useSelect(
( select ) =>
select( blockEditorStore ).getBlocksByClientId( clientIds ),
[ clientIds ]
);
const { replaceBlocks, multiSelect } = useDispatch( blockEditorStore );
const { possibleBlockTransformations, canRemove } = useSelect(
( select ) => {
const {
getBlockRootClientId,
getBlockTransformItems,
canRemoveBlocks,
} = select( blockEditorStore );
const rootClientId = getBlockRootClientId(
Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds
);
return {
possibleBlockTransformations: getBlockTransformItems(
Copy link
Contributor

Choose a reason for hiding this comment

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

In some contexts, this is triggering failures in the command loader. I'm guessing when the "blocks" array is empty.

cc @draganescu

Also @artpi for more details.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still tracking down the exact reproduction path, but here is a stack trace I am getting when I run command loader quite often (for a different env that command pallette):

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure what to do to trigger a visible error. Ultimately empty blocks is guarded in getPossibleBlockTransformations by:

if ( ! blocks.length ) {
		return [];
	}

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I see that in the code and yet I get the above stack trace. My suspicion is that we're getting [null, null ] or something similar

Copy link
Contributor

Choose a reason for hiding this comment

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

I finally have a reproduciton path. It took me quite a while:

#59289

blocks,
rootClientId
),
canRemove: canRemoveBlocks( clientIds, rootClientId ),
};
},
[ clientIds, blocks ]
);

const isTemplate = blocks.length === 1 && isTemplatePart( blocks[ 0 ] );

function selectForMultipleBlocks( insertedBlocks ) {
if ( insertedBlocks.length > 1 ) {
multiSelect(
insertedBlocks[ 0 ].clientId,
insertedBlocks[ insertedBlocks.length - 1 ].clientId
);
}
}

// Simple block tranformation based on the `Block Transforms` API.
function onBlockTransform( name ) {
const newBlocks = switchToBlockType( blocks, name );
replaceBlocks( clientIds, newBlocks );
selectForMultipleBlocks( newBlocks );
}

/**
* The `isTemplate` check is a stopgap solution here.
* Ideally, the Transforms API should handle this
Copy link
Member

Choose a reason for hiding this comment

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

Seems it'd be better to capture this comment in a separate issue instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that is copy pasta from the block tools code :)

* by allowing to exclude blocks from wildcard transformations.
*/
const hasPossibleBlockTransformations =
!! possibleBlockTransformations.length && canRemove && ! isTemplate;

if (
! clientIds ||
clientIds.length < 1 ||
! hasPossibleBlockTransformations
) {
return { isLoading: false, commands: [] };
}

const commands = possibleBlockTransformations.map( ( transformation ) => {
const { name, title, icon } = transformation;
return {
name: 'core/block-editor/transform-to-' + name.replace( '/', '-' ),
// translators: %s: block title/name.
label: sprintf( __( 'Transform to %s' ), title ),
icon: icon.src,
callback: ( { close } ) => {
onBlockTransform( name );
close();
},
};
} );

return { isLoading: false, commands };
};

const useActionsCommands = () => {
const { clientIds } = useSelect( ( select ) => {
const { getSelectedBlockClientIds } = select( blockEditorStore );
const selectedBlockClientIds = getSelectedBlockClientIds();

return {
clientIds: selectedBlockClientIds,
};
}, [] );
const {
canInsertBlockType,
getBlockRootClientId,
getBlocksByClientId,
canMoveBlocks,
canRemoveBlocks,
} = useSelect( blockEditorStore );
const { getDefaultBlockName, getGroupingBlockName } =
useSelect( blocksStore );

const blocks = getBlocksByClientId( clientIds );
const rootClientId = getBlockRootClientId( clientIds[ 0 ] );

const canDuplicate = blocks.every( ( block ) => {
return (
!! block &&
hasBlockSupport( block.name, 'multiple', true ) &&
canInsertBlockType( block.name, rootClientId )
);
} );

const canInsertDefaultBlock = canInsertBlockType(
getDefaultBlockName(),
rootClientId
);

const canMove = canMoveBlocks( clientIds, rootClientId );
const canRemove = canRemoveBlocks( clientIds, rootClientId );

const {
removeBlocks,
replaceBlocks,
duplicateBlocks,
insertAfterBlock,
insertBeforeBlock,
setBlockMovingClientId,
setNavigationMode,
selectBlock,
} = useDispatch( blockEditorStore );

const onDuplicate = () => {
if ( ! canDuplicate ) {
return;
}
return duplicateBlocks( clientIds, true );
};
const onRemove = () => {
if ( ! canRemove ) {
return;
}
return removeBlocks( clientIds, true );
};
const onAddBefore = () => {
if ( ! canInsertDefaultBlock ) {
return;
}
const clientId = Array.isArray( clientIds ) ? clientIds[ 0 ] : clientId;
insertBeforeBlock( clientId );
};
const onAddAfter = () => {
if ( ! canInsertDefaultBlock ) {
return;
}
const clientId = Array.isArray( clientIds )
? clientIds[ clientIds.length - 1 ]
: clientId;
insertAfterBlock( clientId );
};
const onMoveTo = () => {
if ( ! canMove ) {
return;
}
setNavigationMode( true );
selectBlock( clientIds[ 0 ] );
setBlockMovingClientId( clientIds[ 0 ] );
};
const onGroup = () => {
if ( ! blocks.length ) {
return;
}

const groupingBlockName = getGroupingBlockName();

// Activate the `transform` on `core/group` which does the conversion.
const newBlocks = switchToBlockType( blocks, groupingBlockName );

if ( ! newBlocks ) {
return;
}
replaceBlocks( clientIds, newBlocks );
};
const onUngroup = () => {
if ( ! blocks.length ) {
return;
}

const innerBlocks = blocks[ 0 ].innerBlocks;

if ( ! innerBlocks.length ) {
return;
}

replaceBlocks( clientIds, innerBlocks );
};

if ( ! clientIds || clientIds.length < 1 ) {
return { isLoading: false, commands: [] };
}

const icons = {
ungroup,
group,
move,
add,
remove,
duplicate: copy,
};

const commands = [
onUngroup,
onGroup,
onMoveTo,
onAddAfter,
onAddBefore,
onRemove,
onDuplicate,
].map( ( callback ) => {
const action = callback.name
.replace( 'on', '' )
.replace( /([a-z])([A-Z])/g, '$1 $2' );

return {
name: 'core/block-editor/action-' + callback.name,
// translators: %s: type of the command.
label: action,
icon: icons[
callback.name
.replace( 'on', '' )
.match( /[A-Z]{1}[a-z]*/ )
.toString()
.toLowerCase()
],
callback: ( { close } ) => {
callback();
close();
},
};
} );

return { isLoading: false, commands };
};

export const useBlockCommands = () => {
useCommandLoader( {
name: 'core/block-editor/blockTransforms',
hook: useTransformCommands,
} );
useCommandLoader( {
name: 'core/block-editor/blockActions',
hook: useActionsCommands,
} );
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Both of these command loaders use a lot of selectors... I do wonder about a potential performance impact on the command palette with long posts. Maybe it's fine though. One follow-up here could be to add a performance metric about the command palette.

2 changes: 2 additions & 0 deletions packages/edit-post/src/components/layout/index.js
Expand Up @@ -19,6 +19,7 @@ import {
} from '@wordpress/editor';
import { useSelect, useDispatch } from '@wordpress/data';
import {
useBlockCommands,
BlockBreadcrumb,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
Expand Down Expand Up @@ -72,6 +73,7 @@ const interfaceLabels = {
};

function Layout() {
useBlockCommands();
const isMobileViewport = useViewportMatch( 'medium', '<' );
const isHugeViewport = useViewportMatch( 'huge', '>=' );
const isLargeViewport = useViewportMatch( 'large' );
Expand Down
6 changes: 5 additions & 1 deletion packages/edit-site/src/components/layout/index.js
Expand Up @@ -26,7 +26,10 @@ import {
privateApis as commandsPrivateApis,
} from '@wordpress/commands';
import { store as preferencesStore } from '@wordpress/preferences';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import {
privateApis as blockEditorPrivateApis,
useBlockCommands,
} from '@wordpress/block-editor';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands';

Expand Down Expand Up @@ -66,6 +69,7 @@ export default function Layout() {
useCommands();
useEditModeCommands();
useCommonCommands();
useBlockCommands();

const hubRef = useRef();
const { params } = useLocation();
Expand Down