From f4389b9851256d1f02b513d8379ee544e446af98 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:41:38 -0800 Subject: [PATCH 01/20] `ToggleGroupControl`: Add opt-in prop for 40px default size (#55789) * `ToggleGroupControl`: Add opt-in prop for 40px default size * Update changelog --- packages/components/CHANGELOG.md | 4 ++++ .../toggle-group-control/component.tsx | 10 ++++++++-- .../toggle-group-control/styles.ts | 17 +++++++++++++---- .../src/toggle-group-control/types.ts | 6 ++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 39ec52d6e11856..72edad0df4e10e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - Migrate `Divider` from `reakit` to `ariakit` ([#55622](https://github.com/WordPress/gutenberg/pull/55622)) +### Enhancements + +- `ToggleGroupControl`: Add opt-in prop for 40px default size ([#55789](https://github.com/WordPress/gutenberg/pull/55789)). + ## 25.11.0 (2023-11-02) ### Enhancements diff --git a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx index 924c5ecb587466..1d48349f0ee799 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx @@ -31,6 +31,7 @@ function UnconnectedToggleGroupControl( ) { const { __nextHasNoMarginBottom = false, + __next40pxDefaultSize = false, className, isAdaptiveWidth = false, isBlock = false, @@ -52,11 +53,16 @@ function UnconnectedToggleGroupControl( const classes = useMemo( () => cx( - styles.toggleGroupControl( { isBlock, isDeselectable, size } ), + styles.toggleGroupControl( { + isBlock, + isDeselectable, + size, + __next40pxDefaultSize, + } ), isBlock && styles.block, className ), - [ className, cx, isBlock, isDeselectable, size ] + [ className, cx, isBlock, isDeselectable, size, __next40pxDefaultSize ] ); const MainControl = isDeselectable diff --git a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts index 5d0b90096024ce..b8a8552f98beb0 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts @@ -14,7 +14,11 @@ export const toggleGroupControl = ( { isBlock, isDeselectable, size, -}: Pick< ToggleGroupControlProps, 'isBlock' | 'isDeselectable' > & { + __next40pxDefaultSize, +}: Pick< + ToggleGroupControlProps, + 'isBlock' | 'isDeselectable' | '__next40pxDefaultSize' +> & { size: NonNullable< ToggleGroupControlProps[ 'size' ] >; } ) => css` background: ${ COLORS.ui.background }; @@ -25,7 +29,7 @@ export const toggleGroupControl = ( { padding: 2px; position: relative; - ${ toggleGroupControlSize( size ) } + ${ toggleGroupControlSize( size, __next40pxDefaultSize ) } ${ ! isDeselectable && enclosingBorders( isBlock ) } `; @@ -53,13 +57,18 @@ const enclosingBorders = ( isBlock: ToggleGroupControlProps[ 'isBlock' ] ) => { }; export const toggleGroupControlSize = ( - size: NonNullable< ToggleGroupControlProps[ 'size' ] > + size: NonNullable< ToggleGroupControlProps[ 'size' ] >, + __next40pxDefaultSize: ToggleGroupControlProps[ '__next40pxDefaultSize' ] ) => { const heights = { - default: '36px', + default: '40px', '__unstable-large': '40px', }; + if ( ! __next40pxDefaultSize ) { + heights.default = '36px'; + } + return css` min-height: ${ heights[ size ] }; `; diff --git a/packages/components/src/toggle-group-control/types.ts b/packages/components/src/toggle-group-control/types.ts index d170997c5b2608..d49ef3cbb77cb4 100644 --- a/packages/components/src/toggle-group-control/types.ts +++ b/packages/components/src/toggle-group-control/types.ts @@ -122,6 +122,12 @@ export type ToggleGroupControlProps = Pick< * @default 'default' */ size?: 'default' | '__unstable-large'; + /** + * Start opting into the larger default height that will become the default size in a future version. + * + * @default false + */ + __next40pxDefaultSize?: boolean; }; export type ToggleGroupControlContextProps = { From 294774aeaa17cf3a5c9b247c3c0156061da0059d Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 7 Nov 2023 15:09:03 +1100 Subject: [PATCH 02/20] Patterns: use existing download function for JSON downloads to fix non-ASCII encoding (#55912) * downloadjs has a bug where it cannot decode non-ASCII characters. Gutenberg's reusable blocks code has an existing download function that we can use for JSON. This commit removes the downloadjs dependency and copies over the reusable blocks download function. * Added comment to explain duplicate * Remove IE11-specific code. --- .../src/components/page-patterns/grid-item.js | 24 ++++++++++++++++--- 1 file changed, 21 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 873f71f2f108d1..64b70fb87de62d 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import downloadjs from 'downloadjs'; import { paramCase as kebabCase } from 'change-case'; /** @@ -52,6 +51,25 @@ import { store as editSiteStore } from '../../store'; import { useLink } from '../routes/link'; import { unlock } from '../../lock-unlock'; +/** + * Downloads a file. + * Also used in packages/list-reusable-blocks/src/utils/file.js. + * + * @param {string} fileName File Name. + * @param {string} content File Content. + * @param {string} contentType File mime type. + */ +function download( fileName, content, contentType ) { + const file = new window.Blob( [ content ], { type: contentType } ); + const a = document.createElement( 'a' ); + a.href = URL.createObjectURL( file ); + a.download = fileName; + a.style.display = 'none'; + document.body.appendChild( a ); + a.click(); + document.body.removeChild( a ); +} + const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const templatePartIcons = { header, footer, uncategorized }; @@ -118,9 +136,9 @@ function GridItem( { categoryId, item, ...props } ) { syncStatus: item.patternBlock.wp_pattern_sync_status, }; - return downloadjs( - JSON.stringify( json, null, 2 ), + return download( `${ kebabCase( item.title || item.name ) }.json`, + JSON.stringify( json, null, 2 ), 'application/json' ); }; From 43bf16f98b186901ca3371a2ec25f0ff4a5566e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 7 Nov 2023 08:07:42 +0100 Subject: [PATCH 03/20] Dataviews: add filters in columns (#55508) --- .../src/components/dataviews/view-list.js | 175 +++++++++++++++++- packages/icons/CHANGELOG.md | 4 + packages/icons/src/index.js | 1 + packages/icons/src/library/funnel.js | 12 ++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 packages/icons/src/library/funnel.js diff --git a/packages/edit-site/src/components/dataviews/view-list.js b/packages/edit-site/src/components/dataviews/view-list.js index 0fbdd6ced34c56..cddf316562f346 100644 --- a/packages/edit-site/src/components/dataviews/view-list.js +++ b/packages/edit-site/src/components/dataviews/view-list.js @@ -21,6 +21,8 @@ import { check, arrowUp, arrowDown, + chevronRightSmall, + funnel, } from '@wordpress/icons'; import { Button, @@ -41,6 +43,8 @@ const { DropdownMenuGroupV2, DropdownMenuItemV2, DropdownMenuSeparatorV2, + DropdownSubMenuV2, + DropdownSubMenuTriggerV2, } = unlock( componentsPrivateApis ); const EMPTY_OBJECT = {}; @@ -63,6 +67,29 @@ function HeaderMenu( { dataView, header } ) { return text; } const sortedDirection = header.column.getIsSorted(); + + let filter; + if ( + header.column.columnDef.filters?.length > 0 && + header.column.columnDef.filters.some( + ( f ) => + ( 'string' === typeof f && f === 'enumeration' ) || + ( 'object' === typeof f && f.type === 'enumeration' ) + ) + ) { + filter = { + id: header.column.columnDef.id, + elements: [ + { + value: '', + label: __( 'All' ), + }, + ...( header.column.columnDef.elements || [] ), + ], + }; + } + const isFilterable = !! filter; + return ( ) } + { isFilterable && ( + + } + suffix={ + + } + > + { __( 'Filter by' ) } + + } + > + { filter.elements.map( ( element ) => { + let isActive = false; + const columnFilters = + dataView.getState().columnFilters; + const columnFilter = columnFilters.find( + ( f ) => + Object.keys( f )[ 0 ].split( + ':' + )[ 0 ] === filter.id + ); + + // Set the empty item as active if the filter is not set. + if ( ! columnFilter && element.value === '' ) { + isActive = true; + } + + if ( columnFilter ) { + const value = + Object.values( columnFilter )[ 0 ]; + // Intentionally use loose comparison, so it does type conversion. + // This covers the case where a top-level filter for the same field converts a number into a string. + isActive = element.value == value; // eslint-disable-line eqeqeq + } + + return ( + + } + onSelect={ () => { + const otherFilters = + columnFilters?.filter( + ( f ) => { + const [ + field, + operator, + ] = + Object.keys( + f + )[ 0 ].split( ':' ); + return ( + field !== + filter.id || + operator !== 'in' + ); + } + ); + + if ( element.value === '' ) { + dataView.setColumnFilters( + otherFilters + ); + } else { + dataView.setColumnFilters( [ + ...otherFilters, + { + [ filter.id + ':in' ]: + element.value, + }, + ] ); + } + } } + > + { element.label } + + ); + } ) } + + + ) } ); @@ -186,6 +299,58 @@ function ViewList( { ); }, [ view.hiddenFields ] ); + /** + * Transform the filters from the view format into the tanstack columns filter format. + * + * Input: + * + * view.filters = [ + * { field: 'date', operator: 'before', value: '2020-01-01' }, + * { field: 'date', operator: 'after', value: '2020-01-01' }, + * ] + * + * Output: + * + * columnFilters = [ + * { "date:before": '2020-01-01' }, + * { "date:after": '2020-01-01' } + * ] + * + * @param {Array} filters The view filters to transform. + * @return {Array} The transformed TanStack column filters. + */ + const toTanStackColumnFilters = ( filters ) => + filters.map( ( filter ) => ( { + [ filter.field + ':' + filter.operator ]: filter.value, + } ) ); + + /** + * Transform the filters from the view format into the tanstack columns filter format. + * + * Input: + * + * columnFilters = [ + * { "date:before": '2020-01-01'}, + * { "date:after": '2020-01-01' } + * ] + * + * Output: + * + * view.filters = [ + * { field: 'date', operator: 'before', value: '2020-01-01' }, + * { field: 'date', operator: 'after', value: '2020-01-01' }, + * ] + * + * @param {Array} filters The TanStack column filters to transform. + * @return {Array} The transformed view filters. + */ + const fromTanStackColumnFilters = ( filters ) => + filters.map( ( filter ) => { + const [ key, value ] = Object.entries( filter )[ 0 ]; + const [ field, operator ] = key.split( ':' ); + return { field, operator, value }; + } ); + const dataView = useReactTable( { data, columns, @@ -203,6 +368,7 @@ function ViewList( { ] : [], globalFilter: view.search, + columnFilters: toTanStackColumnFilters( view.filters ), pagination: { pageIndex: view.page, pageSize: view.perPage, @@ -261,7 +427,14 @@ function ViewList( { } ); }, onGlobalFilterChange: ( value ) => { - onChangeView( { ...view, search: value, page: 0 } ); + onChangeView( { ...view, search: value, page: 1 } ); + }, + onColumnFiltersChange: ( columnFiltersUpdater ) => { + onChangeView( { + ...view, + filters: fromTanStackColumnFilters( columnFiltersUpdater() ), + page: 1, + } ); }, onPaginationChange: ( paginationUpdater ) => { onChangeView( ( currentView ) => { diff --git a/packages/icons/CHANGELOG.md b/packages/icons/CHANGELOG.md index 0b7ccc517defaa..d99b95eb4bf371 100644 --- a/packages/icons/CHANGELOG.md +++ b/packages/icons/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New features + +- Add new `funnel` icon. + ## 9.36.0 (2023-11-02) ## 9.35.0 (2023-10-18) diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index f135e77e066bed..bb078b348e6043 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -95,6 +95,7 @@ export { default as formatStrikethrough } from './library/format-strikethrough'; export { default as formatUnderline } from './library/format-underline'; export { default as formatUppercase } from './library/format-uppercase'; export { default as fullscreen } from './library/fullscreen'; +export { default as funnel } from './library/funnel'; export { default as gallery } from './library/gallery'; export { default as globe } from './library/globe'; export { default as grid } from './library/grid'; diff --git a/packages/icons/src/library/funnel.js b/packages/icons/src/library/funnel.js new file mode 100644 index 00000000000000..87687dc7608db9 --- /dev/null +++ b/packages/icons/src/library/funnel.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const funnel = ( + + + +); + +export default funnel; From a3f6e952db5c60e9197d0881b832d0efde186573 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 7 Nov 2023 09:26:16 +0200 Subject: [PATCH 04/20] [Core data]: Fix wrong store results when page receives less items that what is stored (#55832) * [Core data]: Fix wrong store results when page receives less items that what is stored * continue only if itemid is undefined --- packages/core-data/src/queried-data/reducer.js | 6 +++--- packages/core-data/src/queried-data/selectors.js | 4 +++- packages/core-data/src/queried-data/test/reducer.js | 11 +++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/core-data/src/queried-data/reducer.js b/packages/core-data/src/queried-data/reducer.js index 40b0a9ba1f080b..3462f00b685693 100644 --- a/packages/core-data/src/queried-data/reducer.js +++ b/packages/core-data/src/queried-data/reducer.js @@ -56,10 +56,10 @@ export function getMergedItemIds( itemIds, nextItemIds, page, perPage ) { for ( let i = 0; i < size; i++ ) { // Preserve existing item ID except for subset of range of next items. + // We need to check against the possible maximum upper boundary because + // a page could recieve less items than what was previously stored. const isInNextItemsRange = - i >= nextItemIdsStartIndex && - i < nextItemIdsStartIndex + nextItemIds.length; - + i >= nextItemIdsStartIndex && i < nextItemIdsStartIndex + perPage; mergedItemIds[ i ] = isInNextItemsRange ? nextItemIds[ i - nextItemIdsStartIndex ] : itemIds?.[ i ]; diff --git a/packages/core-data/src/queried-data/selectors.js b/packages/core-data/src/queried-data/selectors.js index e1f52f721abf6e..f605b8c26e52d7 100644 --- a/packages/core-data/src/queried-data/selectors.js +++ b/packages/core-data/src/queried-data/selectors.js @@ -52,7 +52,9 @@ function getQueriedItemsUncached( state, query ) { if ( Array.isArray( include ) && ! include.includes( itemId ) ) { continue; } - + if ( itemId === undefined ) { + continue; + } // Having a target item ID doesn't guarantee that this object has been queried. if ( ! state.items[ context ]?.hasOwnProperty( itemId ) ) { return null; diff --git a/packages/core-data/src/queried-data/test/reducer.js b/packages/core-data/src/queried-data/test/reducer.js index 4271f8d80a4a3e..d996f5b07ea22b 100644 --- a/packages/core-data/src/queried-data/test/reducer.js +++ b/packages/core-data/src/queried-data/test/reducer.js @@ -64,6 +64,17 @@ describe( 'getMergedItemIds', () => { expect( result ).toEqual( [ 1, 3, 4 ] ); } ); + it( 'should update a page properly if less items are provided than previously stored', () => { + let original = deepFreeze( [ 1, 2, 3 ] ); + let result = getMergedItemIds( original, [ 1, 2 ], 1, 3 ); + + expect( result ).toEqual( [ 1, 2 ] ); + + original = deepFreeze( [ 1, 2, 3, 4, 5, 6 ] ); + result = getMergedItemIds( original, [ 9 ], 2, 2 ); + + expect( result ).toEqual( [ 1, 2, 9, undefined, 5, 6 ] ); + } ); } ); describe( 'itemIsComplete', () => { From 478ea734e45b3e4b947b17f7bfd1d8ad80ed1670 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:18:49 +0100 Subject: [PATCH 05/20] Query Loop - Add accesibility markup at the end of the loop in all cases. (#55890) * Use tagName if exists * Added a test using string positions, we could refactor * Refactor to use Tag Processor --- packages/block-library/src/query/index.php | 8 +++- phpunit/blocks/render-query-test.php | 43 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 201ceed737a12a..b6a5733632ff44 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -44,7 +44,11 @@ function render_block_core_query( $attributes, $content, $block ) { $block->block_type->supports['interactivity'] = true; // Add a div to announce messages using `aria-live`. - $last_div_position = strripos( $content, '' ); + $html_tag = 'div'; + if ( ! empty( $attributes['tagName'] ) ) { + $html_tag = esc_attr( $attributes['tagName'] ); + } + $last_tag_position = strripos( $content, '' ); $content = substr_replace( $content, '
', - $last_div_position, + $last_tag_position, 0 ); } diff --git a/phpunit/blocks/render-query-test.php b/phpunit/blocks/render-query-test.php index 8a148e89be87bf..2d81bfdb513b33 100644 --- a/phpunit/blocks/render-query-test.php +++ b/phpunit/blocks/render-query-test.php @@ -131,6 +131,49 @@ public function test_rendering_query_with_enhanced_pagination_auto_disabled_when $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) ); } + + /** + * Tests that the `core/query` last tag is rendered with the tagName attribute + * if is defined, having a div as default. + */ + public function test_enhanced_query_markup_rendering_at_bottom_on_custom_html_element_tags() { + global $wp_query, $wp_the_query; + + $content = << + + + +HTML; + + // Set main query to single post. + $wp_query = new WP_Query( + array( + 'posts_per_page' => 1, + ) + ); + + $wp_the_query = $wp_query; + + $output = do_blocks( $content ); + + $p = new WP_HTML_Tag_Processor( $output ); + + $p->next_tag( 'span' ); + + // Test that there is a div added just after the last tag inside the aside. + $this->assertSame( $p->next_tag(), true ); + // Test that that div is the accesibility one. + $this->assertSame( 'screen-reader-text', $p->get_attribute( 'class' ) ); + $this->assertSame( 'context.core.query.message', $p->get_attribute( 'data-wp-text' ) ); + $this->assertSame( 'polite', $p->get_attribute( 'aria-live' ) ); + } + /** * Tests that the `core/query` block adds an extra attribute to disable the * enhanced pagination in the browser when a post content block is found inside. From 827cc8317f9479afd5cb80550f4ee89ee2aea88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:39:09 +0100 Subject: [PATCH 06/20] DataViews: make items per page an even number (#55906) --- packages/edit-site/src/components/dataviews/view-actions.js | 2 +- .../edit-site/src/components/sidebar-dataviews/default-views.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/dataviews/view-actions.js b/packages/edit-site/src/components/dataviews/view-actions.js index 035ab6e36facf2..cb6aa022cf9a50 100644 --- a/packages/edit-site/src/components/dataviews/view-actions.js +++ b/packages/edit-site/src/components/dataviews/view-actions.js @@ -86,7 +86,7 @@ function ViewTypeMenu( { view, onChangeView } ) { ); } -const PAGE_SIZE_VALUES = [ 5, 20, 50 ]; +const PAGE_SIZE_VALUES = [ 20, 50, 100 ]; function PageSizeMenu( { view, onChangeView } ) { return ( Date: Tue, 7 Nov 2023 11:15:45 +0100 Subject: [PATCH 07/20] Fix Inaccurate description of the Show icon button setting in Nav block (#55429) * Navigation: Fix Inaccurate description of the Show icon button setting * Update overlay-menu-preview.js Co-authored-by: Dave Smith --------- Co-authored-by: Dave Smith --- .../block-library/src/navigation/edit/overlay-menu-preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/overlay-menu-preview.js b/packages/block-library/src/navigation/edit/overlay-menu-preview.js index adb1377a604ede..8ea07351821c89 100644 --- a/packages/block-library/src/navigation/edit/overlay-menu-preview.js +++ b/packages/block-library/src/navigation/edit/overlay-menu-preview.js @@ -20,7 +20,7 @@ export default function OverlayMenuPreview( { setAttributes, hasIcon, icon } ) { __nextHasNoMarginBottom label={ __( 'Show icon button' ) } help={ __( - 'Configure the visual appearance of the button opening the overlay menu.' + 'Configure the visual appearance of the button that toggles the overlay menu.' ) } onChange={ ( value ) => setAttributes( { hasIcon: value } ) } checked={ hasIcon } From 11df44a7aa2d705958e740cabbd0865798fdc2ec Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 7 Nov 2023 12:28:11 +0200 Subject: [PATCH 08/20] [Data views]: Make used taxonomy private (#55918) --- lib/experimental/data-views.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/data-views.php b/lib/experimental/data-views.php index e0346184ffc21c..e9fb2134f3b39c 100644 --- a/lib/experimental/data-views.php +++ b/lib/experimental/data-views.php @@ -40,7 +40,7 @@ function _gutenberg_register_data_views_post_type() { 'wp_dataviews_type', array( 'wp_dataviews' ), array( - 'public' => true, + 'public' => false, 'hierarchical' => false, 'labels' => array( 'name' => __( 'Dataview types', 'gutenberg' ), From 3e4c0530d80737e46da1682d4855c202a0bb26c8 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 7 Nov 2023 12:45:53 +0200 Subject: [PATCH 09/20] [Site editor]: Add edit page slug field (#55767) * [Site editor]: Add edit page slug field * address feedback * use `getEditedEntityRecord` * fallback to generated slug and use proper permalink * clean slug on blur to allow empty spaces * rename function * Combine selectors; there's no need to create two subscriptions to the same store * Avoid making the two HTTP requests for the same record * Remove redundant target blank * Handle case when the dropdown is closed without triggering a blur event * address feedback * update design by using InputControl * remove prefix * add form in order to close on Enter --------- Co-authored-by: George Mamadashvili --- .../page-panels/page-slug.js | 161 ++++++++++++++++++ .../page-panels/page-summary.js | 2 + .../sidebar-edit-mode/page-panels/style.scss | 7 + 3 files changed, 170 insertions(+) create mode 100644 packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js new file mode 100644 index 00000000000000..d6ffa1991333ec --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-slug.js @@ -0,0 +1,161 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { + safeDecodeURIComponent, + filterURLForDisplay, + cleanForSlug, +} from '@wordpress/url'; +import { useState, useMemo } from '@wordpress/element'; +import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { + __experimentalInputControl as InputControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalText as Text, + Dropdown, + Button, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; + +export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; + +function getPostPermalink( record, isEditable ) { + if ( ! record?.permalink_template ) { + return; + } + const slug = record?.slug || record?.generated_slug; + const [ prefix, suffix ] = record.permalink_template.split( + PERMALINK_POSTNAME_REGEX + ); + const permalink = isEditable ? prefix + slug + suffix : record.link; + return filterURLForDisplay( safeDecodeURIComponent( permalink ) ); +} + +export default function PageSlug( { postType, postId } ) { + const { editEntityRecord } = useDispatch( coreStore ); + const { record, savedSlug } = useSelect( + ( select ) => { + const { getEntityRecord, getEditedEntityRecord } = + select( coreStore ); + const savedRecord = getEntityRecord( 'postType', postType, postId ); + return { + record: getEditedEntityRecord( 'postType', postType, postId ), + savedSlug: savedRecord?.slug || savedRecord?.generated_slug, + }; + }, + [ postType, postId ] + ); + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); + const [ forceEmptyField, setForceEmptyField ] = useState( false ); + const isEditable = + PERMALINK_POSTNAME_REGEX.test( record?.permalink_template ) && + record?._links?.[ 'wp:action-publish' ]; + const popoverProps = useMemo( + () => ( { + // Anchor the popover to the middle of the entire row so that it doesn't + // move around when the label changes. + anchor: popoverAnchor, + 'aria-label': __( 'Change slug' ), + placement: 'bottom-end', + } ), + [ popoverAnchor ] + ); + if ( ! record || ! isEditable ) { + return null; + } + const recordSlug = safeDecodeURIComponent( + record?.slug || record?.generated_slug + ); + const permaLink = getPostPermalink( record, isEditable ); + const onSlugChange = ( newValue ) => { + editEntityRecord( 'postType', postType, postId, { + slug: newValue, + } ); + }; + return ( + + + { __( 'URL' ) } + + { + if ( forceEmptyField ) { + onSlugChange( cleanForSlug( savedSlug ) ); + setForceEmptyField( false ); + } + } } + renderToggle={ ( { onToggle } ) => ( + + ) } + renderContent={ ( { onClose } ) => { + return ( + <> + + +
+ { + onSlugChange( newValue ); + // When we delete the field the permalink gets + // reverted to the original value. + // The forceEmptyField logic allows the user to have + // the field temporarily empty while typing. + if ( ! newValue ) { + if ( ! forceEmptyField ) { + setForceEmptyField( true ); + } + return; + } + if ( forceEmptyField ) { + setForceEmptyField( false ); + } + } } + onBlur={ ( event ) => { + onSlugChange( + cleanForSlug( + event.target.value || + savedSlug + ) + ); + if ( forceEmptyField ) { + setForceEmptyField( false ); + } + } } + /> + +
+ + ); + } } + /> +
+ ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js index c4dafeab6cb372..25b69985bcbd6e 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js @@ -8,6 +8,7 @@ import { __experimentalVStack as VStack } from '@wordpress/components'; import PageStatus from './page-status'; import PublishDate from './publish-date'; import EditTemplate from './edit-template'; +import PageSlug from './page-slug'; export default function PageSummary( { status, @@ -32,6 +33,7 @@ export default function PageSummary( { postType={ postType } /> + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index 5501fe49e5876b..e1a8e4acb72273 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -85,3 +85,10 @@ min-width: 240px; } } + +.edit-site-page-panels-edit-slug__dropdown { + .components-popover__content { + min-width: 320px; + padding: $grid-unit-20; + } +} From 38726abc4dcd3aaeb3eed4c352a615316746fee2 Mon Sep 17 00:00:00 2001 From: Pooja Killekar <41000648+pooja-muchandikar@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:09:55 +0530 Subject: [PATCH 10/20] Migrate Child Block Test to Playwright (#55199) * Migrate Child Block Test to Playwright * Fix Stylelint failing and modified the test case * Fix failed CI * Update locators * Address Feedback * Address feedbacks * Address feedbacks * Update test case * Update test case --- .../specs/editor/plugins/child-blocks.test.js | 66 ------------- .../specs/editor/plugins/child-blocks.spec.js | 97 +++++++++++++++++++ 2 files changed, 97 insertions(+), 66 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/plugins/child-blocks.test.js create mode 100644 test/e2e/specs/editor/plugins/child-blocks.spec.js diff --git a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js deleted file mode 100644 index c7ca368003397e..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - closeGlobalBlockInserter, - createNewPost, - deactivatePlugin, - getAllBlockInserterItemTitles, - insertBlock, - openGlobalBlockInserter, -} from '@wordpress/e2e-test-utils'; - -describe( 'Child Blocks', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-child-blocks' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-child-blocks' ); - } ); - - it( 'are hidden from the global block inserter', async () => { - await openGlobalBlockInserter(); - await expect( await getAllBlockInserterItemTitles() ).not.toContain( - 'Child Blocks Child' - ); - } ); - - it( 'shows up in a parent block', async () => { - await insertBlock( 'Child Blocks Unrestricted Parent' ); - await closeGlobalBlockInserter(); - await page.waitForSelector( - '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' - ); - await page.click( - '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' - ); - await openGlobalBlockInserter(); - const inserterItemTitles = await getAllBlockInserterItemTitles(); - expect( inserterItemTitles ).toContain( 'Child Blocks Child' ); - expect( inserterItemTitles.length ).toBeGreaterThan( 20 ); - } ); - - it( 'display in a parent block with allowedItems', async () => { - await insertBlock( 'Child Blocks Restricted Parent' ); - await closeGlobalBlockInserter(); - await page.waitForSelector( - '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' - ); - await page.click( - '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' - ); - await openGlobalBlockInserter(); - const allowedBlocks = await getAllBlockInserterItemTitles(); - expect( allowedBlocks.sort() ).toEqual( [ - 'Child Blocks Child', - 'Image', - 'Paragraph', - ] ); - } ); -} ); diff --git a/test/e2e/specs/editor/plugins/child-blocks.spec.js b/test/e2e/specs/editor/plugins/child-blocks.spec.js new file mode 100644 index 00000000000000..b3073b70a5409a --- /dev/null +++ b/test/e2e/specs/editor/plugins/child-blocks.spec.js @@ -0,0 +1,97 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Child Blocks', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-child-blocks' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-child-blocks' ); + } ); + + test( 'are hidden from the global block inserter', async ( { page } ) => { + const blockInserter = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockInserter.click(); + await expect( blockLibrary ).toBeVisible(); + expect( blockLibrary.getByRole( 'option' ) ).not.toContain( [ + 'Child Blocks Child', + ] ); + } ); + + test( 'shows up in a parent block', async ( { page, editor } ) => { + await editor.insertBlock( { + name: 'test/child-blocks-unrestricted-parent', + } ); + + await page + .getByRole( 'document', { + name: 'Block: Child Blocks Unrestricted Parent', + } ) + .getByRole( 'button', { + name: 'Add default block', + } ) + .click(); + + const blockInserter = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockInserter.click(); + await expect( blockLibrary ).toBeVisible(); + await expect( blockLibrary.getByRole( 'option' ) ).toContainText( [ + 'Child Blocks Child', + ] ); + expect( + await blockLibrary.getByRole( 'option' ).count() + ).toBeGreaterThan( 10 ); + } ); + + test( 'display in a parent block with allowedItems', async ( { + page, + editor, + } ) => { + await editor.insertBlock( { + name: 'test/child-blocks-restricted-parent', + } ); + + await page + .getByRole( 'document', { + name: 'Block: Child Blocks Restricted Parent', + } ) + .getByRole( 'button', { + name: 'Add default block', + } ) + .click(); + + const blockInserter = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockInserter.click(); + await expect( blockLibrary ).toBeVisible(); + await expect( blockLibrary.getByRole( 'option' ) ).toHaveText( [ + 'Paragraph', + 'Child Blocks Child', + 'Image', + ] ); + } ); +} ); From 137ad68e41b0ed2e48d7d70f3428cc4245a856a4 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:55:30 +0900 Subject: [PATCH 11/20] Edit Post: Fix pattern modal reopening when making the title empty again (#55873) * Fix pattern modal reopening when making the title empty again * Use boolean value --- .../components/start-page-options/index.js | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/edit-post/src/components/start-page-options/index.js b/packages/edit-post/src/components/start-page-options/index.js index 02473fd4eaa148..77264d27a5e7df 100644 --- a/packages/edit-post/src/components/start-page-options/index.js +++ b/packages/edit-post/src/components/start-page-options/index.js @@ -3,7 +3,7 @@ */ import { Modal } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useState, useMemo } from '@wordpress/element'; import { store as blockEditorStore, __experimentalBlockPatternsList as BlockPatternsList, @@ -62,19 +62,11 @@ function PatternSelection( { blockPatterns, onChoosePattern } ) { ); } -function StartPageOptionsModal() { - const [ modalState, setModalState ] = useState( 'initial' ); +function StartPageOptionsModal( { onClose } ) { const startPatterns = useStartPatterns(); const hasStartPattern = startPatterns.length > 0; - const shouldOpenModal = hasStartPattern && modalState === 'initial'; - useEffect( () => { - if ( shouldOpenModal ) { - setModalState( 'open' ); - } - }, [ shouldOpenModal ] ); - - if ( modalState !== 'open' ) { + if ( ! hasStartPattern ) { return null; } @@ -83,12 +75,12 @@ function StartPageOptionsModal() { className="edit-post-start-page-options__modal" title={ __( 'Choose a pattern' ) } isFullScreen - onRequestClose={ () => setModalState( 'closed' ) } + onRequestClose={ onClose } >
setModalState( 'closed' ) } + onChoosePattern={ onClose } />
@@ -96,6 +88,7 @@ function StartPageOptionsModal() { } export default function StartPageOptions() { + const [ isClosed, setIsClosed ] = useState( false ); const shouldEnableModal = useSelect( ( select ) => { const { isCleanNewPost } = select( editorStore ); const { isEditingTemplate, isFeatureActive } = select( editPostStore ); @@ -107,9 +100,9 @@ export default function StartPageOptions() { ); }, [] ); - if ( ! shouldEnableModal ) { + if ( ! shouldEnableModal || isClosed ) { return null; } - return ; + return setIsClosed( true ) } />; } From 837374ecb19c3342bb4e88073c6d7ee00f35b694 Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Tue, 7 Nov 2023 12:12:54 +0000 Subject: [PATCH 12/20] Perf Tests: Stabilise the Site Editor metrics (#55922) --- .../src/admin/visit-site-editor.ts | 4 +- test/performance/specs/site-editor.spec.js | 73 +++++-------------- 2 files changed, 22 insertions(+), 55 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index da21f17aade117..5883a2b92a5bcc 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -47,7 +47,9 @@ export async function visitSiteEditor( * loading is done. */ await this.page - .locator( '.edit-site-canvas-loader' ) + // Spinner was used instead of the progress bar in an earlier version of + // the site editor. + .locator( '.edit-site-canvas-loader, .edit-site-canvas-spinner' ) // Bigger timeout is needed for larger entities, for example the large // post html fixture that we load for performance tests, which often // doesn't make it under the default 10 seconds. diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index bd1a5f0b87cc01..38bcceb14edd61 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -58,19 +58,13 @@ test.describe( 'Site Editor Performance', () => { } ); test.describe( 'Loading', () => { - let draftURL = null; + let draftId = null; - test( 'Setup the test page', async ( { page, admin, perfUtils } ) => { + test( 'Setup the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); await perfUtils.loadBlocksForLargePost(); - await perfUtils.saveDraft(); - await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), - postType: 'page', - } ); - - draftURL = page.url(); + draftId = await perfUtils.saveDraft(); } ); const samples = 10; @@ -78,15 +72,18 @@ test.describe( 'Site Editor Performance', () => { const iterations = samples + throwaway; for ( let i = 1; i <= iterations; i++ ) { test( `Run the test (${ i } of ${ iterations })`, async ( { - page, + admin, perfUtils, metrics, } ) => { // Go to the test draft. - await page.goto( draftURL ); - const canvas = await perfUtils.getCanvas(); + await admin.visitSiteEditor( { + postId: draftId, + postType: 'page', + } ); // Wait for the first block. + const canvas = await perfUtils.getCanvas(); await canvas.locator( '.wp-block' ).first().waitFor(); // Get the durations. @@ -109,44 +106,25 @@ test.describe( 'Site Editor Performance', () => { } ); test.describe( 'Typing', () => { - let draftURL = null; - - test( 'Setup the test post', async ( { - page, - admin, - editor, - perfUtils, - } ) => { + let draftId = null; + + test( 'Setup the test post', async ( { admin, editor, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); await perfUtils.loadBlocksForLargePost(); await editor.insertBlock( { name: 'core/paragraph' } ); - await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); + } ); + + test( 'Run the test', async ( { admin, perfUtils, metrics } ) => { + // Go to the test draft. await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), + postId: draftId, postType: 'page', } ); - draftURL = page.url(); - } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); - await perfUtils.disableAutosave(); - - // Wait for the loader overlay to disappear. This is necessary - // because the overlay is still visible for a while after the editor - // canvas is ready, and we don't want it to affect the typing - // timings. - await page - .locator( - // Spinner was used instead of the progress bar in an earlier version of the site editor. - '.edit-site-canvas-loader, .edit-site-canvas-spinner' - ) - .waitFor( { state: 'hidden' } ); - - const canvas = await perfUtils.getCanvas(); - // Enter edit mode (second click is needed for the legacy edit mode). + const canvas = await perfUtils.getCanvas(); await canvas.locator( 'body' ).click(); await canvas .getByRole( 'document', { name: /Block:( Post)? Content/ } ) @@ -210,19 +188,6 @@ test.describe( 'Site Editor Performance', () => { path: '/wp_template', } ); - // Wait for the loader overlay to disappear. This is necessary - // because the overlay is still visible for a while after the editor - // canvas is ready, and we don't want it to affect the typing - // timings. - await page - .locator( - // Spinner was used instead of the progress bar in an earlier version of the site editor. - '.edit-site-canvas-loader, .edit-site-canvas-spinner' - ) - .waitFor( { state: 'hidden' } ); - // Additional time to ensure the browser is completely idle. - // eslint-disable-next-line playwright/no-wait-for-timeout - // Start tracing. await metrics.startTracing(); From 76356caba6e39bfc8726069efb416ead6b3e3558 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 7 Nov 2023 13:24:16 +0100 Subject: [PATCH 13/20] Core Data: Move the template lookup to core-data selectors/resolvers (#55883) --- docs/reference-guides/data/data-core.md | 26 +++++++++++++++++++ packages/core-data/README.md | 26 +++++++++++++++++++ packages/core-data/src/actions.js | 16 ++++++++++++ packages/core-data/src/reducer.js | 21 +++++++++++++++ packages/core-data/src/resolvers.js | 11 ++++++++ packages/core-data/src/selectors.ts | 22 ++++++++++++++++ .../start-template-options/index.js | 26 ++++++++++--------- packages/edit-site/src/store/actions.js | 20 ++++++++++---- 8 files changed, 151 insertions(+), 17 deletions(-) diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 8a190869f99e78..ea97ce28e4d85c 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -150,6 +150,19 @@ _Returns_ - `undefined< 'edit' >`: Current user object. +### getDefaultTemplateId + +Returns the default template use to render a given query. + +_Parameters_ + +- _state_ `State`: Data state. +- _query_ `TemplateQuery`: Query. + +_Returns_ + +- `string`: The default template id for the given query. + ### getEditedEntityRecord Returns the specified entity record, merged with its edits. @@ -648,6 +661,19 @@ _Returns_ - `Object`: Action object. +### receiveDefaultTemplateId + +Returns an action object used to set the template for a given query. + +_Parameters_ + +- _query_ `Object`: The lookup query. +- _templateId_ `string`: The resolved template id. + +_Returns_ + +- `Object`: Action object. + ### receiveEntityRecords Returns an action object used in signalling that entity records have been received. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index a20e86e9695a26..ef5d9c1197f099 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -205,6 +205,19 @@ _Returns_ - `Object`: Action object. +### receiveDefaultTemplateId + +Returns an action object used to set the template for a given query. + +_Parameters_ + +- _query_ `Object`: The lookup query. +- _templateId_ `string`: The resolved template id. + +_Returns_ + +- `Object`: Action object. + ### receiveEntityRecords Returns an action object used in signalling that entity records have been received. @@ -444,6 +457,19 @@ _Returns_ - `undefined< 'edit' >`: Current user object. +### getDefaultTemplateId + +Returns the default template use to render a given query. + +_Parameters_ + +- _state_ `State`: Data state. +- _query_ `TemplateQuery`: Query. + +_Returns_ + +- `string`: The default template id for the given query. + ### getEditedEntityRecord Returns the specified entity record, merged with its edits. diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index c4b19819ed7a41..9e7277f35a62a7 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -908,3 +908,19 @@ export function receiveNavigationFallbackId( fallbackId ) { fallbackId, }; } + +/** + * Returns an action object used to set the template for a given query. + * + * @param {Object} query The lookup query. + * @param {string} templateId The resolved template id. + * + * @return {Object} Action object. + */ +export function receiveDefaultTemplateId( query, templateId ) { + return { + type: 'RECEIVE_DEFAULT_TEMPLATE', + query, + templateId, + }; +} diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 68c0cc233d7b68..a21623d8ba89d3 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -572,6 +572,26 @@ export function themeGlobalStyleRevisions( state = {}, action ) { return state; } +/** + * Reducer managing the template lookup per query. + * + * @param {Record} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Record} Updated state. + */ +export function defaultTemplates( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_DEFAULT_TEMPLATE': + return { + ...state, + [ JSON.stringify( action.query ) ]: action.templateId, + }; + } + + return state; +} + export default combineReducers( { terms, users, @@ -592,4 +612,5 @@ export default combineReducers( { blockPatternCategories, userPatternCategories, navigationFallbackId, + defaultTemplates, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 5fc7cd14f35c0b..cd2a65a60b0139 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -707,3 +707,14 @@ export const getNavigationFallbackId = ] ); } }; + +export const getDefaultTemplateId = + ( query ) => + async ( { dispatch } ) => { + const template = await apiFetch( { + path: addQueryArgs( '/wp/v2/templates/lookup', query ), + } ); + if ( template ) { + dispatch.receiveDefaultTemplateId( query, template.id ); + } + }; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index b6b36fad2ee934..2a046941611c7d 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -46,6 +46,7 @@ export interface State { users: UserState; navigationFallbackId: EntityRecordKey; userPatternCategories: Array< UserPatternCategory >; + defaultTemplates: Record< string, string >; } type EntityRecordKey = string | number; @@ -81,6 +82,12 @@ interface UserState { byId: Record< EntityRecordKey, ET.User< 'edit' > >; } +type TemplateQuery = { + slug?: string; + is_custom?: boolean; + ignore_empty?: boolean; +}; + export interface UserPatternCategory { id: number; name: string; @@ -1351,3 +1358,18 @@ export function getCurrentThemeGlobalStylesRevisions( return state.themeGlobalStyleRevisions[ currentGlobalStylesId ]; } + +/** + * Returns the default template use to render a given query. + * + * @param state Data state. + * @param query Query. + * + * @return The default template id for the given query. + */ +export function getDefaultTemplateId( + state: State, + query: TemplateQuery +): string { + return state.defaultTemplates[ JSON.stringify( query ) ]; +} diff --git a/packages/edit-site/src/components/start-template-options/index.js b/packages/edit-site/src/components/start-template-options/index.js index fe4179a338504b..771f380db05a3d 100644 --- a/packages/edit-site/src/components/start-template-options/index.js +++ b/packages/edit-site/src/components/start-template-options/index.js @@ -3,7 +3,7 @@ */ import { Modal, Flex, FlexItem, Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useState, useMemo } from '@wordpress/element'; import { __experimentalBlockPatternsList as BlockPatternsList, store as blockEditorStore, @@ -13,8 +13,6 @@ import { useAsyncList } from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; import { parse } from '@wordpress/blocks'; import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data'; -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -23,18 +21,22 @@ import { store as editSiteStore } from '../../store'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; function useFallbackTemplateContent( slug, isCustom = false ) { - const [ templateContent, setTemplateContent ] = useState( '' ); - - useEffect( () => { - apiFetch( { - path: addQueryArgs( '/wp/v2/templates/lookup', { + return useSelect( + ( select ) => { + const { getEntityRecord, getDefaultTemplateId } = + select( coreStore ); + const templateId = getDefaultTemplateId( { slug, is_custom: isCustom, ignore_empty: true, - } ), - } ).then( ( { content } ) => setTemplateContent( content.raw ) ); - }, [ isCustom, slug ] ); - return templateContent; + } ); + return templateId + ? getEntityRecord( 'postType', TEMPLATE_POST_TYPE, templateId ) + ?.content?.raw + : undefined; + }, + [ slug, isCustom ] + ); } function useStartPatterns( fallbackContent ) { diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index ce698a757f6bb3..30ee9e6aab01af 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -266,12 +266,22 @@ export const setPage = ( page ) => async ( { dispatch, registry } ) => { let template; - const getDefaultTemplate = async ( slug ) => - apiFetch( { - path: addQueryArgs( '/wp/v2/templates/lookup', { + const getDefaultTemplate = async ( slug ) => { + const templateId = await registry + .resolveSelect( coreStore ) + .getDefaultTemplateId( { slug: `page-${ slug }`, - } ), - } ); + } ); + return templateId + ? await registry + .resolveSelect( coreStore ) + .getEntityRecord( + 'postType', + TEMPLATE_POST_TYPE, + templateId + ) + : undefined; + }; if ( page.path ) { template = await registry From 7ce6b179ab049be8b1e5648e5cd2a6eb65d54736 Mon Sep 17 00:00:00 2001 From: scruffian Date: Tue, 7 Nov 2023 12:30:55 +0000 Subject: [PATCH 14/20] move the logic for inner block rendering to a separate function --- .../class-wp-navigation-block-renderer.php | 611 ++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php new file mode 100644 index 00000000000000..2d7cb67b4882a2 --- /dev/null +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -0,0 +1,611 @@ +render(); + $p = new WP_HTML_Tag_Processor( $inner_block_content ); + if ( $p->next_tag( + array( + 'name' => 'LI', + 'class_name' => 'has-child', + ) + ) ) { + $has_submenus = true; + } + } + return $has_submenus; + } + + /** + * Determine whether to load the view script. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return bool Returns whether or not to load the view script. + */ + private static function should_load_view_script( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; + } + + /** + * Returns the markup for a single inner block. + * + * @param WP_Block $inner_block The inner block. + * @return string Returns the markup for a single inner block. + */ + private static function get_markup_for_inner_block( $inner_block ) { + $inner_block_content = $inner_block->render(); + if ( ! empty( $inner_block_content ) ) { + if ( in_array( $inner_block->name, static::$needs_list_item_wrapper, true ) ) { + return '
  • ' . $inner_block_content . '
  • '; + } + + return $inner_block_content; + } + } + + /** + * Returns the html for the inner blocks of the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the html for the inner blocks of the navigation block. + */ + private static function get_inner_blocks_html( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $container_attributes = get_block_wrapper_attributes( + array( + 'class' => 'wp-block-navigation__container ' . $class, + 'style' => $style, + ) + ); + + $inner_blocks_html = ''; + $is_list_open = false; + + foreach ( $inner_blocks as $inner_block ) { + $is_list_item = in_array( $inner_block->name, static::$list_item_nav_blocks, true ); + + if ( $is_list_item && ! $is_list_open ) { + $is_list_open = true; + $inner_blocks_html .= sprintf( + '
      ', + $container_attributes + ); + } + + if ( ! $is_list_item && $is_list_open ) { + $is_list_open = false; + $inner_blocks_html .= '
    '; + } + + $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); + } + + if ( $is_list_open ) { + $inner_blocks_html .= ''; + } + + // Add directives to the submenu if needed. + if ( $has_submenus && $should_load_view_script ) { + $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); + $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); + } + + return $inner_blocks_html; + } + + /** + * Gets the inner blocks for the navigation block from the navigation post. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_navigation_post( $attributes ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return ''; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $parsed_blocks = parse_blocks( $navigation_post->post_content ); + + // 'parse_blocks' includes a null block with '\n\n' as the content when + // it encounters whitespace. This code strips it. + $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + + // TODO - this uses the full navigation block attributes for the + // context which could be refined. + return new WP_Block_List( $compacted_blocks, $attributes ); + } + } + + /** + * Gets the inner blocks for the navigation block from the fallback. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_fallback( $attributes ) { + $fallback_blocks = block_core_navigation_get_fallback_blocks(); + + // Fallback my have been filtered so do basic test for validity. + if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { + return ''; + } + + return new WP_Block_List( $fallback_blocks, $attributes ); + } + + /** + * Gets the inner blocks for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks( $attributes, $block ) { + $inner_blocks = $block->inner_blocks; + + // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. + if ( array_key_exists( 'navigationMenuId', $attributes ) ) { + $attributes['ref'] = $attributes['navigationMenuId']; + } + + // If: + // - the gutenberg plugin is active + // - `__unstableLocation` is defined + // - we have menu items at the defined location + // - we don't have a relationship to a `wp_navigation` Post (via `ref`). + // ...then create inner blocks from the classic menu assigned to that location. + if ( + defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && + array_key_exists( '__unstableLocation', $attributes ) && + ! array_key_exists( 'ref', $attributes ) && + ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) + ) { + $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); + } + + // Load inner blocks from the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); + } + + // If there are no inner blocks then fallback to rendering an appropriate fallback. + if ( empty( $inner_blocks ) ) { + $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); + } + + /** + * Filter navigation block $inner_blocks. + * Allows modification of a navigation block menu items. + * + * @since 6.1.0 + * + * @param \WP_Block_List $inner_blocks + */ + $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); + + $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); + if ( $post_ids ) { + _prime_post_caches( $post_ids, false, false ); + } + + return $inner_blocks; + } + + /** + * Gets the name of the current navigation, if it has one. + * + * @param array $attributes The block attributes. + * @param array $seen_menu_names The list of seen menu names, passed by reference so they can be updated. + * @return string Returns the name of the navigation. + */ + private static function get_navigation_name( $attributes, &$seen_menu_names ) { + $navigation_name = $attributes['ariaLabel'] ?? ''; + + // Load the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return $navigation_name; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $navigation_name = $navigation_post->post_title; + + // This is used to count the number of times a navigation name has been seen, + // so that we can ensure every navigation has a unique id. + if ( isset( $seen_menu_names[ $navigation_name ] ) ) { + ++$seen_menu_names[ $navigation_name ]; + } else { + $seen_menu_names[ $navigation_name ] = 1; + } + } + } + + return $navigation_name; + } + + /** + * Returns the layout class for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the layout class for the navigation block. + */ + private static function get_layout_class( $attributes ) { + $layout_justification = array( + 'left' => 'items-justified-left', + 'right' => 'items-justified-right', + 'center' => 'items-justified-center', + 'space-between' => 'items-justified-space-between', + ); + + $layout_class = ''; + if ( + isset( $attributes['layout']['justifyContent'] ) && + isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) + ) { + $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; + } + if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { + $layout_class .= ' is-vertical'; + } + + if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { + $layout_class .= ' no-wrap'; + } + return $layout_class; + } + + /** + * Return classes for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the classes for the navigation block. + */ + private static function get_classes( $attributes ) { + // Restore legacy classnames for submenu positioning. + $layout_class = static::get_layout_class( $attributes ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $is_responsive_menu = static::is_responsive( $attributes ); + + // Manually add block support text decoration as CSS class. + $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; + $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); + + $classes = array_merge( + $colors['css_classes'], + $font_sizes['css_classes'], + $is_responsive_menu ? array( 'is-responsive' ) : array(), + $layout_class ? array( $layout_class ) : array(), + $text_decoration ? array( $text_decoration_class ) : array() + ); + return implode( ' ', $classes ); + } + + /** + * Get styles for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the styles for the navigation block. + */ + private static function get_styles( $attributes ) { + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; + return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; + } + + /** + * Get the responsive container markup + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @param string $inner_blocks_html The markup for the inner blocks. + * @return string Returns the container markup. + */ + private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $modal_unique_id = wp_unique_id( 'modal-' ); + + $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + + $responsive_container_classes = array( + 'wp-block-navigation__responsive-container', + $is_hidden_by_default ? 'hidden-by-default' : '', + implode( ' ', $colors['overlay_css_classes'] ), + ); + $open_button_classes = array( + 'wp-block-navigation__responsive-container-open', + $is_hidden_by_default ? 'always-shown' : '', + ); + + $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; + $toggle_button_icon = ''; + if ( isset( $attributes['icon'] ) ) { + if ( 'menu' === $attributes['icon'] ) { + $toggle_button_icon = ''; + } + } + $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); + $toggle_close_button_icon = ''; + $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); + $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. + $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. + + // Add Interactivity API directives to the markup if needed. + $open_button_directives = ''; + $responsive_container_directives = ''; + $responsive_dialog_directives = ''; + $close_button_directives = ''; + if ( $should_load_view_script ) { + $open_button_directives = ' + data-wp-on--click="actions.core.navigation.openMenuOnClick" + data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + '; + $responsive_container_directives = ' + data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" + data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" + data-wp-effect="effects.core.navigation.initMenu" + data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" + tabindex="-1" + '; + $responsive_dialog_directives = ' + data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" + data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" + data-wp-bind--role="selectors.core.navigation.roleAttribute" + data-wp-effect="effects.core.navigation.focusFirstElement" + '; + $close_button_directives = ' + data-wp-on--click="actions.core.navigation.closeMenuOnClick" + '; + } + + return sprintf( + ' +
    +
    +
    + +
    + %2$s +
    +
    +
    +
    ', + esc_attr( $modal_unique_id ), + $inner_blocks_html, + $toggle_aria_label_open, + $toggle_aria_label_close, + esc_attr( implode( ' ', $responsive_container_classes ) ), + esc_attr( implode( ' ', $open_button_classes ) ), + esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), + $toggle_button_content, + $toggle_close_button_content, + $open_button_directives, + $responsive_container_directives, + $responsive_dialog_directives, + $close_button_directives + ); + } + + /** + * Get the wrapper attributes + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks A list of inner blocks. + * @param string $nav_menu_name The name of the navigation menu. + * @return string Returns the navigation block markup. + */ + private static function get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => $class, + 'style' => $style, + 'aria-label' => $nav_menu_name, + ) + ); + + if ( $is_responsive_menu ) { + $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); + $wrapper_attributes .= ' ' . $nav_element_directives; + } + + return $wrapper_attributes; + } + + /** + * Get the nav element directives + * + * @param bool $should_load_view_script Whether or not the view script should be loaded. + * @return string the directives for the navigation element. + */ + private static function get_nav_element_directives( $should_load_view_script ) { + if ( ! $should_load_view_script ) { + return ''; + } + // When adding to this array be mindful of security concerns. + $nav_element_context = wp_json_encode( + array( + 'core' => array( + 'navigation' => array( + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), + ), + ), + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP + ); + return ' + data-wp-interactive + data-wp-context=\'' . $nav_element_context . '\' + '; + } + + /** + * Handle view script loading. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @param WP_Block_List $inner_blocks The list of inner blocks. + */ + private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + + $view_js_file = 'wp-block-navigation-view'; + + // If the script already exists, there is no point in removing it from viewScript. + if ( ! wp_script_is( $view_js_file ) ) { + $script_handles = $block->block_type->view_script_handles; + + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); + } + } + } + + /** + * Returns the markup for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the navigation wrapper markup. + */ + private static function get_wrapper_markup( $attributes, $inner_blocks ) { + $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); + if ( static::is_responsive( $attributes ) ) { + return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); + } + return $inner_blocks_html; + } + + /** + * Renders the navigation block. + * + * @param array $attributes The block attributes. + * @param string $content The saved content. + * @param WP_Block $block The parsed block. + * @return string Returns the navigation block markup. + */ + public static function render( $attributes, $content, $block ) { + static $seen_menu_names = array(); + + $nav_menu_name = static::get_navigation_name( $attributes, $seen_menu_names ); + + /** + * Deprecated: + * The rgbTextColor and rgbBackgroundColor attributes + * have been deprecated in favor of + * customTextColor and customBackgroundColor ones. + * Move the values from old attrs to the new ones. + */ + if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { + $attributes['customTextColor'] = $attributes['rgbTextColor']; + } + + if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { + $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; + } + + unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); + + $inner_blocks = static::get_inner_blocks( $attributes, $block ); + // Prevent navigation blocks referencing themselves from rendering. + if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { + return ''; + } + + // If the menu name has been used previously then append an ID + // to the name to ensure uniqueness across a given post. + if ( isset( $seen_menu_names[ $nav_menu_name ] ) && $seen_menu_names[ $nav_menu_name ] > 1 ) { + $count = $seen_menu_names[ $nav_menu_name ]; + $nav_menu_name = $nav_menu_name . ' ' . ( $count ); + } + + $wrapper_attributes = static::get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ); + + static::handle_view_script_loading( $attributes, $block, $inner_blocks ); + + return sprintf( + '', + $wrapper_attributes, + static::get_wrapper_markup( $attributes, $inner_blocks ) + ); + } +} From a9f307bf4f9a111b82b31062a4c3a8c34a1b881a Mon Sep 17 00:00:00 2001 From: scruffian Date: Tue, 7 Nov 2023 12:37:02 +0000 Subject: [PATCH 15/20] Revert "move the logic for inner block rendering to a separate function" This reverts commit 7ce6b179ab049be8b1e5648e5cd2a6eb65d54736. --- .../class-wp-navigation-block-renderer.php | 611 ------------------ 1 file changed, 611 deletions(-) delete mode 100644 lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php deleted file mode 100644 index 2d7cb67b4882a2..00000000000000 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ /dev/null @@ -1,611 +0,0 @@ -render(); - $p = new WP_HTML_Tag_Processor( $inner_block_content ); - if ( $p->next_tag( - array( - 'name' => 'LI', - 'class_name' => 'has-child', - ) - ) ) { - $has_submenus = true; - } - } - return $has_submenus; - } - - /** - * Determine whether to load the view script. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not to load the view script. - */ - private static function should_load_view_script( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; - } - - /** - * Returns the markup for a single inner block. - * - * @param WP_Block $inner_block The inner block. - * @return string Returns the markup for a single inner block. - */ - private static function get_markup_for_inner_block( $inner_block ) { - $inner_block_content = $inner_block->render(); - if ( ! empty( $inner_block_content ) ) { - if ( in_array( $inner_block->name, static::$needs_list_item_wrapper, true ) ) { - return '
  • ' . $inner_block_content . '
  • '; - } - - return $inner_block_content; - } - } - - /** - * Returns the html for the inner blocks of the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the html for the inner blocks of the navigation block. - */ - private static function get_inner_blocks_html( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $container_attributes = get_block_wrapper_attributes( - array( - 'class' => 'wp-block-navigation__container ' . $class, - 'style' => $style, - ) - ); - - $inner_blocks_html = ''; - $is_list_open = false; - - foreach ( $inner_blocks as $inner_block ) { - $is_list_item = in_array( $inner_block->name, static::$list_item_nav_blocks, true ); - - if ( $is_list_item && ! $is_list_open ) { - $is_list_open = true; - $inner_blocks_html .= sprintf( - '
      ', - $container_attributes - ); - } - - if ( ! $is_list_item && $is_list_open ) { - $is_list_open = false; - $inner_blocks_html .= '
    '; - } - - $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); - } - - if ( $is_list_open ) { - $inner_blocks_html .= ''; - } - - // Add directives to the submenu if needed. - if ( $has_submenus && $should_load_view_script ) { - $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); - $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); - } - - return $inner_blocks_html; - } - - /** - * Gets the inner blocks for the navigation block from the navigation post. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_navigation_post( $attributes ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return ''; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $parsed_blocks = parse_blocks( $navigation_post->post_content ); - - // 'parse_blocks' includes a null block with '\n\n' as the content when - // it encounters whitespace. This code strips it. - $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); - - // TODO - this uses the full navigation block attributes for the - // context which could be refined. - return new WP_Block_List( $compacted_blocks, $attributes ); - } - } - - /** - * Gets the inner blocks for the navigation block from the fallback. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_fallback( $attributes ) { - $fallback_blocks = block_core_navigation_get_fallback_blocks(); - - // Fallback my have been filtered so do basic test for validity. - if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { - return ''; - } - - return new WP_Block_List( $fallback_blocks, $attributes ); - } - - /** - * Gets the inner blocks for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks( $attributes, $block ) { - $inner_blocks = $block->inner_blocks; - - // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. - if ( array_key_exists( 'navigationMenuId', $attributes ) ) { - $attributes['ref'] = $attributes['navigationMenuId']; - } - - // If: - // - the gutenberg plugin is active - // - `__unstableLocation` is defined - // - we have menu items at the defined location - // - we don't have a relationship to a `wp_navigation` Post (via `ref`). - // ...then create inner blocks from the classic menu assigned to that location. - if ( - defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && - array_key_exists( '__unstableLocation', $attributes ) && - ! array_key_exists( 'ref', $attributes ) && - ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) - ) { - $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); - } - - // Load inner blocks from the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); - } - - // If there are no inner blocks then fallback to rendering an appropriate fallback. - if ( empty( $inner_blocks ) ) { - $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); - } - - /** - * Filter navigation block $inner_blocks. - * Allows modification of a navigation block menu items. - * - * @since 6.1.0 - * - * @param \WP_Block_List $inner_blocks - */ - $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - - $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); - if ( $post_ids ) { - _prime_post_caches( $post_ids, false, false ); - } - - return $inner_blocks; - } - - /** - * Gets the name of the current navigation, if it has one. - * - * @param array $attributes The block attributes. - * @param array $seen_menu_names The list of seen menu names, passed by reference so they can be updated. - * @return string Returns the name of the navigation. - */ - private static function get_navigation_name( $attributes, &$seen_menu_names ) { - $navigation_name = $attributes['ariaLabel'] ?? ''; - - // Load the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return $navigation_name; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $navigation_name = $navigation_post->post_title; - - // This is used to count the number of times a navigation name has been seen, - // so that we can ensure every navigation has a unique id. - if ( isset( $seen_menu_names[ $navigation_name ] ) ) { - ++$seen_menu_names[ $navigation_name ]; - } else { - $seen_menu_names[ $navigation_name ] = 1; - } - } - } - - return $navigation_name; - } - - /** - * Returns the layout class for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the layout class for the navigation block. - */ - private static function get_layout_class( $attributes ) { - $layout_justification = array( - 'left' => 'items-justified-left', - 'right' => 'items-justified-right', - 'center' => 'items-justified-center', - 'space-between' => 'items-justified-space-between', - ); - - $layout_class = ''; - if ( - isset( $attributes['layout']['justifyContent'] ) && - isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) - ) { - $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; - } - if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { - $layout_class .= ' is-vertical'; - } - - if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { - $layout_class .= ' no-wrap'; - } - return $layout_class; - } - - /** - * Return classes for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the classes for the navigation block. - */ - private static function get_classes( $attributes ) { - // Restore legacy classnames for submenu positioning. - $layout_class = static::get_layout_class( $attributes ); - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); - $is_responsive_menu = static::is_responsive( $attributes ); - - // Manually add block support text decoration as CSS class. - $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; - $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); - - $classes = array_merge( - $colors['css_classes'], - $font_sizes['css_classes'], - $is_responsive_menu ? array( 'is-responsive' ) : array(), - $layout_class ? array( $layout_class ) : array(), - $text_decoration ? array( $text_decoration_class ) : array() - ); - return implode( ' ', $classes ); - } - - /** - * Get styles for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the styles for the navigation block. - */ - private static function get_styles( $attributes ) { - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); - $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; - return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; - } - - /** - * Get the responsive container markup - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @param string $inner_blocks_html The markup for the inner blocks. - * @return string Returns the container markup. - */ - private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $colors = block_core_navigation_build_css_colors( $attributes ); - $modal_unique_id = wp_unique_id( 'modal-' ); - - $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - - $responsive_container_classes = array( - 'wp-block-navigation__responsive-container', - $is_hidden_by_default ? 'hidden-by-default' : '', - implode( ' ', $colors['overlay_css_classes'] ), - ); - $open_button_classes = array( - 'wp-block-navigation__responsive-container-open', - $is_hidden_by_default ? 'always-shown' : '', - ); - - $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; - $toggle_button_icon = ''; - if ( isset( $attributes['icon'] ) ) { - if ( 'menu' === $attributes['icon'] ) { - $toggle_button_icon = ''; - } - } - $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); - $toggle_close_button_icon = ''; - $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); - $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. - $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. - - // Add Interactivity API directives to the markup if needed. - $open_button_directives = ''; - $responsive_container_directives = ''; - $responsive_dialog_directives = ''; - $close_button_directives = ''; - if ( $should_load_view_script ) { - $open_button_directives = ' - data-wp-on--click="actions.core.navigation.openMenuOnClick" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - '; - $responsive_container_directives = ' - data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" - data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" - data-wp-effect="effects.core.navigation.initMenu" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" - tabindex="-1" - '; - $responsive_dialog_directives = ' - data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" - data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" - data-wp-bind--role="selectors.core.navigation.roleAttribute" - data-wp-effect="effects.core.navigation.focusFirstElement" - '; - $close_button_directives = ' - data-wp-on--click="actions.core.navigation.closeMenuOnClick" - '; - } - - return sprintf( - ' -
    -
    -
    - -
    - %2$s -
    -
    -
    -
    ', - esc_attr( $modal_unique_id ), - $inner_blocks_html, - $toggle_aria_label_open, - $toggle_aria_label_close, - esc_attr( implode( ' ', $responsive_container_classes ) ), - esc_attr( implode( ' ', $open_button_classes ) ), - esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), - $toggle_button_content, - $toggle_close_button_content, - $open_button_directives, - $responsive_container_directives, - $responsive_dialog_directives, - $close_button_directives - ); - } - - /** - * Get the wrapper attributes - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks A list of inner blocks. - * @param string $nav_menu_name The name of the navigation menu. - * @return string Returns the navigation block markup. - */ - private static function get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => $class, - 'style' => $style, - 'aria-label' => $nav_menu_name, - ) - ); - - if ( $is_responsive_menu ) { - $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); - $wrapper_attributes .= ' ' . $nav_element_directives; - } - - return $wrapper_attributes; - } - - /** - * Get the nav element directives - * - * @param bool $should_load_view_script Whether or not the view script should be loaded. - * @return string the directives for the navigation element. - */ - private static function get_nav_element_directives( $should_load_view_script ) { - if ( ! $should_load_view_script ) { - return ''; - } - // When adding to this array be mindful of security concerns. - $nav_element_context = wp_json_encode( - array( - 'core' => array( - 'navigation' => array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - ), - ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP - ); - return ' - data-wp-interactive - data-wp-context=\'' . $nav_element_context . '\' - '; - } - - /** - * Handle view script loading. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @param WP_Block_List $inner_blocks The list of inner blocks. - */ - private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - - $view_js_file = 'wp-block-navigation-view'; - - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - } - - /** - * Returns the markup for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the navigation wrapper markup. - */ - private static function get_wrapper_markup( $attributes, $inner_blocks ) { - $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); - if ( static::is_responsive( $attributes ) ) { - return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); - } - return $inner_blocks_html; - } - - /** - * Renders the navigation block. - * - * @param array $attributes The block attributes. - * @param string $content The saved content. - * @param WP_Block $block The parsed block. - * @return string Returns the navigation block markup. - */ - public static function render( $attributes, $content, $block ) { - static $seen_menu_names = array(); - - $nav_menu_name = static::get_navigation_name( $attributes, $seen_menu_names ); - - /** - * Deprecated: - * The rgbTextColor and rgbBackgroundColor attributes - * have been deprecated in favor of - * customTextColor and customBackgroundColor ones. - * Move the values from old attrs to the new ones. - */ - if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { - $attributes['customTextColor'] = $attributes['rgbTextColor']; - } - - if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { - $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; - } - - unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); - - $inner_blocks = static::get_inner_blocks( $attributes, $block ); - // Prevent navigation blocks referencing themselves from rendering. - if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { - return ''; - } - - // If the menu name has been used previously then append an ID - // to the name to ensure uniqueness across a given post. - if ( isset( $seen_menu_names[ $nav_menu_name ] ) && $seen_menu_names[ $nav_menu_name ] > 1 ) { - $count = $seen_menu_names[ $nav_menu_name ]; - $nav_menu_name = $nav_menu_name . ' ' . ( $count ); - } - - $wrapper_attributes = static::get_nav_wrapper_attributes( $attributes, $inner_blocks, $nav_menu_name ); - - static::handle_view_script_loading( $attributes, $block, $inner_blocks ); - - return sprintf( - '', - $wrapper_attributes, - static::get_wrapper_markup( $attributes, $inner_blocks ) - ); - } -} From e94446acb3071db0b5321e29def411ea58be3043 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 7 Nov 2023 17:03:55 +0400 Subject: [PATCH 16/20] Playwright Utils: Fix 'clickBlockOptionsMenuItem' helper (#55923) --- .../src/editor/click-block-options-menu-item.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts b/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts index 2c473352c6123d..e8c02fb11dcb71 100644 --- a/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts +++ b/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts @@ -12,8 +12,7 @@ import type { Editor } from './index'; export async function clickBlockOptionsMenuItem( this: Editor, label: string ) { await this.clickBlockToolbarButton( 'Options' ); await this.page - .locator( - `role=menu[name="Options"i] >> role=menuitem[name="${ label }"i]` - ) + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: label } ) .click(); } From 3cdfd9d3525307ac6e491694bd3c203557371f4a Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 7 Nov 2023 17:21:03 +0400 Subject: [PATCH 17/20] Migrate 'Meta boxes' e2e tests to Playwright (#55915) * Migrate 'Meta boxes' e2e tests to Playwright * Remove old test file --- .../specs/editor/plugins/meta-boxes.test.js | 137 ------------------ .../specs/editor/plugins/meta-boxes.spec.js | 123 ++++++++++++++++ 2 files changed, 123 insertions(+), 137 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js create mode 100644 test/e2e/specs/editor/plugins/meta-boxes.spec.js diff --git a/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js b/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js deleted file mode 100644 index 3a75f9656c71e5..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - findSidebarPanelToggleButtonWithTitle, - insertBlock, - openDocumentSettingsSidebar, - publishPost, - saveDraft, -} from '@wordpress/e2e-test-utils'; - -describe( 'Meta boxes', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-plugin-meta-box' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); - } ); - - it( 'Should save the post', async () => { - // Save should not be an option for new empty post. - expect( await page.$( '.editor-post-save-draft' ) ).toBe( null ); - - // Add title to enable valid non-empty post save. - await page.type( '.editor-post-title__input', 'Hello Meta' ); - expect( await page.$( '.editor-post-save-draft' ) ).not.toBe( null ); - - await saveDraft(); - - // After saving, affirm that the button returns to Save Draft. - await page.waitForSelector( '.editor-post-save-draft' ); - } ); - - it( 'Should render dynamic blocks when the meta box uses the excerpt for front end rendering', async () => { - // Publish a post so there's something for the latest posts dynamic block to render. - await page.type( '.editor-post-title__input', 'A published post' ); - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Hello there!' ); - await publishPost(); - - // Publish a post with the latest posts dynamic block. - await createNewPost(); - await page.type( '.editor-post-title__input', 'Dynamic block test' ); - await insertBlock( 'Latest Posts' ); - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Check the dynamic block appears. - const latestPostsBlock = await page.waitForSelector( - '.wp-block-latest-posts' - ); - - expect( - await latestPostsBlock.evaluate( ( block ) => block.textContent ) - ).toContain( 'A published post' ); - - expect( - await latestPostsBlock.evaluate( ( block ) => block.textContent ) - ).toContain( 'Dynamic block test' ); - } ); - - it( 'Should render the excerpt in meta based on post content if no explicit excerpt exists', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Excerpt from content.' ); - await page.type( '.editor-post-title__input', 'A published post' ); - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Retrieve the excerpt used as meta. - const metaExcerpt = await page.evaluate( () => { - return document - .querySelector( 'meta[property="gutenberg:hello"]' ) - .getAttribute( 'content' ); - } ); - - expect( metaExcerpt ).toEqual( 'Excerpt from content.' ); - } ); - - it( 'Should render the explicitly set excerpt in meta instead of the content based one', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Excerpt from content.' ); - await page.type( '.editor-post-title__input', 'A published post' ); - - // Open the excerpt panel. - await openDocumentSettingsSidebar(); - const excerptButton = - await findSidebarPanelToggleButtonWithTitle( 'Excerpt' ); - if ( excerptButton ) { - await excerptButton.click( 'button' ); - } - - await page.waitForSelector( '.editor-post-excerpt textarea' ); - - await page.type( - '.editor-post-excerpt textarea', - 'Explicitly set excerpt.' - ); - - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Retrieve the excerpt used as meta. - const metaExcerpt = await page.evaluate( () => { - return document - .querySelector( 'meta[property="gutenberg:hello"]' ) - .getAttribute( 'content' ); - } ); - - expect( metaExcerpt ).toEqual( 'Explicitly set excerpt.' ); - } ); -} ); diff --git a/test/e2e/specs/editor/plugins/meta-boxes.spec.js b/test/e2e/specs/editor/plugins/meta-boxes.spec.js new file mode 100644 index 00000000000000..b901201ff6c1de --- /dev/null +++ b/test/e2e/specs/editor/plugins/meta-boxes.spec.js @@ -0,0 +1,123 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Meta boxes', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-plugin-meta-box' ); + await requestUtils.deleteAllPosts(); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); + } ); + + test( 'Should save the post', async ( { editor, page } ) => { + const saveDraft = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save draft' } ); + + // Save should not be an option for new empty post. + await expect( saveDraft ).toBeDisabled(); + + // Add title to enable valid non-empty post save. + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello Meta' ); + + await expect( saveDraft ).toBeEnabled(); + + await editor.saveDraft(); + + // After saving, affirm that the button returns to Save Draft. + await expect( saveDraft ).toBeEnabled(); + } ); + + test( 'Should render dynamic blocks when the meta box uses the excerpt for front end rendering', async ( { + admin, + editor, + page, + } ) => { + // Publish a post so there's something for the latest posts dynamic block to render. + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'A published post' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Hello there!' ); + await editor.publishPost(); + + // Publish a post with the latest posts dynamic block. + await admin.createNewPost(); + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Dynamic block test' ); + await editor.insertBlock( { name: 'core/latest-posts' } ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + + await expect( + page.locator( '.wp-block-latest-posts > li' ) + ).toContainText( [ 'A published post', 'Dynamic block test' ] ); + } ); + + test( 'Should render the excerpt in meta based on post content if no explicit excerpt exists', async ( { + editor, + page, + } ) => { + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'A published post' ); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page.keyboard.type( 'Excerpt from content.' ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + + await expect( + page.locator( 'meta[property="gutenberg:hello"]' ) + ).toHaveAttribute( 'content', 'Excerpt from content.' ); + } ); + + test( 'Should render the explicitly set excerpt in meta instead of the content based one', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await page.keyboard.type( 'Excerpt from content.' ); + await page + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'A published post' ); + + const documentSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + const excerptButton = documentSettings.getByRole( 'button', { + name: 'Excerpt', + } ); + + // eslint-disable-next-line playwright/no-conditional-in-test + if ( + ( await excerptButton.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await excerptButton.click(); + } + + await documentSettings + .getByRole( 'textbox', { name: 'Write an Excerpt' } ) + .fill( 'Explicitly set excerpt.' ); + + const postId = await editor.publishPost(); + await page.goto( `/?p=${ postId }` ); + + await expect( + page.locator( 'meta[property="gutenberg:hello"]' ) + ).toHaveAttribute( 'content', 'Explicitly set excerpt.' ); + } ); +} ); From 9b1cbc60955123efc0f7b946867257a77ff7e5d1 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 7 Nov 2023 13:22:16 +0000 Subject: [PATCH 18/20] Dataviews: Add icon for the side by side view. (#55925) --- .../src/components/sidebar-dataviews/dataview-item.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js index 45e3a9d50f3f6f..1d24ff8d5ef3b4 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js +++ b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { page, columns } from '@wordpress/icons'; +import { page, columns, pullRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; /** @@ -13,7 +13,7 @@ import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); function getDataViewIcon( type ) { - const icons = { list: page, grid: columns }; + const icons = { list: page, grid: columns, 'side-by-side': pullRight }; return icons[ type ]; } From d3274f615af500e2707b0c023abf7a7fa2baf7d7 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 7 Nov 2023 13:58:05 +0000 Subject: [PATCH 19/20] Fix: 404 link in get-started-with-create-block docs. (#55932) --- docs/getting-started/devenv/get-started-with-create-block.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/devenv/get-started-with-create-block.md b/docs/getting-started/devenv/get-started-with-create-block.md index a01c08a4ce2f44..3a2c6607b82cff 100644 --- a/docs/getting-started/devenv/get-started-with-create-block.md +++ b/docs/getting-started/devenv/get-started-with-create-block.md @@ -1,6 +1,6 @@ # Get started with create-block -Custom blocks for the Block Editor in WordPress are typically registered using plugins and are defined through a specific set of files. The [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package is an officially supported tool to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts.md)) with no configuration required. +Custom blocks for the Block Editor in WordPress are typically registered using plugins and are defined through a specific set of files. The [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package is an officially supported tool to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/)) with no configuration required. The package is designed to help developers quickly set up a block development environment following WordPress best practices. From a82dedf8f9238f7f250f75ef5931b16c8dd1f19c Mon Sep 17 00:00:00 2001 From: Siobhan Bamber Date: Tue, 7 Nov 2023 14:26:40 +0000 Subject: [PATCH 20/20] [RNMobile] Enable rendering a block's SVG icon directly from an XML string (#55742) Enable rendering a block's SVG icon directly from an XML string. --- packages/primitives/src/svg/index.native.js | 1 + test/native/setup.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/primitives/src/svg/index.native.js b/packages/primitives/src/svg/index.native.js index c8c735283c05a0..719f4d233cc3af 100644 --- a/packages/primitives/src/svg/index.native.js +++ b/packages/primitives/src/svg/index.native.js @@ -26,6 +26,7 @@ export { LinearGradient, Stop, Line, + SvgXml, } from 'react-native-svg'; const AnimatedSvg = Animated.createAnimatedComponent( diff --git a/test/native/setup.js b/test/native/setup.js index 00fb95070d84d7..53ab28f861a1ef 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -151,6 +151,7 @@ jest.mock( 'react-native-svg', () => { G: () => 'G', Polygon: () => 'Polygon', Rect: () => 'Rect', + SvgXml: jest.fn(), }; } );