-
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
Show all blocks all the times to avoid confusing UX #61873
Changes from all commits
dec05da
744f623
0d4a803
4455c10
97155f9
8de5ca8
df9f4f2
071f067
36d7125
7145856
c2fbc8f
a8b51dc
23ae559
19c8abc
cd3f029
3db4d5f
e1827aa
c9bbc41
202875d
b1775a7
27379f2
e19ab3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __, _x } from '@wordpress/i18n'; | ||
import { __, _x, sprintf } from '@wordpress/i18n'; | ||
import { useMemo, useEffect, forwardRef } from '@wordpress/element'; | ||
import { pipe, useAsyncList } from '@wordpress/compose'; | ||
|
||
|
@@ -14,6 +14,8 @@ import useBlockTypesState from './hooks/use-block-types-state'; | |
import InserterListbox from '../inserter-listbox'; | ||
import { orderBy } from '../../utils/sorting'; | ||
import InserterNoResults from './no-results'; | ||
import { select } from '@wordpress/data'; | ||
import { store as blockEditorStore } from '../../store'; | ||
|
||
const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ]; | ||
|
||
|
@@ -31,10 +33,27 @@ export function BlockTypesTab( | |
{ rootClientId, onInsert, onHover, showMostUsedBlocks }, | ||
ref | ||
) { | ||
const [ items, categories, collections, onSelectItem ] = useBlockTypesState( | ||
rootClientId, | ||
onInsert | ||
); | ||
const { getBlockName } = select( blockEditorStore ); | ||
const [ items, categories, collections, onSelectItem, allItems ] = | ||
useBlockTypesState( rootClientId, onInsert ); | ||
|
||
const allItemsPerCategory = useMemo( () => { | ||
return pipe( | ||
( itemList ) => | ||
itemList.filter( | ||
( item ) => item.category && item.category !== 'reusable' | ||
), | ||
( itemList ) => | ||
itemList.reduce( ( acc, item ) => { | ||
const { category } = item; | ||
if ( ! acc[ category ] ) { | ||
acc[ category ] = []; | ||
} | ||
acc[ category ].push( item ); | ||
return acc; | ||
}, {} ) | ||
)( allItems ); | ||
}, [ allItems ] ); | ||
|
||
const suggestedItems = useMemo( () => { | ||
return orderBy( items, 'frecency', 'desc' ).slice( | ||
|
@@ -83,6 +102,19 @@ export function BlockTypesTab( | |
// Hide block preview on unmount. | ||
useEffect( () => () => onHover( null ), [] ); | ||
|
||
const getRootBlockTitle = () => { | ||
const blockName = getBlockName( rootClientId ); | ||
// Find the title within the list of all possible blocks. | ||
const block = allItems.find( ( item ) => item.name === blockName ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we getting the block title here? Why not check the block type instead of looping through all blocks, like we normally do? |
||
return block | ||
? sprintf( | ||
// translators: %s: Block title, e.g: "List", "Buttons", "Group" | ||
__( '%s Block' ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need "Block" appended here. The block name itself will be enough context. "List", "Columns", "Buttons", etc. |
||
block.title | ||
) | ||
: __( 'Allowed' ); | ||
}; | ||
|
||
/** | ||
* The inserter contains a big number of blocks and opening it is a costful operation. | ||
* The rendering is the most costful part of it, in order to improve the responsiveness | ||
|
@@ -101,13 +133,18 @@ export function BlockTypesTab( | |
didRenderAllCategories ? collectionEntries : EMPTY_ARRAY | ||
); | ||
|
||
if ( ! items.length ) { | ||
return <InserterNoResults />; | ||
} | ||
const availableCategories = categories.filter( ( category ) => { | ||
const categoryItems = itemsPerCategory[ category.slug ]; | ||
if ( ! categoryItems || ! categoryItems.length ) { | ||
return false; | ||
} | ||
return category; | ||
} ); | ||
|
||
return ( | ||
<InserterListbox> | ||
<div ref={ ref }> | ||
{ ! items.length && ! allItems.length && <InserterNoResults /> } | ||
{ showMostUsedBlocks && !! suggestedItems.length && ( | ||
<InserterPanel title={ _x( 'Most used', 'blocks' ) }> | ||
<BlockTypesList | ||
|
@@ -127,7 +164,11 @@ export function BlockTypesTab( | |
return ( | ||
<InserterPanel | ||
key={ category.slug } | ||
title={ category.title } | ||
title={ | ||
categoryItems.length === 1 | ||
? getRootBlockTitle() | ||
: category.title | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this condition. Why do we fall back to the title of the parent of the selected block when there's 1 category? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I got confused by |
||
} | ||
icon={ category.icon } | ||
> | ||
<BlockTypesList | ||
|
@@ -177,6 +218,30 @@ export function BlockTypesTab( | |
); | ||
} | ||
) } | ||
|
||
{ ( items.length === 0 || | ||
availableCategories.length === 1 ) && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be great to comment on this condition. Why show this when there's 1 category available and not 2? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Pure convention 🤷🏻 |
||
<div className="block-editor-inserter__all-blocks"> | ||
{ categories.map( ( category ) => { | ||
const categoryItems = | ||
allItemsPerCategory[ category.slug ]; | ||
return ( | ||
<InserterPanel | ||
key={ category.slug } | ||
title={ category.title } | ||
icon={ category.icon } | ||
> | ||
<BlockTypesList | ||
items={ categoryItems } | ||
onSelect={ onSelectItem } | ||
onHover={ onHover } | ||
label={ category.title } | ||
/> | ||
</InserterPanel> | ||
); | ||
} ) } | ||
</div> | ||
) } | ||
</div> | ||
</InserterListbox> | ||
); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,25 @@ import { store as blockEditorStore } from '../../../store'; | |
* @return {Array} Returns the block types state. (block types, categories, collections, onSelect handler) | ||
*/ | ||
const useBlockTypesState = ( rootClientId, onInsert ) => { | ||
const [ allItems ] = useSelect( ( select ) => { | ||
let availableItems = select( blockEditorStore ).getInserterItems( '' ); | ||
|
||
// use current selection as root for situations like | ||
// template locked mode | ||
const rootBlocks = select( blockEditorStore ).getBlocks(); | ||
while ( availableItems.length === 0 ) { | ||
for ( const block of rootBlocks ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This loop feels like it can be very bad perf wise in nested templates and things like that. (not sure how true though). The other thing that feels odd, is that it will end up with a random client id and the behavior for the user can be confusing: where am I going to insert when I click that thing? It feels like a better path, would be to try to find the "rootClientId" that we want to insert in before hand and update
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes by all means there a couple of risky loops but they're in place just to make things "work" and to show conceptual limitations we still have. For example a loop later in the PR will make the action of inserting a block while a post content block is selected in a template locked edit mode and it shows that we can't tell where to insert without:
... both of which are kinda weird. Separately, the theoretical Maybe the block list should advertise this in a separate precomputed part of the store? As in top down places to insert blocks if the next thing is unavailable ... or something. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two separate discussions here:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, that's why I said: "by all means there a couple of risky loops but they're in place just to make things "work" and to show conceptual limitations we still have." and @jeryj said "We have gotten it to work, but it needs refactoring to be more stable and easier to maintain." 😂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, sorry I misread the original reply :) |
||
availableItems = select( blockEditorStore ).getInserterItems( | ||
block.clientId | ||
); | ||
if ( availableItems.length ) { | ||
break; | ||
} | ||
} | ||
} | ||
return [ availableItems ]; | ||
} ); | ||
|
||
const [ items ] = useSelect( | ||
( select ) => [ | ||
select( blockEditorStore ).getInserterItems( rootClientId ), | ||
|
@@ -32,6 +51,7 @@ const useBlockTypesState = ( rootClientId, onInsert ) => { | |
|
||
const [ categories, collections ] = useSelect( ( select ) => { | ||
const { getCategories, getCollections } = select( blocksStore ); | ||
|
||
return [ getCategories(), getCollections() ]; | ||
}, [] ); | ||
|
||
|
@@ -56,7 +76,7 @@ const useBlockTypesState = ( rootClientId, onInsert ) => { | |
[ onInsert ] | ||
); | ||
|
||
return [ items, categories, collections, onSelectItem ]; | ||
return [ items, categories, collections, onSelectItem, allItems ]; | ||
}; | ||
|
||
export default useBlockTypesState; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,21 +41,29 @@ function useInsertionPoint( { | |
onSelect, | ||
shouldFocusBlock = true, | ||
selectBlockOnInsert = true, | ||
blockToInsert, | ||
} ) { | ||
const { getSelectedBlock } = useSelect( blockEditorStore ); | ||
const { destinationRootClientId, destinationIndex } = useSelect( | ||
const { | ||
destinationRootClientId, | ||
destinationIndex, | ||
blockInsertionRootClientId, | ||
blockInsertionIndex, | ||
} = useSelect( | ||
( select ) => { | ||
const { | ||
getSelectedBlockClientId, | ||
getBlockRootClientId, | ||
getBlockIndex, | ||
getBlockOrder, | ||
getBlockIndex, | ||
getBlockRootClientId, | ||
getBlockInsertionPoint, | ||
} = select( blockEditorStore ); | ||
const selectedBlockClientId = getSelectedBlockClientId(); | ||
|
||
let _destinationRootClientId = rootClientId; | ||
let _destinationIndex; | ||
|
||
const blockInsertionPoint = getBlockInsertionPoint(); | ||
if ( insertionIndex !== undefined ) { | ||
// Insert into a specific index. | ||
_destinationIndex = insertionIndex; | ||
|
@@ -74,9 +82,23 @@ function useInsertionPoint( { | |
).length; | ||
} | ||
|
||
// When we're using the appender or an insertion index has been passed directly, | ||
// we want to use that over our "best guess" block insertion point | ||
const isDirectInsert = insertionIndex !== undefined || isAppender; | ||
const shouldUseBlockInsertionPoint = | ||
! isDirectInsert && | ||
blockInsertionPoint?.rootClientId !== undefined && | ||
blockInsertionPoint?.index !== undefined; | ||
|
||
return { | ||
destinationRootClientId: _destinationRootClientId, | ||
destinationIndex: _destinationIndex, | ||
blockInsertionRootClientId: shouldUseBlockInsertionPoint | ||
? blockInsertionPoint.rootClientId | ||
: _destinationRootClientId, | ||
blockInsertionIndex: shouldUseBlockInsertionPoint | ||
? blockInsertionPoint.index | ||
: _destinationIndex, | ||
}; | ||
}, | ||
[ rootClientId, insertionIndex, clientId, isAppender ] | ||
|
@@ -121,8 +143,8 @@ function useInsertionPoint( { | |
} else { | ||
insertBlocks( | ||
blocks, | ||
destinationIndex, | ||
destinationRootClientId, | ||
blockInsertionIndex, | ||
blockInsertionRootClientId, | ||
selectBlockOnInsert, | ||
shouldFocusBlock || shouldForceFocusBlock ? 0 : null, | ||
meta | ||
|
@@ -145,18 +167,21 @@ function useInsertionPoint( { | |
getSelectedBlock, | ||
replaceBlocks, | ||
insertBlocks, | ||
destinationRootClientId, | ||
destinationIndex, | ||
blockInsertionRootClientId, | ||
blockInsertionIndex, | ||
onSelect, | ||
shouldFocusBlock, | ||
selectBlockOnInsert, | ||
setLastFocus, | ||
] | ||
); | ||
|
||
const onToggleInsertionPoint = useCallback( | ||
( show ) => { | ||
if ( show ) { | ||
showInsertionPoint( destinationRootClientId, destinationIndex ); | ||
showInsertionPoint( destinationRootClientId, destinationIndex, { | ||
block: blockToInsert, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume we need to pass the block so that we can change the target if necessary. Can't be change |
||
} ); | ||
} else { | ||
hideInsertionPoint(); | ||
} | ||
|
@@ -166,6 +191,7 @@ function useInsertionPoint( { | |
hideInsertionPoint, | ||
destinationRootClientId, | ||
destinationIndex, | ||
blockToInsert, | ||
] | ||
); | ||
|
||
|
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.
Why loop over this twice and add
pipe
if it can be done in a single loop?