diff --git a/packages/block-editor/src/components/block-patterns-list/index.js b/packages/block-editor/src/components/block-patterns-list/index.js index ec2aad3da2b7a..daea191920efc 100644 --- a/packages/block-editor/src/components/block-patterns-list/index.js +++ b/packages/block-editor/src/components/block-patterns-list/index.js @@ -11,6 +11,7 @@ import { } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; +import { Icon, symbolFilled } from '@wordpress/icons'; /** * Internal dependencies @@ -101,6 +102,21 @@ function BlockPattern( { { pattern.description } ) } + { pattern.id && ! pattern.syncStatus && ( + + + + + + ) } diff --git a/packages/block-editor/src/components/block-patterns-list/style.scss b/packages/block-editor/src/components/block-patterns-list/style.scss index ab80fc71d36df..fbc1fddbbd1ca 100644 --- a/packages/block-editor/src/components/block-patterns-list/style.scss +++ b/packages/block-editor/src/components/block-patterns-list/style.scss @@ -40,9 +40,14 @@ @include button-style-outset__focus(var(--wp-admin-theme-color)); } - &:hover .block-editor-block-patterns-list__item-title, &:focus .block-editor-block-patterns-list__item-title { color: var(--wp-admin-theme-color); } + + .block-editor-patterns__pattern-icon { + fill: #fff; + background: var(--wp-block-synced-color); + border-radius: 4px; + } } diff --git a/packages/block-editor/src/components/block-patterns-paging/index.js b/packages/block-editor/src/components/block-patterns-paging/index.js new file mode 100644 index 0000000000000..4b57e9b500b8b --- /dev/null +++ b/packages/block-editor/src/components/block-patterns-paging/index.js @@ -0,0 +1,83 @@ +/** + * WordPress dependencies + */ +import { + __experimentalVStack as VStack, + __experimentalHStack as HStack, + __experimentalText as Text, + Button, +} from '@wordpress/components'; +import { __, _x, _n, sprintf } from '@wordpress/i18n'; + +export default function Pagination( { + currentPage, + numPages, + changePage, + totalItems, +} ) { + return ( + + + { + // translators: %s: Total number of patterns. + sprintf( + // translators: %s: Total number of patterns. + _n( '%s item', '%s items', totalItems ), + totalItems + ) + } + + + + + + + + { sprintf( + // translators: %1$s: Current page number, %2$s: Total number of pages. + _x( '%1$s of %2$s', 'paging' ), + currentPage, + numPages + ) } + + + + + + + + ); +} diff --git a/packages/block-editor/src/components/block-patterns-paging/style.scss b/packages/block-editor/src/components/block-patterns-paging/style.scss new file mode 100644 index 0000000000000..321f251597e1e --- /dev/null +++ b/packages/block-editor/src/components/block-patterns-paging/style.scss @@ -0,0 +1,19 @@ +.block-editor-patterns__grid-pagination { + border-top: 1px solid $gray-800; + padding: $grid-unit-05; + + .components-button.is-tertiary { + width: $button-size-compact; + height: $button-size-compact; + justify-content: center; + + &:disabled { + color: $gray-600; + background: none; + } + + &:hover:not(:disabled) { + background-color: $gray-700; + } + } +} diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js index fda1a00c1a07d..5223b8e270a76 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js @@ -3,7 +3,7 @@ */ import { useMemo, useEffect } from '@wordpress/element'; import { _n, sprintf } from '@wordpress/i18n'; -import { useDebounce, useAsyncList } from '@wordpress/compose'; +import { useDebounce } from '@wordpress/compose'; import { __experimentalHeading as Heading } from '@wordpress/components'; import { speak } from '@wordpress/a11y'; @@ -16,8 +16,8 @@ import useInsertionPoint from '../hooks/use-insertion-point'; import usePatternsState from '../hooks/use-patterns-state'; import InserterListbox from '../../inserter-listbox'; import { searchItems } from '../search-items'; - -const INITIAL_INSERTER_RESULTS = 2; +import BlockPatternsPaging from '../../block-patterns-paging'; +import usePatternsPaging from '../hooks/use-patterns-paging'; function PatternsListHeader( { filterValue, filteredBlockPatternsLength } ) { if ( ! filterValue ) { @@ -97,9 +97,16 @@ function PatternList( { filterValue, selectedCategory, patternCategories } ) { debouncedSpeak( resultsFoundMessage ); }, [ filterValue, debouncedSpeak, filteredBlockPatterns.length ] ); - const currentShownPatterns = useAsyncList( filteredBlockPatterns, { - step: INITIAL_INSERTER_RESULTS, - } ); + const { + totalItems, + categoryPatternsList, + numPages, + changePage, + currentPage, + } = usePatternsPaging( + filteredBlockPatterns, + '.components-modal__content.is-scrollable' + ); const hasItems = !! filteredBlockPatterns?.length; return ( @@ -114,12 +121,20 @@ function PatternList( { filterValue, selectedCategory, patternCategories } ) { { ! hasItems && } { hasItems && ( ) } + { numPages > 1 && ( + + ) } ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js index f66d27ac06170..56c480696b939 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js @@ -9,7 +9,7 @@ import { useEffect, } from '@wordpress/element'; import { _x, __, isRTL } from '@wordpress/i18n'; -import { useAsyncList, useViewportMatch } from '@wordpress/compose'; +import { useViewportMatch } from '@wordpress/compose'; import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, @@ -27,6 +27,8 @@ import usePatternsState from './hooks/use-patterns-state'; import BlockPatternList from '../block-patterns-list'; import PatternsExplorerModal from './block-patterns-explorer/explorer'; import MobileTabNavigation from './mobile-tab-navigation'; +import BlockPatternsPaging from '../block-patterns-paging'; +import usePatternsPaging from './hooks/use-patterns-paging'; const noop = () => {}; @@ -145,7 +147,6 @@ export function BlockPatternsCategoryPanel( { onInsert, rootClientId ); - const availableCategories = usePatternsCategories( rootClientId ); const currentCategoryPatterns = useMemo( () => @@ -169,11 +170,21 @@ export function BlockPatternsCategoryPanel( { [ allPatterns, availableCategories, category.name ] ); - const categoryPatternsList = useAsyncList( currentCategoryPatterns ); - // Hide block pattern preview on unmount. + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect( () => () => onHover( null ), [] ); + const { + totalItems, + categoryPatternsList, + numPages, + changePage, + currentPage, + } = usePatternsPaging( + currentCategoryPatterns, + '.block-editor-inserter__patterns-category-dialog' + ); + if ( ! currentCategoryPatterns.length ) { return null; } @@ -191,10 +202,18 @@ export function BlockPatternsCategoryPanel( { onHover={ onHover } label={ category.label } orientation="vertical" - category={ category.label } + category={ category.name } isDraggable showTitlesAsTooltip={ showTitlesAsTooltip } /> + { numPages > 1 && ( + + ) } ); } diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-paging.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-paging.js new file mode 100644 index 0000000000000..b0c7db9da8cb2 --- /dev/null +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-paging.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import { useMemo, useState } from '@wordpress/element'; + +import { useAsyncList } from '@wordpress/compose'; + +const PAGE_SIZE = 20; +const INITIAL_INSERTER_RESULTS = 2; + +/** + * Supplies values needed to page the patterns list client side. + * + * @param {Array} currentCategoryPatterns An array of the current categories to display. + * @param {string} scrollContainerClass Class of container to scroll when moving between pages. + * + * @return {Object} Returns the relevant paging values. (totalItems, categoryPatternsList, numPages, changePage, currentPage) + */ +export default function usePatternsPaging( + currentCategoryPatterns, + scrollContainerClass +) { + const [ currentPage, setCurrentPage ] = useState( 1 ); + const totalItems = currentCategoryPatterns.length; + const pageIndex = currentPage - 1; + const list = useMemo( () => { + return currentCategoryPatterns.slice( + pageIndex * PAGE_SIZE, + pageIndex * PAGE_SIZE + PAGE_SIZE + ); + }, [ pageIndex, currentCategoryPatterns ] ); + const categoryPatternsList = useAsyncList( list, { + step: INITIAL_INSERTER_RESULTS, + } ); + const numPages = Math.ceil( currentCategoryPatterns.length / PAGE_SIZE ); + const changePage = ( page ) => { + const scrollContainer = document.querySelector( scrollContainerClass ); + scrollContainer?.scrollTo( 0, 0 ); + + setCurrentPage( page ); + }; + return { + totalItems, + categoryPatternsList, + numPages, + changePage, + currentPage, + }; +} diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js index 5f47897f50b7a..9ceebf83f6765 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useCallback, useMemo } from '@wordpress/element'; -import { cloneBlock } from '@wordpress/blocks'; +import { cloneBlock, createBlock } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; @@ -49,8 +49,13 @@ const usePatternsState = ( onInsert, rootClientId ) => { const { createSuccessNotice } = useDispatch( noticesStore ); const onClickPattern = useCallback( ( pattern, blocks ) => { + const patternBlocks = + pattern.syncStatus !== 'unsynced' + ? [ createBlock( 'core/block', { ref: pattern.id } ) ] + : blocks; + onInsert( - ( blocks ?? [] ).map( ( block ) => cloneBlock( block ) ), + ( patternBlocks ?? [] ).map( ( block ) => cloneBlock( block ) ), pattern.name ); createSuccessNotice( diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index c2257cf237669..8978da4ccb9aa 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -27,7 +27,6 @@ import BlockTypesTab from './block-types-tab'; import BlockPatternsTabs, { BlockPatternsCategoryDialog, } from './block-patterns-tab'; -import ReusableBlocksTab from './reusable-blocks-tab'; import { MediaTab, MediaCategoryDialog, useMediaCategories } from './media-tab'; import InserterSearchResults from './search-results'; import useDebouncedInput from './hooks/use-debounced-input'; @@ -174,17 +173,6 @@ function InserterMenu( ] ); - const reusableBlocksTab = useMemo( - () => ( - - ), - [ destinationRootClientId, onInsert, onHover ] - ); - const mediaTab = useMemo( () => ( { - return items.filter( - ( { category, syncStatus } ) => - category === 'reusable' && syncStatus !== 'unsynced' - ); - }, [ items ] ); - - if ( filteredItems.length === 0 ) { - return ; - } - - return ( - - - - ); -} - -// The unwrapped component is only exported for use by unit tests. -/** - * List of reusable blocks shown in the "Reusable" tab of the inserter. - * - * @param {Object} props Component props. - * @param {?string} props.rootClientId Client id of block to insert into. - * @param {Function} props.onInsert Callback to run when item is inserted. - * @param {Function} props.onHover Callback to run when item is hovered. - * - * @return {WPComponent} The component. - */ -export function ReusableBlocksTab( { rootClientId, onInsert, onHover } ) { - return ( - <> -
- -
- -
- -
- - ); -} - -export default ReusableBlocksTab; diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index 1ff8b529707a4..2444bfe6d056c 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { symbol as reusableBlockIcon } from '@wordpress/icons'; import { useMemo } from '@wordpress/element'; import { TabPanel } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -16,12 +15,6 @@ const patternsTab = { /* translators: Theme and Directory Patterns tab title in the block inserter. */ title: __( 'Patterns' ), }; -const reusableBlocksTab = { - name: 'reusable', - /* translators: Locally created Patterns tab title in the block inserter. */ - title: __( 'Synced patterns' ), - icon: reusableBlockIcon, -}; const mediaTab = { name: 'media', /* translators: Media tab title in the block inserter. */ @@ -31,7 +24,6 @@ const mediaTab = { function InserterTabs( { children, showPatterns = false, - showReusableBlocks = false, showMedia = false, onSelect, prioritizePatterns, @@ -48,11 +40,8 @@ function InserterTabs( { if ( showMedia ) { tempTabs.push( mediaTab ); } - if ( showReusableBlocks ) { - tempTabs.push( reusableBlocksTab ); - } return tempTabs; - }, [ prioritizePatterns, showPatterns, showReusableBlocks, showMedia ] ); + }, [ prioritizePatterns, showPatterns, showMedia ] ); return ( { - // This allows us to tweak the returned value on each test. - const mock = jest.fn(); - return mock; -} ); - -describe( 'InserterMenu', () => { - beforeAll( () => { - registerBlockType( 'core/block', { - save: () => {}, - title: 'reusable block', - edit: () => {}, - } ); - } ); - - afterAll( () => { - unregisterBlockType( 'core/block' ); - } ); - - beforeEach( () => { - useBlockTypesState.mockImplementation( () => [ - items, - categories, - collections, - ] ); - } ); - - it( 'should show nothing if there are no items', () => { - const noItems = []; - useBlockTypesState.mockImplementation( () => [ - noItems, - categories, - collections, - ] ); - - render( ); - - expect( screen.queryByRole( 'option' ) ).not.toBeInTheDocument(); - } ); - - it( 'should list reusable blocks', () => { - render( ); - - expect( - screen.getByRole( 'option', { name: 'My reusable block' } ) - ).toBeVisible(); - } ); - - it( 'should trim whitespace of search terms', () => { - render( ); - - expect( - screen.getByRole( 'option', { name: 'My reusable block' } ) - ).toBeVisible(); - } ); -} ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3c961c130b78a..e3859f7ab298a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2294,19 +2294,16 @@ function getUnsyncedPatterns( state ) { const reusableBlocks = state?.settings?.__experimentalReusableBlocks ?? EMPTY_ARRAY; - return reusableBlocks - .filter( - ( reusableBlock ) => - reusableBlock.wp_pattern_sync_status === 'unsynced' - ) - .map( ( reusableBlock ) => { - return { - name: `core/block/${ reusableBlock.id }`, - title: reusableBlock.title.raw, - categories: [ 'custom' ], - content: reusableBlock.content.raw, - }; - } ); + return reusableBlocks.map( ( reusableBlock ) => { + return { + id: reusableBlock.id, + name: `core/block/${ reusableBlock.id }`, + title: reusableBlock.title.raw, + categories: [ 'custom' ], + content: reusableBlock.content.raw, + syncStatus: reusableBlock.wp_pattern_sync_status, + }; + } ); } export const __experimentalGetParsedPattern = createSelector( diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 5eafc0766ae22..51ca87b758a1e 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -12,6 +12,8 @@ @import "./components/block-navigation/style.scss"; @import "./components/block-parent-selector/style.scss"; @import "./components/block-patterns-list/style.scss"; +@import "./components/block-patterns-paging/style.scss"; +@import "./components/block-patterns-sync-filter/style.scss"; @import "./components/block-popover/style.scss"; @import "./components/block-preview/style.scss"; @import "./components/block-settings-menu/style.scss";