From 57666b75f108e2de42e192dd07b82c5d36c31db0 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Jul 2023 09:39:54 +0800 Subject: [PATCH 01/15] Try to add pagination to the patterns screen --- lib/compat/wordpress-6.3/blocks.php | 47 +++++++ .../src/components/page-patterns/grid-item.js | 14 ++- .../src/components/page-patterns/grid.js | 86 ++++++++++--- .../components/page-patterns/patterns-list.js | 107 ++++++++-------- .../src/components/page-patterns/style.scss | 69 ++++++---- .../components/page-patterns/use-patterns.js | 119 +++++++++--------- .../use-my-patterns.js | 13 +- 7 files changed, 293 insertions(+), 162 deletions(-) diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php index ccc68786dc6ad..903d82f77b423 100644 --- a/lib/compat/wordpress-6.3/blocks.php +++ b/lib/compat/wordpress-6.3/blocks.php @@ -120,3 +120,50 @@ function gutenberg_wp_block_register_post_meta() { ); } add_action( 'init', 'gutenberg_wp_block_register_post_meta' ); + +/** + * Allow querying blocks by sync_status. + * + * Note: This should be removed when the minimum required WP version is >= 6.3. + * + * @param array $args Array of arguments for WP_Query. + * @param WP_REST_Request $request The REST API request. + * + * @return array Updated array of arguments for WP_Query. + */ +function gutenberg_rest_wp_block_query( $args, $request ) { + if ( isset( $request['sync_status'] ) ) { + if ( 'fully' === $request['sync_status'] ) { + $sync_status_query = array( + 'relation' => 'OR', + array( + 'key' => 'sync_status', + 'value' => '', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'sync_status', + 'value' => 'fully', + 'compare' => '=', + ), + ); + } else { + $sync_status_query = array( + array( + 'key' => 'sync_status', + 'value' => sanitize_text_field( $request['sync_status'] ), + 'compare' => '=', + ), + ); + } + + if ( isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ) { + array_push( $args['meta_query'], $sync_status_query ); + } else { + $args['meta_query'] = $sync_status_query; + } + } + + return $args; +} +add_filter( 'rest_wp_block_query', 'gutenberg_rest_wp_block_query', 10, 2 ); diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js index 7f40fbce9035c..d5d8156966a97 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -25,6 +25,7 @@ import { header, footer, symbolFilled as uncategorized, + symbol, moreHorizontal, lockSmall, } from '@wordpress/icons'; @@ -37,13 +38,13 @@ import { DELETE, BACKSPACE } from '@wordpress/keycodes'; */ import RenameMenuItem from './rename-menu-item'; import DuplicateMenuItem from './duplicate-menu-item'; -import { PATTERNS, TEMPLATE_PARTS, USER_PATTERNS } from './utils'; +import { PATTERNS, TEMPLATE_PARTS, USER_PATTERNS, SYNC_TYPES } from './utils'; import { store as editSiteStore } from '../../store'; import { useLink } from '../routes/link'; const templatePartIcons = { header, footer, uncategorized }; -export default function GridItem( { categoryId, composite, icon, item } ) { +export default function GridItem( { categoryId, composite, item } ) { const descriptionId = useId(); const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false ); @@ -122,9 +123,10 @@ export default function GridItem( { categoryId, composite, icon, item } ) { ariaDescriptions.push( __( 'Theme patterns cannot be edited.' ) ); } - const itemIcon = templatePartIcons[ categoryId ] - ? templatePartIcons[ categoryId ] - : icon; + const itemIcon = + item.syncStatus === SYNC_TYPES.full + ? symbol + : templatePartIcons[ categoryId ]; const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' ); const confirmPrompt = hasThemeFile @@ -180,7 +182,7 @@ export default function GridItem( { categoryId, composite, icon, item } ) { spacing={ 3 } className="edit-site-patterns__pattern-title" > - { icon && ( + { itemIcon && ( index % 2 === 0; +const isOdd = ( _, index ) => index % 2 !== 0; + +export default function Grid( { categoryId, items } ) { const composite = useCompositeState( { orientation: 'vertical' } ); + const isSmallerViewport = useViewportMatch( 'large', '<' ); + const [ page, setPage ] = useState( 1 ); + const gridRef = useRef(); if ( ! items?.length ) { return null; } + const maxCount = page * PAGE_SIZE; + const list = items.slice( 0, maxCount ); + return ( - - { items.map( ( item ) => ( - - ) ) } - + <> + + { isSmallerViewport ? ( + + { list.map( ( item ) => ( + + ) ) } + + ) : ( + <> + + { list.filter( isEven ).map( ( item ) => ( + + ) ) } + + + { list.filter( isOdd ).map( ( item ) => ( + + ) ) } + + + ) } + + { items.length >= maxCount && ( + + ) } + ); } diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index d59596f20e795..93cca1d10a6bc 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -1,17 +1,19 @@ /** * WordPress dependencies */ - +import { useState, useDeferredValue } from '@wordpress/element'; import { SearchControl, - __experimentalHeading as Heading, - __experimentalText as Text, + // __experimentalHeading as Heading, + // __experimentalText as Text, __experimentalVStack as VStack, Flex, FlexBlock, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, } from '@wordpress/components'; import { __, isRTL } from '@wordpress/i18n'; -import { symbol, chevronLeft, chevronRight } from '@wordpress/icons'; +import { chevronLeft, chevronRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useViewportMatch } from '@wordpress/compose'; @@ -24,28 +26,44 @@ import usePatterns from './use-patterns'; import SidebarButton from '../sidebar-button'; import useDebouncedInput from '../../utils/use-debounced-input'; import { unlock } from '../../lock-unlock'; +import { SYNC_TYPES, USER_PATTERN_CATEGORY } from './utils'; const { useLocation, useHistory } = unlock( routerPrivateApis ); +const SYNC_FILTERS = { + all: __( 'All' ), + [ SYNC_TYPES.full ]: __( 'Synced' ), + [ SYNC_TYPES.unsynced ]: __( 'Standard' ), +}; + export default function PatternsList( { categoryId, type } ) { const location = useLocation(); const history = useHistory(); const isMobileViewport = useViewportMatch( 'medium', '<' ); const [ filterValue, setFilterValue, delayedFilterValue ] = useDebouncedInput( '' ); + const deferredFilterValue = useDeferredValue( delayedFilterValue ); - const [ patterns, isResolving ] = usePatterns( - type, - categoryId, - delayedFilterValue - ); + const [ syncFilter, setSyncFilter ] = useState( 'all' ); + const [ patterns, isResolving ] = usePatterns( type, categoryId, { + filterValue: deferredFilterValue, + syncFilter: syncFilter === 'all' ? undefined : syncFilter, + } ); - const { syncedPatterns, unsyncedPatterns } = patterns; - const hasPatterns = !! syncedPatterns.length || !! unsyncedPatterns.length; + const hasPatterns = patterns.length; return ( - + { /* + + { __( 'Synced' ) } + + + { __( 'Patterns that are kept in sync across your site' ) } + + */ } + + { isMobileViewport && ( ) } - + setFilterValue( value ) } @@ -71,46 +89,33 @@ export default function PatternsList( { categoryId, type } ) { __nextHasNoMarginBottom /> + { categoryId === USER_PATTERN_CATEGORY && ( + setSyncFilter( value ) } + __nextHasNoMarginBottom + > + { Object.entries( SYNC_FILTERS ).map( + ( [ key, label ] ) => ( + + ) + ) } + + ) } + { isResolving && __( 'Loading' ) } - { ! isResolving && !! syncedPatterns.length && ( - <> - - - { __( 'Synced' ) } - - - { __( - 'Patterns that are kept in sync across the site' - ) } - - - - - ) } - { ! isResolving && !! unsyncedPatterns.length && ( - <> - - - { __( 'Standard' ) } - - - { __( - 'Patterns that can be changed freely without affecting the site' - ) } - - - - + { ! isResolving && hasPatterns && ( + ) } { ! isResolving && ! hasPatterns && } diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 7a7bf026b9c62..02b8310d9aece 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -12,14 +12,56 @@ @include break-medium { margin: 0; } -} -.edit-site-patterns__grid { - column-gap: $grid-unit-30; - @include break-large() { - column-count: 2; + .edit-site-patterns__search-block { + min-width: fit-content; + flex-grow: 1; + } + + // The increased specificity here is to overcome component styles + // without relying on internal component class names. + .edit-site-patterns__search { + &#{&} input[type="search"] { + background: $gray-800; + color: $gray-200; + + &:focus { + background: $gray-800; + } + } + + svg { + fill: $gray-600; + } + } + + .edit-site-patterns__sync-status-filter { + background: #000; + border: none; + height: 48px; + border-radius: 6px; + min-width: max-content; + width: 100vw; + max-width: 100%; + + @include break-medium { + width: 300px; + } + + // Override the default component style. + & [role="presentation"] { + border-radius: 6px; + } + + .edit-site-patterns__sync-status-filter-option { + border-radius: 6px; + } } +} +.edit-site-patterns__grid { + display: flex; + gap: $grid-unit-30; // Small top padding required to avoid cutting off the visible outline // when hovering items. padding-top: $border-width-focus-fallback; @@ -73,23 +115,6 @@ } } -// The increased specificity here is to overcome component styles -// without relying on internal component class names. -.edit-site-patterns__search { - &#{&} input[type="search"] { - background: $gray-800; - color: $gray-200; - - &:focus { - background: $gray-800; - } - } - - svg { - fill: $gray-600; - } -} - .edit-site-patterns__pattern-title { color: $gray-200; diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index 295d1eee8e410..5caff5c0d20e4 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -46,7 +46,7 @@ const templatePartHasCategory = ( item, category ) => const useTemplatePartsAsPatterns = ( categoryId, postType = TEMPLATE_PARTS, - filterValue = '' + { filterValue = '' } = {} ) => { const { templateParts, isResolving } = useSelect( ( select ) => { @@ -59,7 +59,10 @@ const useTemplatePartsAsPatterns = ( const { getEntityRecords, isResolving: _isResolving } = select( coreStore ); - const query = { per_page: -1 }; + const query = { + per_page: 5, + area: categoryId, + }; const rawTemplateParts = getEntityRecords( 'postType', postType, @@ -78,7 +81,7 @@ const useTemplatePartsAsPatterns = ( ] ), }; }, - [ postType ] + [ postType, categoryId ] ); const filteredTemplateParts = useMemo( () => { @@ -98,7 +101,7 @@ const useTemplatePartsAsPatterns = ( const useThemePatterns = ( categoryId, postType = PATTERNS, - filterValue = '' + { filterValue = '' } = {} ) => { const blockPatterns = useSelect( ( select ) => { const { getSettings } = unlock( select( editSiteStore ) ); @@ -159,10 +162,10 @@ const reusableBlockToPattern = ( reusableBlock ) => ( { const useUserPatterns = ( categoryId, categoryType = PATTERNS, - filterValue = '' + { filterValue = '', syncFilter } = {} ) => { const postType = categoryType === PATTERNS ? USER_PATTERNS : categoryType; - const unfilteredPatterns = useSelect( + const { patterns, isResolving } = useSelect( ( select ) => { if ( postType !== USER_PATTERNS || @@ -171,73 +174,69 @@ const useUserPatterns = ( return EMPTY_PATTERN_LIST; } - const { getEntityRecords } = select( coreStore ); - const records = getEntityRecords( 'postType', postType, { - per_page: -1, - } ); + const { getEntityRecords, isResolving: _isResolving } = + select( coreStore ); - if ( ! records ) { - return EMPTY_PATTERN_LIST; - } + const query = { + per_page: -1, + sync_status: syncFilter, + search: filterValue || undefined, + }; + const records = getEntityRecords( 'postType', postType, query ); - return records.map( ( record ) => - reusableBlockToPattern( record ) - ); + return { + patterns: records + ? records.map( ( record ) => + reusableBlockToPattern( record ) + ) + : EMPTY_PATTERN_LIST, + isResolving: _isResolving( 'getEntityRecords', [ + 'postType', + postType, + query, + ] ), + }; }, - [ postType, categoryId ] + [ postType, categoryId, syncFilter, filterValue ] ); - const filteredPatterns = useMemo( () => { - if ( ! unfilteredPatterns.length ) { - return EMPTY_PATTERN_LIST; - } - - return searchItems( unfilteredPatterns, filterValue, { - // We exit user pattern retrieval early if we aren't in the - // catch-all category for user created patterns, so it has - // to be in the category. - hasCategory: () => true, - } ); - }, [ unfilteredPatterns, filterValue ] ); - - const patterns = { syncedPatterns: [], unsyncedPatterns: [] }; + // const filteredPatterns = useMemo( () => { + // if ( ! unfilteredPatterns.length ) { + // return EMPTY_PATTERN_LIST; + // } - filteredPatterns.forEach( ( pattern ) => { - if ( pattern.syncStatus === SYNC_TYPES.full ) { - patterns.syncedPatterns.push( pattern ); - } else { - patterns.unsyncedPatterns.push( pattern ); - } - } ); + // return searchItems( unfilteredPatterns, filterValue, { + // // We exit user pattern retrieval early if we aren't in the + // // catch-all category for user created patterns, so it has + // // to be in the category. + // hasCategory: () => true, + // } ); + // }, [ unfilteredPatterns, filterValue ] ); - return patterns; + return { patterns, isResolving }; }; -export const usePatterns = ( categoryType, categoryId, filterValue ) => { - const blockPatterns = useThemePatterns( - categoryId, - categoryType, - filterValue - ); +export const usePatterns = ( + categoryType, + categoryId, + { filterValue = '', syncFilter } +) => { + const blockPatterns = useThemePatterns( categoryId, categoryType, { + filterValue, + } ); - const { syncedPatterns = [], unsyncedPatterns = [] } = useUserPatterns( - categoryId, - categoryType, - filterValue - ); + const { patterns: userPatterns, isResolving: isResolvingUserPatterns } = + useUserPatterns( categoryId, categoryType, { + filterValue, + syncFilter, + } ); - const { templateParts, isResolving } = useTemplatePartsAsPatterns( - categoryId, - categoryType, - filterValue - ); + const { templateParts, isResolving: isResolvingTemplateParts } = + useTemplatePartsAsPatterns( categoryId, categoryType, { filterValue } ); - const patterns = { - syncedPatterns: [ ...templateParts, ...syncedPatterns ], - unsyncedPatterns: [ ...blockPatterns, ...unsyncedPatterns ], - }; + const patterns = [ ...templateParts, ...userPatterns, ...blockPatterns ]; - return [ patterns, isResolving ]; + return [ patterns, isResolvingUserPatterns && isResolvingTemplateParts ]; }; export default usePatterns; diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js index e3d5cc297164a..37f0b0f8a4e06 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-my-patterns.js @@ -6,18 +6,19 @@ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; export default function useMyPatterns() { - const myPatterns = useSelect( ( select ) => - select( coreStore ).getEntityRecords( 'postType', 'wp_block', { - per_page: -1, - } ) + const myPatternsCount = useSelect( + ( select ) => + select( coreStore ).getEntityRecords( 'postType', 'wp_block', { + per_page: -1, + } )?.length ?? 0 ); return { myPatterns: { - count: myPatterns?.length || 0, + count: myPatternsCount, name: 'my-patterns', label: __( 'My patterns' ), }, - hasPatterns: !! myPatterns?.length, + hasPatterns: myPatternsCount > 0, }; } From 88b88ab40eb9f312c8d9bad0943320e4a5442525 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Wed, 5 Jul 2023 15:32:17 +1000 Subject: [PATCH 02/15] grid for patterns --- .../src/components/page-patterns/grid.js | 49 +++---------------- .../src/components/page-patterns/style.scss | 13 ++--- 2 files changed, 15 insertions(+), 47 deletions(-) diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js index b70157fc1c1a1..dbfe0da064792 100644 --- a/packages/edit-site/src/components/page-patterns/grid.js +++ b/packages/edit-site/src/components/page-patterns/grid.js @@ -4,10 +4,8 @@ import { __unstableComposite as Composite, __unstableUseCompositeState as useCompositeState, - FlexBlock, Button, } from '@wordpress/components'; -import { useViewportMatch } from '@wordpress/compose'; import { useState, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -18,12 +16,8 @@ import GridItem from './grid-item'; const PAGE_SIZE = 100; -const isEven = ( _, index ) => index % 2 === 0; -const isOdd = ( _, index ) => index % 2 !== 0; - export default function Grid( { categoryId, items } ) { const composite = useCompositeState( { orientation: 'vertical' } ); - const isSmallerViewport = useViewportMatch( 'large', '<' ); const [ page, setPage ] = useState( 1 ); const gridRef = useRef(); @@ -42,41 +36,14 @@ export default function Grid( { categoryId, items } ) { className="edit-site-patterns__grid" ref={ gridRef } > - { isSmallerViewport ? ( - - { list.map( ( item ) => ( - - ) ) } - - ) : ( - <> - - { list.filter( isEven ).map( ( item ) => ( - - ) ) } - - - { list.filter( isOdd ).map( ( item ) => ( - - ) ) } - - - ) } + { list.map( ( item ) => ( + + ) ) } { items.length >= maxCount && ( diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index 93cca1d10a6bc..cb055c2ec956c 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -45,9 +45,11 @@ export default function PatternsList( { categoryId, type } ) { const deferredFilterValue = useDeferredValue( delayedFilterValue ); const [ syncFilter, setSyncFilter ] = useState( 'all' ); + const deferredSyncedFilter = useDeferredValue( syncFilter ); const [ patterns, isResolving ] = usePatterns( type, categoryId, { filterValue: deferredFilterValue, - syncFilter: syncFilter === 'all' ? undefined : syncFilter, + syncFilter: + deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter, } ); const hasPatterns = patterns.length; diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index 5caff5c0d20e4..ae25c113c04de 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -61,7 +61,7 @@ const useTemplatePartsAsPatterns = ( select( coreStore ); const query = { per_page: 5, - area: categoryId, + // area: categoryId, }; const rawTemplateParts = getEntityRecords( 'postType', @@ -81,7 +81,7 @@ const useTemplatePartsAsPatterns = ( ] ), }; }, - [ postType, categoryId ] + [ postType ] ); const filteredTemplateParts = useMemo( () => { @@ -165,23 +165,19 @@ const useUserPatterns = ( { filterValue = '', syncFilter } = {} ) => { const postType = categoryType === PATTERNS ? USER_PATTERNS : categoryType; - const { patterns, isResolving } = useSelect( + let { patterns, isResolving } = useSelect( ( select ) => { if ( postType !== USER_PATTERNS || categoryId !== USER_PATTERN_CATEGORY ) { - return EMPTY_PATTERN_LIST; + return { patterns: EMPTY_PATTERN_LIST, isResolving: false }; } const { getEntityRecords, isResolving: _isResolving } = select( coreStore ); - const query = { - per_page: -1, - sync_status: syncFilter, - search: filterValue || undefined, - }; + const query = { per_page: -1 }; const records = getEntityRecords( 'postType', postType, query ); return { @@ -197,21 +193,30 @@ const useUserPatterns = ( ] ), }; }, - [ postType, categoryId, syncFilter, filterValue ] + [ postType, categoryId ] ); - // const filteredPatterns = useMemo( () => { - // if ( ! unfilteredPatterns.length ) { - // return EMPTY_PATTERN_LIST; - // } + patterns = useMemo( () => { + if ( ! syncFilter ) { + return patterns; + } + return patterns.filter( + ( pattern ) => pattern.syncStatus === syncFilter + ); + }, [ patterns, syncFilter ] ); - // return searchItems( unfilteredPatterns, filterValue, { - // // We exit user pattern retrieval early if we aren't in the - // // catch-all category for user created patterns, so it has - // // to be in the category. - // hasCategory: () => true, - // } ); - // }, [ unfilteredPatterns, filterValue ] ); + patterns = useMemo( () => { + if ( ! patterns.length ) { + return EMPTY_PATTERN_LIST; + } + + return searchItems( patterns, filterValue, { + // We exit user pattern retrieval early if we aren't in the + // catch-all category for user created patterns, so it has + // to be in the category. + hasCategory: () => true, + } ); + }, [ patterns, filterValue ] ); return { patterns, isResolving }; }; @@ -234,9 +239,12 @@ export const usePatterns = ( const { templateParts, isResolving: isResolvingTemplateParts } = useTemplatePartsAsPatterns( categoryId, categoryType, { filterValue } ); - const patterns = [ ...templateParts, ...userPatterns, ...blockPatterns ]; + const patterns = useMemo( + () => [ ...templateParts, ...userPatterns, ...blockPatterns ], + [ templateParts, userPatterns, blockPatterns ] + ); - return [ patterns, isResolvingUserPatterns && isResolvingTemplateParts ]; + return [ patterns, isResolvingUserPatterns || isResolvingTemplateParts ]; }; export default usePatterns; From dbe0487dc6e8f3ea550a5857435963f6f28967fc Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Jul 2023 16:45:57 +0800 Subject: [PATCH 04/15] Remove rest api patch --- lib/compat/wordpress-6.3/blocks.php | 47 ----------------------------- 1 file changed, 47 deletions(-) diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php index 903d82f77b423..ccc68786dc6ad 100644 --- a/lib/compat/wordpress-6.3/blocks.php +++ b/lib/compat/wordpress-6.3/blocks.php @@ -120,50 +120,3 @@ function gutenberg_wp_block_register_post_meta() { ); } add_action( 'init', 'gutenberg_wp_block_register_post_meta' ); - -/** - * Allow querying blocks by sync_status. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @param array $args Array of arguments for WP_Query. - * @param WP_REST_Request $request The REST API request. - * - * @return array Updated array of arguments for WP_Query. - */ -function gutenberg_rest_wp_block_query( $args, $request ) { - if ( isset( $request['sync_status'] ) ) { - if ( 'fully' === $request['sync_status'] ) { - $sync_status_query = array( - 'relation' => 'OR', - array( - 'key' => 'sync_status', - 'value' => '', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => 'sync_status', - 'value' => 'fully', - 'compare' => '=', - ), - ); - } else { - $sync_status_query = array( - array( - 'key' => 'sync_status', - 'value' => sanitize_text_field( $request['sync_status'] ), - 'compare' => '=', - ), - ); - } - - if ( isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ) { - array_push( $args['meta_query'], $sync_status_query ); - } else { - $args['meta_query'] = $sync_status_query; - } - } - - return $args; -} -add_filter( 'rest_wp_block_query', 'gutenberg_rest_wp_block_query', 10, 2 ); From 73afc1ca67e8591e9c5c737555980e87747f44f9 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Jul 2023 17:53:20 +0800 Subject: [PATCH 05/15] Add header --- .../src/components/page-patterns/header.js | 64 +++++++++++++++++++ .../components/page-patterns/patterns-list.js | 12 +--- .../src/components/page-patterns/utils.js | 5 ++ 3 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 packages/edit-site/src/components/page-patterns/header.js diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js new file mode 100644 index 0000000000000..79ce6e116f6d7 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/header.js @@ -0,0 +1,64 @@ +/** + * WordPress dependencies + */ +import { + __experimentalVStack as VStack, + __experimentalHeading as Heading, + __experimentalText as Text, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { store as editorStore } from '@wordpress/editor'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; +import { + USER_PATTERN_CATEGORY, + USER_PATTERNS, + TEMPLATE_PARTS, + PATTERNS, +} from './utils'; + +export default function PatternsHeader( { categoryId, type } ) { + const { patternCategories } = usePatternCategories(); + const templatePartAreas = useSelect( + ( select ) => + select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), + [] + ); + + let title, description; + if ( categoryId === USER_PATTERN_CATEGORY && type === USER_PATTERNS ) { + title = __( 'My Patterns' ); + description = __( 'Patterns that are kept in sync across your site.' ); // TODO + } else if ( type === TEMPLATE_PARTS ) { + const templatePartArea = templatePartAreas.find( + ( area ) => area.area === categoryId + ); + title = templatePartArea?.label; + description = templatePartArea?.description; + } else if ( type === PATTERNS ) { + const patternCategory = patternCategories.find( + ( category ) => category.name === categoryId + ); + title = patternCategory?.label; + description = patternCategory?.description; + } + + if ( ! title ) return null; + + return ( + + + { title } + + { description ? ( + + { description } + + ) : null } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index cb055c2ec956c..49132dd9accd4 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -4,8 +4,6 @@ import { useState, useDeferredValue } from '@wordpress/element'; import { SearchControl, - // __experimentalHeading as Heading, - // __experimentalText as Text, __experimentalVStack as VStack, Flex, FlexBlock, @@ -20,6 +18,7 @@ import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ +import PatternsHeader from './header'; import Grid from './grid'; import NoPatterns from './no-patterns'; import usePatterns from './use-patterns'; @@ -56,14 +55,7 @@ export default function PatternsList( { categoryId, type } ) { return ( - { /* - - { __( 'Synced' ) } - - - { __( 'Patterns that are kept in sync across your site' ) } - - */ } + { isMobileViewport && ( diff --git a/packages/edit-site/src/components/page-patterns/utils.js b/packages/edit-site/src/components/page-patterns/utils.js index bbdff872fe355..e4cbfc9a05104 100644 --- a/packages/edit-site/src/components/page-patterns/utils.js +++ b/packages/edit-site/src/components/page-patterns/utils.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + export const DEFAULT_CATEGORY = 'my-patterns'; export const DEFAULT_TYPE = 'wp_block'; export const PATTERNS = 'pattern'; From 71dd4868cc77e848a05f6b6820da2d6042723ac2 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Wed, 5 Jul 2023 18:03:36 +0800 Subject: [PATCH 06/15] Fix some a11y issues --- .../src/components/page-patterns/grid.js | 3 ++- .../src/components/page-patterns/header.js | 11 +++++++--- .../components/page-patterns/patterns-list.js | 22 +++++++++++++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js index 44635dbf7b330..9aa7bb2ddbefa 100644 --- a/packages/edit-site/src/components/page-patterns/grid.js +++ b/packages/edit-site/src/components/page-patterns/grid.js @@ -16,7 +16,7 @@ import GridItem from './grid-item'; const PAGE_SIZE = 100; -export default function Grid( { categoryId, items } ) { +export default function Grid( { categoryId, items, ...props } ) { const composite = useCompositeState( { orientation: 'vertical' } ); const [ page, setPage ] = useState( 1 ); const [ nextFocusIndex, setNextFocusIndex ] = useState( -1 ); @@ -45,6 +45,7 @@ export default function Grid( { categoryId, items } ) { { ...composite } role="listbox" className="edit-site-patterns__grid" + { ...props } ref={ gridRef } > { list.map( ( item ) => ( diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js index 79ce6e116f6d7..6d493fd866349 100644 --- a/packages/edit-site/src/components/page-patterns/header.js +++ b/packages/edit-site/src/components/page-patterns/header.js @@ -21,7 +21,12 @@ import { PATTERNS, } from './utils'; -export default function PatternsHeader( { categoryId, type } ) { +export default function PatternsHeader( { + categoryId, + type, + titleId, + descriptionId, +} ) { const { patternCategories } = usePatternCategories(); const templatePartAreas = useSelect( ( select ) => @@ -51,11 +56,11 @@ export default function PatternsHeader( { categoryId, type } ) { return ( - + { title } { description ? ( - + { description } ) : null } diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index 49132dd9accd4..c227ec1cf00f2 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useState, useDeferredValue } from '@wordpress/element'; +import { useState, useDeferredValue, useId } from '@wordpress/element'; import { SearchControl, __experimentalVStack as VStack, @@ -51,11 +51,20 @@ export default function PatternsList( { categoryId, type } ) { deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter, } ); + const id = useId(); + const titleId = `${ id }-title`; + const descriptionId = `${ id }-description`; + const hasPatterns = patterns.length; return ( - + { isMobileViewport && ( @@ -87,7 +96,7 @@ export default function PatternsList( { categoryId, type } ) { setSyncFilter( value ) } @@ -109,7 +118,12 @@ export default function PatternsList( { categoryId, type } ) { { isResolving && __( 'Loading' ) } { ! isResolving && hasPatterns && ( - + ) } { ! isResolving && ! hasPatterns && } From e41089317e4e56047348a4f70e313078dcd5318d Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 6 Jul 2023 13:01:54 +1000 Subject: [PATCH 07/15] style changes patterns grid --- .../src/components/page-patterns/grid.js | 1 + .../src/components/page-patterns/style.scss | 29 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js index 9aa7bb2ddbefa..29c5d2e000f02 100644 --- a/packages/edit-site/src/components/page-patterns/grid.js +++ b/packages/edit-site/src/components/page-patterns/grid.js @@ -59,6 +59,7 @@ export default function Grid( { categoryId, items, ...props } ) { { items.length >= maxCount && ( + { restLength > 0 && ( + + { sprintf( + /* translators: %d: number of patterns */ + __( '+ %d more patterns' ), + restLength + ) } + ) } ); diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index c227ec1cf00f2..c53057b17aef9 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -45,9 +45,9 @@ export default function PatternsList( { categoryId, type } ) { const [ syncFilter, setSyncFilter ] = useState( 'all' ); const deferredSyncedFilter = useDeferredValue( syncFilter ); - const [ patterns, isResolving ] = usePatterns( type, categoryId, { - filterValue: deferredFilterValue, - syncFilter: + const { patterns, isResolving } = usePatterns( type, categoryId, { + search: deferredFilterValue, + syncStatus: deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter, } ); @@ -116,8 +116,7 @@ export default function PatternsList( { categoryId, type } ) { ) } - { isResolving && __( 'Loading' ) } - { ! isResolving && hasPatterns && ( + { hasPatterns && ( ( { const templatePartHasCategory = ( item, category ) => item.templatePart.area === category; -const useTemplatePartsAsPatterns = ( - categoryId, - postType = TEMPLATE_PARTS, - { filterValue = '' } = {} +const selectTemplatePartsAsPatterns = ( + select, + { categoryId, search = '' } = {} ) => { - const { templateParts, isResolving } = useSelect( - ( select ) => { - if ( postType !== TEMPLATE_PARTS ) { - return { - templateParts: EMPTY_PATTERN_LIST, - isResolving: false, - }; - } - - const { getEntityRecords, isResolving: _isResolving } = - select( coreStore ); - const query = { per_page: -1 }; - const rawTemplateParts = getEntityRecords( - 'postType', - postType, - query - ); - const partsAsPatterns = rawTemplateParts?.map( ( templatePart ) => - templatePartToPattern( templatePart ) - ); - - return { - templateParts: partsAsPatterns, - isResolving: _isResolving( 'getEntityRecords', [ - 'postType', - 'wp_template_part', - query, - ] ), - }; - }, - [ postType ] + const { getEntityRecords, getIsResolving } = select( coreStore ); + const query = { per_page: -1 }; + const rawTemplateParts = + getEntityRecords( 'postType', TEMPLATE_PARTS, query ) ?? + EMPTY_PATTERN_LIST; + const templateParts = rawTemplateParts.map( ( templatePart ) => + templatePartToPattern( templatePart ) ); - const filteredTemplateParts = useMemo( () => { - if ( ! templateParts ) { - return EMPTY_PATTERN_LIST; - } + const isResolving = getIsResolving( 'getEntityRecords', [ + 'postType', + 'wp_template_part', + query, + ] ); - return searchItems( templateParts, filterValue, { - categoryId, - hasCategory: templatePartHasCategory, - } ); - }, [ templateParts, filterValue, categoryId ] ); + const patterns = searchItems( templateParts, search, { + categoryId, + hasCategory: templatePartHasCategory, + } ); - return { templateParts: filteredTemplateParts, isResolving }; + return { patterns, isResolving }; }; -const useThemePatterns = ( - categoryId, - postType = PATTERNS, - { filterValue = '' } = {} -) => { - const blockPatterns = useSelect( ( select ) => { - const { getSettings } = unlock( select( editSiteStore ) ); - const settings = getSettings(); - return ( - settings.__experimentalAdditionalBlockPatterns ?? - settings.__experimentalBlockPatterns - ); +const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + const settings = getSettings(); + const blockPatterns = + settings.__experimentalAdditionalBlockPatterns ?? + settings.__experimentalBlockPatterns; + + const restBlockPatterns = select( coreStore ).getBlockPatterns(); + + let patterns = [ + ...( blockPatterns || [] ), + ...( restBlockPatterns || [] ), + ] + .filter( + ( pattern ) => ! CORE_PATTERN_SOURCES.includes( pattern.source ) + ) + .filter( filterOutDuplicatesByName ) + .map( ( pattern ) => ( { + ...pattern, + keywords: pattern.keywords || [], + type: 'pattern', + blocks: parse( pattern.content ), + } ) ); + + patterns = searchItems( patterns, search, { + categoryId, + hasCategory: ( item, currentCategory ) => + item.categories?.includes( currentCategory ), } ); - const restBlockPatterns = useSelect( ( select ) => - select( coreStore ).getBlockPatterns() - ); - - const patterns = useMemo( - () => - [ ...( blockPatterns || [] ), ...( restBlockPatterns || [] ) ] - .filter( - ( pattern ) => - ! CORE_PATTERN_SOURCES.includes( pattern.source ) - ) - .filter( filterOutDuplicatesByName ) - .map( ( pattern ) => ( { - ...pattern, - keywords: pattern.keywords || [], - type: 'pattern', - blocks: parse( pattern.content ), - } ) ), - [ blockPatterns, restBlockPatterns ] - ); - - const filteredPatterns = useMemo( () => { - if ( postType !== PATTERNS ) { - return EMPTY_PATTERN_LIST; - } - - return searchItems( patterns, filterValue, { - categoryId, - hasCategory: ( item, currentCategory ) => - item.categories?.includes( currentCategory ), - } ); - }, [ patterns, filterValue, categoryId, postType ] ); - - return filteredPatterns; + return { patterns, isResolving: false }; }; const reusableBlockToPattern = ( reusableBlock ) => ( { @@ -156,64 +112,33 @@ const reusableBlockToPattern = ( reusableBlock ) => ( { reusableBlock, } ); -const useUserPatterns = ( - categoryId, - categoryType = PATTERNS, - { filterValue = '', syncFilter } = {} -) => { - const postType = categoryType === PATTERNS ? USER_PATTERNS : categoryType; - let { patterns, isResolving } = useSelect( - ( select ) => { - if ( - postType !== USER_PATTERNS || - categoryId !== USER_PATTERN_CATEGORY - ) { - return { patterns: EMPTY_PATTERN_LIST, isResolving: false }; - } +const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => { + const { getEntityRecords, getIsResolving } = select( coreStore ); - const { getEntityRecords, isResolving: _isResolving } = - select( coreStore ); - - const query = { per_page: -1 }; - const records = getEntityRecords( 'postType', postType, query ); - - return { - patterns: records - ? records.map( ( record ) => - reusableBlockToPattern( record ) - ) - : EMPTY_PATTERN_LIST, - isResolving: _isResolving( 'getEntityRecords', [ - 'postType', - postType, - query, - ] ), - }; - }, - [ postType, categoryId ] - ); + const query = { per_page: -1 }; + const records = getEntityRecords( 'postType', USER_PATTERNS, query ); - patterns = useMemo( () => { - if ( ! syncFilter ) { - return patterns; - } - return patterns.filter( - ( pattern ) => pattern.syncStatus === syncFilter - ); - }, [ patterns, syncFilter ] ); + let patterns = records + ? records.map( ( record ) => reusableBlockToPattern( record ) ) + : EMPTY_PATTERN_LIST; + const isResolving = getIsResolving( 'getEntityRecords', [ + 'postType', + USER_PATTERNS, + query, + ] ); - patterns = useMemo( () => { - if ( ! patterns.length ) { - return EMPTY_PATTERN_LIST; - } + if ( syncStatus ) { + patterns = patterns.filter( + ( pattern ) => pattern.syncStatus === syncStatus + ); + } - return searchItems( patterns, filterValue, { - // We exit user pattern retrieval early if we aren't in the - // catch-all category for user created patterns, so it has - // to be in the category. - hasCategory: () => true, - } ); - }, [ patterns, filterValue ] ); + patterns = searchItems( patterns, search, { + // We exit user pattern retrieval early if we aren't in the + // catch-all category for user created patterns, so it has + // to be in the category. + hasCategory: () => true, + } ); return { patterns, isResolving }; }; @@ -221,27 +146,24 @@ const useUserPatterns = ( export const usePatterns = ( categoryType, categoryId, - { filterValue = '', syncFilter } + { search = '', syncStatus } ) => { - const blockPatterns = useThemePatterns( categoryId, categoryType, { - filterValue, - } ); - - const { patterns: userPatterns, isResolving: isResolvingUserPatterns } = - useUserPatterns( categoryId, categoryType, { - filterValue, - syncFilter, - } ); - - const { templateParts, isResolving: isResolvingTemplateParts } = - useTemplatePartsAsPatterns( categoryId, categoryType, { filterValue } ); - - const patterns = useMemo( - () => [ ...templateParts, ...userPatterns, ...blockPatterns ], - [ templateParts, userPatterns, blockPatterns ] + return useSelect( + ( select ) => { + if ( categoryType === TEMPLATE_PARTS ) { + return selectTemplatePartsAsPatterns( select, { + categoryId, + search, + } ); + } else if ( categoryType === PATTERNS ) { + return selectThemePatterns( select, { categoryId, search } ); + } else if ( categoryType === USER_PATTERNS ) { + return selectUserPatterns( select, { search, syncStatus } ); + } + return { patterns: EMPTY_PATTERN_LIST, isResolving: false }; + }, + [ categoryId, categoryType, search, syncStatus ] ); - - return [ patterns, isResolvingUserPatterns || isResolvingTemplateParts ]; }; export default usePatterns; From 07c92f6eeb6665b93fc7cac99fad24f81682dbc6 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 7 Jul 2023 13:59:25 +0800 Subject: [PATCH 12/15] Fix lint error --- packages/edit-site/src/components/page-patterns/utils.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/edit-site/src/components/page-patterns/utils.js b/packages/edit-site/src/components/page-patterns/utils.js index e4cbfc9a05104..bbdff872fe355 100644 --- a/packages/edit-site/src/components/page-patterns/utils.js +++ b/packages/edit-site/src/components/page-patterns/utils.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - export const DEFAULT_CATEGORY = 'my-patterns'; export const DEFAULT_TYPE = 'wp_block'; export const PATTERNS = 'pattern'; From a5a9ae7ce1f3edddbbca2a9bde882562fdce37dc Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 7 Jul 2023 14:55:26 +0800 Subject: [PATCH 13/15] Add comment --- .../edit-site/src/components/page-patterns/grid-item.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js index d60365c8f32a9..d8e3b2fe16d53 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -124,9 +124,8 @@ function GridItem( { categoryId, item, ...props } ) { } const itemIcon = - item.syncStatus === SYNC_TYPES.full - ? symbol - : templatePartIcons[ categoryId ]; + templatePartIcons[ categoryId ] || + ( item.syncStatus === SYNC_TYPES.full ? symbol : undefined ); const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' ); const confirmPrompt = hasThemeFile @@ -144,6 +143,8 @@ function GridItem( { categoryId, item, ...props } ) { className={ previewClassNames } role="option" as="div" + // Even though still incomplete, passing ids helps performance. + // @see https://reakit.io/docs/composite/#performance. id={ `edit-site-patterns-${ item.name }` } { ...props } onClick={ item.type !== PATTERNS ? onClick : undefined } From e6068c11a6801cfb548077c8294ae4992975213a Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 7 Jul 2023 15:47:37 +0800 Subject: [PATCH 14/15] Reset states --- packages/edit-site/src/components/page-patterns/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 961ed51f39e5d..d90fc74844244 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -32,7 +32,12 @@ export default function PagePatterns() { title={ __( 'Patterns content' ) } hideTitleFromUI > - + ); From 4e7021274cf9a58304b135d50dccb3891e8d916c Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 7 Jul 2023 01:20:19 +0800 Subject: [PATCH 15/15] WIP --- lib/compat/wordpress-6.3/blocks.php | 47 ++++++ .../components/page-patterns/pagination.js | 68 +++++++++ .../components/page-patterns/patterns-list.js | 51 +++++-- .../components/page-patterns/use-patterns.js | 138 ++++++++++++------ packages/router/src/router.js | 5 +- 5 files changed, 252 insertions(+), 57 deletions(-) create mode 100644 packages/edit-site/src/components/page-patterns/pagination.js diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php index ccc68786dc6ad..903d82f77b423 100644 --- a/lib/compat/wordpress-6.3/blocks.php +++ b/lib/compat/wordpress-6.3/blocks.php @@ -120,3 +120,50 @@ function gutenberg_wp_block_register_post_meta() { ); } add_action( 'init', 'gutenberg_wp_block_register_post_meta' ); + +/** + * Allow querying blocks by sync_status. + * + * Note: This should be removed when the minimum required WP version is >= 6.3. + * + * @param array $args Array of arguments for WP_Query. + * @param WP_REST_Request $request The REST API request. + * + * @return array Updated array of arguments for WP_Query. + */ +function gutenberg_rest_wp_block_query( $args, $request ) { + if ( isset( $request['sync_status'] ) ) { + if ( 'fully' === $request['sync_status'] ) { + $sync_status_query = array( + 'relation' => 'OR', + array( + 'key' => 'sync_status', + 'value' => '', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'sync_status', + 'value' => 'fully', + 'compare' => '=', + ), + ); + } else { + $sync_status_query = array( + array( + 'key' => 'sync_status', + 'value' => sanitize_text_field( $request['sync_status'] ), + 'compare' => '=', + ), + ); + } + + if ( isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ) { + array_push( $args['meta_query'], $sync_status_query ); + } else { + $args['meta_query'] = $sync_status_query; + } + } + + return $args; +} +add_filter( 'rest_wp_block_query', 'gutenberg_rest_wp_block_query', 10, 2 ); diff --git a/packages/edit-site/src/components/page-patterns/pagination.js b/packages/edit-site/src/components/page-patterns/pagination.js new file mode 100644 index 0000000000000..7f52fd911a9d2 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/pagination.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies + */ +import { + useLayoutEffect, + useEffect, + useRef, + useState, + startTransition, +} from '@wordpress/element'; +import { __experimentalHStack as HStack, Button } from '@wordpress/components'; + +export default function PatternsPagination( { + patterns, + page, + setPage, + getTotalPages, +} ) { + const [ totalPages, setTotalPages ] = useState( page ); + const getTotalPagesRef = useRef( getTotalPages ); + useLayoutEffect( () => { + getTotalPagesRef.current = getTotalPages; + } ); + + // Refetch total pages when `patterns` changes. + // This is not a good indicator of when to refetch the total pages, + // but the only one we have for now. + useEffect( () => { + const abortController = new AbortController(); + const signal = abortController.signal; + getTotalPagesRef + .current( { signal } ) + .then( ( pages ) => setTotalPages( pages ) ) + .catch( () => setTotalPages( 1 ) ); + return () => { + abortController.abort(); + }; + }, [ patterns ] ); + + const pages = Array.from( + { length: totalPages }, + ( _, index ) => index + 1 + ); + + return ( + + { pages.map( ( p ) => + p === page ? ( + + { p } + + ) : ( + + ) + ) } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index c53057b17aef9..c4f2ce0b4ccc8 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -22,6 +22,7 @@ import PatternsHeader from './header'; import Grid from './grid'; import NoPatterns from './no-patterns'; import usePatterns from './use-patterns'; +import PatternsPagination from './pagination'; import SidebarButton from '../sidebar-button'; import useDebouncedInput from '../../utils/use-debounced-input'; import { unlock } from '../../lock-unlock'; @@ -39,17 +40,25 @@ export default function PatternsList( { categoryId, type } ) { const location = useLocation(); const history = useHistory(); const isMobileViewport = useViewportMatch( 'medium', '<' ); + const [ page, setPage ] = useState( 1 ); const [ filterValue, setFilterValue, delayedFilterValue ] = useDebouncedInput( '' ); const deferredFilterValue = useDeferredValue( delayedFilterValue ); - const [ syncFilter, setSyncFilter ] = useState( 'all' ); const deferredSyncedFilter = useDeferredValue( syncFilter ); - const { patterns, isResolving } = usePatterns( type, categoryId, { - search: deferredFilterValue, - syncStatus: - deferredSyncedFilter === 'all' ? undefined : deferredSyncedFilter, - } ); + + const { patterns, isResolving, getTotalPages } = usePatterns( + type, + categoryId, + { + search: deferredFilterValue, + page, + syncStatus: + deferredSyncedFilter === 'all' + ? undefined + : deferredSyncedFilter, + } + ); const id = useId(); const titleId = `${ id }-title`; @@ -85,7 +94,10 @@ export default function PatternsList( { categoryId, type } ) { setFilterValue( value ) } + onChange={ ( value ) => { + setFilterValue( value ); + setPage( 1 ); + } } placeholder={ __( 'Search patterns' ) } label={ __( 'Search patterns' ) } value={ filterValue } @@ -99,7 +111,10 @@ export default function PatternsList( { categoryId, type } ) { label={ __( 'Filter by sync status' ) } value={ syncFilter } isBlock - onChange={ ( value ) => setSyncFilter( value ) } + onChange={ ( value ) => { + setSyncFilter( value ); + setPage( 1 ); + } } __nextHasNoMarginBottom > { Object.entries( SYNC_FILTERS ).map( @@ -117,12 +132,20 @@ export default function PatternsList( { categoryId, type } ) { { hasPatterns && ( - + <> + + + ) } { ! isResolving && ! hasPatterns && } diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index 0bcc52c85cb62..9d3045547402f 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -4,6 +4,8 @@ import { parse } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -38,37 +40,58 @@ const templatePartToPattern = ( templatePart ) => ( { templatePart, } ); -const templatePartHasCategory = ( item, category ) => - item.templatePart.area === category; - const selectTemplatePartsAsPatterns = ( select, - { categoryId, search = '' } = {} + { categoryId, search = '', page = 1 } = {} ) => { - const { getEntityRecords, getIsResolving } = select( coreStore ); - const query = { per_page: -1 }; - const rawTemplateParts = + const { getEntityRecords, getIsResolving, getEntityConfig } = + select( coreStore ); + const query = { + per_page: 20, + page, + area: categoryId, + // TODO: The template parts REST API doesn't support searching yet. + search, + search_columns: 'post_title', + }; + const templateParts = getEntityRecords( 'postType', TEMPLATE_PARTS, query ) ?? EMPTY_PATTERN_LIST; - const templateParts = rawTemplateParts.map( ( templatePart ) => + const patterns = templateParts.map( ( templatePart ) => templatePartToPattern( templatePart ) ); const isResolving = getIsResolving( 'getEntityRecords', [ 'postType', - 'wp_template_part', + TEMPLATE_PARTS, query, ] ); - const patterns = searchItems( templateParts, search, { - categoryId, - hasCategory: templatePartHasCategory, - } ); - - return { patterns, isResolving }; + const getTotalPages = async ( { signal } = {} ) => { + const entityConfig = getEntityConfig( 'postType', TEMPLATE_PARTS ); + const response = await apiFetch( { + path: addQueryArgs( entityConfig.baseURL, { + ...entityConfig.baseURLParams, + ...query, + } ), + method: 'HEAD', + parse: false, + signal, + } ); + return parseInt( response.headers.get( 'X-WP-Totalpages' ), 10 ); + }; + + return { + patterns, + isResolving, + getTotalPages, + }; }; -const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { +const selectThemePatterns = ( + select, + { categoryId, search = '', page = 1 } = {} +) => { const { getSettings } = unlock( select( editSiteStore ) ); const settings = getSettings(); const blockPatterns = @@ -98,7 +121,13 @@ const selectThemePatterns = ( select, { categoryId, search = '' } = {} ) => { item.categories?.includes( currentCategory ), } ); - return { patterns, isResolving: false }; + patterns = patterns.slice( ( page - 1 ) * 20, page * 20 ); + + return { + patterns, + isResolving: false, + getTotalPages: async () => Math.ceil( patterns.length / 20 ), + }; }; const reusableBlockToPattern = ( reusableBlock ) => ( { @@ -112,41 +141,53 @@ const reusableBlockToPattern = ( reusableBlock ) => ( { reusableBlock, } ); -const selectUserPatterns = ( select, { search = '', syncStatus } = {} ) => { - const { getEntityRecords, getIsResolving } = select( coreStore ); - - const query = { per_page: -1 }; +const selectUserPatterns = ( + select, + { search = '', syncStatus, page = 1 } = {} +) => { + const { getEntityRecords, getIsResolving, getEntityConfig } = + select( coreStore ); + + const query = { + per_page: 20, + page, + search, + search_columns: 'post_title', + sync_status: syncStatus, + }; const records = getEntityRecords( 'postType', USER_PATTERNS, query ); - let patterns = records - ? records.map( ( record ) => reusableBlockToPattern( record ) ) - : EMPTY_PATTERN_LIST; const isResolving = getIsResolving( 'getEntityRecords', [ 'postType', USER_PATTERNS, query, ] ); - if ( syncStatus ) { - patterns = patterns.filter( - ( pattern ) => pattern.syncStatus === syncStatus - ); - } - - patterns = searchItems( patterns, search, { - // We exit user pattern retrieval early if we aren't in the - // catch-all category for user created patterns, so it has - // to be in the category. - hasCategory: () => true, - } ); + const patterns = records + ? records.map( ( record ) => reusableBlockToPattern( record ) ) + : EMPTY_PATTERN_LIST; - return { patterns, isResolving }; + const getTotalPages = async ( { signal } = {} ) => { + const entityConfig = getEntityConfig( 'postType', USER_PATTERNS ); + const response = await apiFetch( { + path: addQueryArgs( entityConfig.baseURL, { + ...entityConfig.baseURLParams, + ...query, + } ), + method: 'HEAD', + parse: false, + signal, + } ); + return parseInt( response.headers.get( 'X-Wp-Totalpages' ), 10 ); + }; + + return { patterns, isResolving, getTotalPages }; }; export const usePatterns = ( categoryType, categoryId, - { search = '', syncStatus } + { search = '', page = 1, syncStatus } ) => { return useSelect( ( select ) => { @@ -154,15 +195,28 @@ export const usePatterns = ( return selectTemplatePartsAsPatterns( select, { categoryId, search, + page, } ); } else if ( categoryType === PATTERNS ) { - return selectThemePatterns( select, { categoryId, search } ); + return selectThemePatterns( select, { + categoryId, + search, + page, + } ); } else if ( categoryType === USER_PATTERNS ) { - return selectUserPatterns( select, { search, syncStatus } ); + return selectUserPatterns( select, { + search, + syncStatus, + page, + } ); } - return { patterns: EMPTY_PATTERN_LIST, isResolving: false }; + return { + patterns: EMPTY_PATTERN_LIST, + isResolving: false, + getTotalPages: async () => 1, + }; }, - [ categoryId, categoryType, search, syncStatus ] + [ categoryType, categoryId, search, page, syncStatus ] ); }; diff --git a/packages/router/src/router.js b/packages/router/src/router.js index e5449cef54c6a..d88f7a4522616 100644 --- a/packages/router/src/router.js +++ b/packages/router/src/router.js @@ -6,6 +6,7 @@ import { useState, useEffect, useContext, + startTransition, } from '@wordpress/element'; /** @@ -39,7 +40,9 @@ export function RouterProvider( { children } ) { useEffect( () => { return history.listen( ( { location: updatedLocation } ) => { - setLocation( getLocationWithParams( updatedLocation ) ); + startTransition( () => { + setLocation( getLocationWithParams( updatedLocation ) ); + } ); } ); }, [] );