diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index cf70a7d8193ef8..dec0cb4fdad605 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -473,7 +473,7 @@ A collection of blocks that allow visitors to get around your site. ([Source](ht - **Category:** theme - **Allowed Blocks:** core/navigation-link, core/search, core/social-links, core/page-list, core/spacer, core/home-link, core/site-title, core/site-logo, core/navigation-submenu, core/loginout, core/buttons - **Supports:** align (full, wide), ariaLabel, inserter, interactivity, layout (allowSizingOnChildren, default, ~~allowInheriting~~, ~~allowSwitching~~, ~~allowVerticalAlignment~~), spacing (blockGap, units), typography (fontSize, lineHeight), ~~html~~, ~~renaming~~ -- **Attributes:** __unstableLocation, backgroundColor, customBackgroundColor, customOverlayBackgroundColor, customOverlayTextColor, customTextColor, hasIcon, icon, maxNestingLevel, openSubmenusOnClick, overlayBackgroundColor, overlayMenu, overlayTextColor, ref, rgbBackgroundColor, rgbTextColor, showSubmenuIcon, templateLock, textColor +- **Attributes:** __unstableLocation, backgroundColor, customBackgroundColor, customOverlayBackgroundColor, customOverlayTextColor, customTextColor, hasIcon, icon, maxNestingLevel, openSubmenusOnClick, overlayBackgroundColor, overlayMenu, overlayTextColor, ref, rgbBackgroundColor, rgbTextColor, showSubmenuIcon, slug, templateLock, textColor ## Custom Link diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index b80703dcc67b18..afb373fb7a7104 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -374,6 +374,23 @@ _Returns_ - `any`: The entity record's save error. +### getNavigationMenu + +Undocumented declaration. + +### getNavigationMenuBySlug + +Returns a Navigation Menu object by slug. + +_Parameters_ + +- _state_ `State`: +- _slug_ `string`: the slug of the Navigation Menu. + +_Returns_ + +- `Object | null`: The Navigation Menu object. + ### getRawEntityRecord Returns the entity's record object by key, with its attributes mapped to their raw values. diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index eef6af390de78a..62272a77593ff8 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -24,6 +24,9 @@ "ref": { "type": "number" }, + "slug": { + "type": "string" + }, "textColor": { "type": "string" }, diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 3b526ddb85dc69..924c2abfb469c5 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -108,11 +108,13 @@ function Navigation( { icon = 'handle', } = attributes; - const ref = attributes.ref; + // Older versions of the block used an ID based ref attribute. + // Allow for this to continue to be used. + const ref = attributes.slug || attributes.ref; const setRef = useCallback( - ( postId ) => { - setAttributes( { ref: postId } ); + ( { slug } ) => { + setAttributes( { slug } ); }, [ setAttributes ] ); @@ -190,6 +192,7 @@ function Navigation( { canUserCreateNavigationMenu, isResolvingCanUserCreateNavigationMenu, hasResolvedCanUserCreateNavigationMenu, + navigationMenu, } = useNavigationMenu( ref ); const navMenuResolvedButMissing = @@ -205,9 +208,10 @@ function Navigation( { classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING; const handleUpdateMenu = useCallback( - ( menuId, options = { focusNavigationBlock: false } ) => { + ( menu, options = { focusNavigationBlock: false } ) => { + debugger; const { focusNavigationBlock } = options; - setRef( menuId ); + setRef( menu ); if ( focusNavigationBlock ) { selectBlock( clientId ); } @@ -342,7 +346,16 @@ function Navigation( { const [ detectedOverlayColor, setDetectedOverlayColor ] = useState(); const onSelectClassicMenu = async ( classicMenu ) => { - return convertClassicMenu( classicMenu.id, classicMenu.name, 'draft' ); + const navMenu = await convertClassicMenu( + classicMenu.id, + classicMenu.name, + 'draft' + ); + if ( navMenu ) { + handleUpdateMenu( navMenu, { + focusNavigationBlock: true, + } ); + } }; const onSelectNavigationMenu = ( menuId ) => { @@ -357,7 +370,7 @@ function Navigation( { } if ( createNavigationMenuIsSuccess ) { - handleUpdateMenu( createNavigationMenuPost?.id, { + handleUpdateMenu( createNavigationMenuPost, { focusNavigationBlock: true, } ); @@ -374,7 +387,7 @@ function Navigation( { }, [ createNavigationMenuStatus, createNavigationMenuError, - createNavigationMenuPost?.id, + createNavigationMenuPost, createNavigationMenuIsError, createNavigationMenuIsSuccess, isCreatingNavigationMenu, @@ -393,7 +406,7 @@ function Navigation( { showClassicMenuConversionNotice( __( 'Classic menu imported successfully.' ) ); - handleUpdateMenu( createNavigationMenuPost?.id, { + handleUpdateMenu( createNavigationMenuPost, { focusNavigationBlock: true, } ); } @@ -408,7 +421,7 @@ function Navigation( { classicMenuConversionError, hideClassicMenuConversionNotice, showClassicMenuConversionNotice, - createNavigationMenuPost?.id, + createNavigationMenuPost, handleUpdateMenu, ] ); @@ -814,7 +827,11 @@ function Navigation( { } return ( - + { return ( - navigationMenus?.map( ( { id, title, status }, index ) => { + navigationMenus?.map( ( { id, title, status, slug }, index ) => { const label = buildMenuLabel( title?.rendered, index + 1, @@ -79,7 +81,7 @@ function NavigationMenuSelector( { ); return { - value: id, + value: slug, label, ariaLabel: sprintf( actionLabel, label ), disabled: diff --git a/packages/block-library/src/navigation/edit/test/utils.js b/packages/block-library/src/navigation/edit/test/utils.js new file mode 100644 index 00000000000000..07bfb02ed416df --- /dev/null +++ b/packages/block-library/src/navigation/edit/test/utils.js @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +import { isNumeric } from '../utils'; + +describe( 'isNumeric', () => { + it.each( [ + [ 42, true ], + [ '42', true ], + [ ' 42 ', true ], + [ '', false ], + [ 'some-slug', false ], + [ 'some-42-slug-with-trailing-number-42', false ], + [ '42-some-42-slug-with-leading-number', false ], + [ NaN, false ], + [ Infinity, false ], + ] )( + 'correctly determines variable type for "%s"', + ( candidate, expected ) => { + expect( isNumeric( candidate ) ).toBe( expected ); + } + ); +} ); diff --git a/packages/block-library/src/navigation/edit/use-create-navigation-menu.js b/packages/block-library/src/navigation/edit/use-create-navigation-menu.js index b4e7372f03cbc5..ab180f5fbf5213 100644 --- a/packages/block-library/src/navigation/edit/use-create-navigation-menu.js +++ b/packages/block-library/src/navigation/edit/use-create-navigation-menu.js @@ -27,7 +27,7 @@ export default function useCreateNavigationMenu( clientId ) { // This callback uses data from the two placeholder steps and only creates // a new navigation menu when the user completes the final step. const create = useCallback( - async ( title = null, blocks = [], postStatus ) => { + async ( title = null, blocks = [], postStatus, slug = '' ) => { // Guard against creating Navigations without a title. // Note you can pass no title, but if one is passed it must be // a string otherwise the title may end up being empty. @@ -57,8 +57,12 @@ export default function useCreateNavigationMenu( clientId ) { ); } ); } + + const maybeSlug = slug || title; + const record = { title, + slug: maybeSlug, content: serialize( blocks ), status: postStatus, }; diff --git a/packages/block-library/src/navigation/edit/utils.js b/packages/block-library/src/navigation/edit/utils.js index 2c436af8c5754f..d0d6661fc583ef 100644 --- a/packages/block-library/src/navigation/edit/utils.js +++ b/packages/block-library/src/navigation/edit/utils.js @@ -110,3 +110,10 @@ export function getNavigationChildBlockProps( innerBlocksColors ) { }, }; } + +export function isNumeric( v ) { + if ( v === null || v === undefined || v === '' ) { + return false; + } + return ! isNaN( v ) && isFinite( v ); +} diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 1a8b523e9328bc..88fa4f5f255372 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -204,7 +204,41 @@ private static function get_inner_blocks_html( $attributes, $inner_blocks ) { * @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'] ); + $base_args = array( + 'post_type' => 'wp_navigation', + 'nopaging' => true, + 'posts_per_page' => '1', + 'update_post_term_cache' => false, + 'no_found_rows' => true, + ); + + // Prefer query by slug if available, falling + // back to Post ID. + if ( ! empty( $attributes['slug'] ) ) { + $args = array_merge( + $base_args, + array( + 'name' => $attributes['slug'], // query by slug + ) + ); + } else { + $args = array_merge( + $base_args, + array( + 'p' => $attributes['ref'], // query by post ID + ) + ); + } + + // Query for the Navigation Post. + $navigation_query = new WP_Query( $args ); + + if ( ! isset( $navigation_query->posts[0] ) ) { + return ''; + } + + $navigation_post = $navigation_query->posts[0]; + if ( ! isset( $navigation_post ) ) { return new WP_Block_List( array(), $attributes ); } @@ -280,7 +314,7 @@ private static function get_inner_blocks( $attributes, $block ) { } // Load inner blocks from the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { + if ( array_key_exists( 'slug', $attributes ) || array_key_exists( 'ref', $attributes ) ) { $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); } diff --git a/packages/block-library/src/navigation/use-navigation-menu.js b/packages/block-library/src/navigation/use-navigation-menu.js index 02df0ba2831dcc..696b2fb937c889 100644 --- a/packages/block-library/src/navigation/use-navigation-menu.js +++ b/packages/block-library/src/navigation/use-navigation-menu.js @@ -8,6 +8,11 @@ import { } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { isNumeric } from './edit/utils'; + /** * Internal dependencies */ @@ -70,15 +75,34 @@ function selectExistingMenu( select, ref ) { }; } - const { getEntityRecord, getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); + const { + getNavigationMenuBySlug, + getNavigationMenu, + // getEntityRecord, + getEditedEntityRecord, + hasFinishedResolution, + } = select( coreStore ); + + // const args = [ 'postType', 'wp_navigation', ref ]; + // const navigationMenu = getEntityRecord( ...args ); + + const navigationMenu = isNumeric( ref ) + ? getNavigationMenu( ref ) + : getNavigationMenuBySlug( ref ); + + const hasNavigationMenu = !! navigationMenu; + + const editedNavigationMenu = hasNavigationMenu + ? getEditedEntityRecord( + 'postType', + 'wp_navigation', + navigationMenu.id + ) + : null; - const args = [ 'postType', 'wp_navigation', ref ]; - const navigationMenu = getEntityRecord( ...args ); - const editedNavigationMenu = getEditedEntityRecord( ...args ); const hasResolvedNavigationMenu = hasFinishedResolution( 'getEditedEntityRecord', - args + [ 'postType', 'wp_navigation', navigationMenu?.id ] ); // Only published Navigation posts are considered valid. @@ -86,14 +110,21 @@ function selectExistingMenu( select, ref ) { // requiring a post update to publish to show in frontend. // To achieve that, index.php must reflect this validation only for published. const isNavigationMenuPublishedOrDraft = - editedNavigationMenu.status === 'publish' || - editedNavigationMenu.status === 'draft'; + editedNavigationMenu?.status === 'publish' || + editedNavigationMenu?.status === 'draft'; + + console.log( { + navigationMenu, + editedNavigationMenu, + hasResolvedNavigationMenu, + isNavigationMenuPublishedOrDraft, + } ); return { isNavigationMenuResolved: hasResolvedNavigationMenu, isNavigationMenuMissing: hasResolvedNavigationMenu && - ( ! navigationMenu || ! isNavigationMenuPublishedOrDraft ), + ( ! hasNavigationMenu || ! isNavigationMenuPublishedOrDraft ), // getEditedEntityRecord will return the post regardless of status. // Therefore if the found post is not published then we should ignore it. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 6677a32df08dc9..5de292b2eb5cd8 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -695,6 +695,23 @@ _Returns_ - `any`: The entity record's save error. +### getNavigationMenu + +Undocumented declaration. + +### getNavigationMenuBySlug + +Returns a Navigation Menu object by slug. + +_Parameters_ + +- _state_ `State`: +- _slug_ `string`: the slug of the Navigation Menu. + +_Returns_ + +- `Object | null`: The Navigation Menu object. + ### getRawEntityRecord Returns the entity's record object by key, with its attributes mapped to their raw values. diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 003d01e49641d9..23aa0d509befd1 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -889,3 +889,18 @@ export const getRevision = dispatch.receiveRevisions( kind, name, recordKey, record, query ); } }; + +export const getNavigationMenu = + ( id ) => + async ( { resolveSelect } ) => { + resolveSelect.getEntityRecord( 'postType', 'wp_navigation', id ); + }; + +export const getNavigationMenuBySlug = + ( slug ) => + async ( { resolveSelect } ) => { + resolveSelect.getEntityRecords( 'postType', 'wp_navigation', { + slug, + per_page: 1, + } ); + }; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index a8486494302600..a2db6c48021752 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -1492,3 +1492,39 @@ export const getRevision = createSelector( ]; } ); + +export function getNavigationMenu( + state: State, + id: EntityRecordKey +): Object | null { + const record = getEntityRecord( state, 'postType', 'wp_navigation', id ); + + if ( ! record ) { + return null; + } + + return record; +} + +/** + * Returns a Navigation Menu object by slug. + * + * @param state + * @param slug the slug of the Navigation Menu. + * @return The Navigation Menu object. + */ +export function getNavigationMenuBySlug( + state: State, + slug: string +): Object | null { + const records = getEntityRecords( state, 'postType', 'wp_navigation', { + slug, + per_page: 1, + } ); + + if ( ! records?.length ) { + return null; + } + + return records[ 0 ]; +}