diff --git a/packages/block-editor/src/components/link-control/use-search-handler.js b/packages/block-editor/src/components/link-control/use-search-handler.js index 455d228be1974..80cef60f6329b 100644 --- a/packages/block-editor/src/components/link-control/use-search-handler.js +++ b/packages/block-editor/src/components/link-control/use-search-handler.js @@ -17,8 +17,7 @@ import { URL_TYPE, } from './constants'; import { store as blockEditorStore } from '../../store'; - -export const handleNoop = () => Promise.resolve( [] ); +import { default as settingsKeys } from '../../private-settings-keys'; export const handleDirectEntry = ( val ) => { let type = URL_TYPE; @@ -47,103 +46,65 @@ export const handleDirectEntry = ( val ) => { ] ); }; -const handleEntitySearch = async ( - val, - suggestionsQuery, - fetchSearchSuggestions, - withCreateSuggestion, - pageOnFront, - pageForPosts -) => { - const { isInitialSuggestions } = suggestionsQuery; - - const results = await fetchSearchSuggestions( val, suggestionsQuery ); - - // Identify front page and update type to match. - results.map( ( result ) => { - if ( Number( result.id ) === pageOnFront ) { - result.isFrontPage = true; - return result; - } else if ( Number( result.id ) === pageForPosts ) { - result.isBlogHome = true; - return result; - } - - return result; - } ); - - // If displaying initial suggestions just return plain results. - if ( isInitialSuggestions ) { - return results; - } - - // Here we append a faux suggestion to represent a "CREATE" option. This - // is detected in the rendering of the search results and handled as a - // special case. This is currently necessary because the suggestions - // dropdown will only appear if there are valid suggestions and - // therefore unless the create option is a suggestion it will not - // display in scenarios where there are no results returned from the - // API. In addition promoting CREATE to a first class suggestion affords - // the a11y benefits afforded by `URLInput` to all suggestions (eg: - // keyboard handling, ARIA roles...etc). - // - // Note also that the value of the `title` and `url` properties must correspond - // to the text value of the ``. This is because `title` is used - // when creating the suggestion. Similarly `url` is used when using keyboard to select - // the suggestion (the
`onSubmit` handler falls-back to `url`). - return isURLLike( val ) || ! withCreateSuggestion - ? results - : results.concat( { - // the `id` prop is intentionally ommitted here because it - // is never exposed as part of the component's public API. - // see: https://github.com/WordPress/gutenberg/pull/19775#discussion_r378931316. - title: val, // Must match the existing ``s text value. - url: val, // Must match the existing ``s text value. - type: CREATE_TYPE, - } ); -}; - export default function useSearchHandler( suggestionsQuery, allowDirectEntry, withCreateSuggestion ) { - const { fetchSearchSuggestions, pageOnFront, pageForPosts } = useSelect( - ( select ) => { - const { getSettings } = select( blockEditorStore ); - - return { - pageOnFront: getSettings().pageOnFront, - pageForPosts: getSettings().pageForPosts, - fetchSearchSuggestions: - getSettings().__experimentalFetchLinkSuggestions, - }; - }, - [] - ); - + const handleNoop = useCallback( () => Promise.resolve( [] ), [] ); const directEntryHandler = allowDirectEntry ? handleDirectEntry : handleNoop; + const useHandleEntitySearch = useSelect( ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + return settings[ settingsKeys.useLinkControlEntitySearch ]; + }, [] ); + // Note that this is meant to be a stable function and doesn't change, so it + // doesn't break the rules of hooks. + const handleEntitySearch = useHandleEntitySearch?.() || handleNoop; + return useCallback( - ( val, { isInitialSuggestions } ) => { - return isURLLike( val ) - ? directEntryHandler( val, { isInitialSuggestions } ) - : handleEntitySearch( - val, - { ...suggestionsQuery, isInitialSuggestions }, - fetchSearchSuggestions, - withCreateSuggestion, - pageOnFront, - pageForPosts - ); + async ( val, { isInitialSuggestions } ) => { + if ( isURLLike( val ) ) { + return directEntryHandler( val, { isInitialSuggestions } ); + } + + const results = await handleEntitySearch( val, suggestionsQuery ); + + // If displaying initial suggestions just return plain results. + if ( isInitialSuggestions ) { + return results; + } + + // Here we append a faux suggestion to represent a "CREATE" option. This + // is detected in the rendering of the search results and handled as a + // special case. This is currently necessary because the suggestions + // dropdown will only appear if there are valid suggestions and + // therefore unless the create option is a suggestion it will not + // display in scenarios where there are no results returned from the + // API. In addition promoting CREATE to a first class suggestion affords + // the a11y benefits afforded by `URLInput` to all suggestions (eg: + // keyboard handling, ARIA roles...etc). + // + // Note also that the value of the `title` and `url` properties must correspond + // to the text value of the ``. This is because `title` is used + // when creating the suggestion. Similarly `url` is used when using keyboard to select + // the suggestion (the `onSubmit` handler falls-back to `url`). + return ! withCreateSuggestion + ? results + : results.concat( { + // the `id` prop is intentionally ommitted here because it + // is never exposed as part of the component's public API. + // see: https://github.com/WordPress/gutenberg/pull/19775#discussion_r378931316. + title: val, // Must match the existing ``s text value. + url: val, // Must match the existing ``s text value. + type: CREATE_TYPE, + } ); }, [ directEntryHandler, - fetchSearchSuggestions, - pageOnFront, - pageForPosts, + handleEntitySearch, suggestionsQuery, withCreateSuggestion, ] diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 74bf4af421dfb..ec51a20f845d2 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -24,6 +24,7 @@ import { import { usesContextKey } from './components/rich-text/format-edit'; import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; +import { default as settingsKeys } from './private-settings-keys'; /** * Private @wordpress/block-editor APIs. @@ -52,4 +53,5 @@ lock( privateApis, { ReusableBlocksRenameHint, useReusableBlocksRenameHint, usesContextKey, + settingsKeys, } ); diff --git a/packages/block-editor/src/private-apis.native.js b/packages/block-editor/src/private-apis.native.js index 5555e00477e7b..f710cbdc2aa3a 100644 --- a/packages/block-editor/src/private-apis.native.js +++ b/packages/block-editor/src/private-apis.native.js @@ -4,6 +4,7 @@ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; +import { default as settingsKeys } from './private-settings-keys'; /** * Private @wordpress/block-editor APIs. @@ -12,4 +13,5 @@ export const privateApis = {}; lock( privateApis, { ...globalStyles, ExperimentalBlockEditorProvider, + settingsKeys, } ); diff --git a/packages/block-editor/src/private-settings-keys.js b/packages/block-editor/src/private-settings-keys.js new file mode 100644 index 0000000000000..e58de7c064145 --- /dev/null +++ b/packages/block-editor/src/private-settings-keys.js @@ -0,0 +1,3 @@ +export default { + useLinkControlEntitySearch: Symbol( 'useLinkControlEntitySearch' ), +}; diff --git a/packages/core-data/src/fetch/index.js b/packages/core-data/src/fetch/index.js index 8d4d28e3b0db8..af75a9f5fbaf1 100644 --- a/packages/core-data/src/fetch/index.js +++ b/packages/core-data/src/fetch/index.js @@ -1,2 +1,56 @@ -export { default as __experimentalFetchLinkSuggestions } from './__experimental-fetch-link-suggestions'; +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { store as coreStore } from '../'; +import { default as fetchLinkSuggestions } from './__experimental-fetch-link-suggestions'; + +export const __experimentalFetchLinkSuggestions = fetchLinkSuggestions; export { default as __experimentalFetchUrlData } from './__experimental-fetch-url-data'; + +export function __experimentalUseLinkControlEntitySearch() { + const settings = useSelect( + ( select ) => select( blockEditorStore ).getSettings(), + [] + ); + // The function should either be undefined or a stable function reference + // throughout the editor lifetime, much like importing a function from a + // module. + const { pageOnFront, pageForPosts } = useSelect( ( select ) => { + const { canUser, getEntityRecord } = select( coreStore ); + + const siteSettings = canUser( 'read', 'settings' ) + ? getEntityRecord( 'root', 'site' ) + : undefined; + + return { + pageOnFront: siteSettings?.page_on_front, + pageForPosts: siteSettings?.page_for_posts, + }; + }, [] ); + + return useCallback( + async ( val, suggestionsQuery ) => { + return ( + await fetchLinkSuggestions( val, suggestionsQuery, settings ) + ).map( ( result ) => { + if ( Number( result.id ) === pageOnFront ) { + result.isFrontPage = true; + return result; + } else if ( Number( result.id ) === pageForPosts ) { + result.isBlogHome = true; + return result; + } + + return result; + } ); + }, + [ pageOnFront, pageForPosts, settings ] + ); +} diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index 4abc420434cc4..24df41c54637a 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -9,6 +9,7 @@ import { useEntityBlockEditor, store as coreStore, useResourcePermissions, + __experimentalUseLinkControlEntitySearch as useLinkControlEntitySearch, } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; @@ -25,8 +26,11 @@ import { store as editWidgetsStore } from '../../store'; import { ALLOW_REUSABLE_BLOCKS } from '../../constants'; import { unlock } from '../../lock-unlock'; -const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); +const { ExperimentalBlockEditorProvider, settingsKeys } = unlock( + blockEditorPrivateApis +); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); + export default function WidgetAreasBlockEditorProvider( { blockEditorSettings, children, @@ -34,36 +38,25 @@ export default function WidgetAreasBlockEditorProvider( { } ) { const mediaPermissions = useResourcePermissions( 'media' ); const isLargeViewport = useViewportMatch( 'medium' ); - const { - reusableBlocks, - isFixedToolbarActive, - keepCaretInsideBlock, - pageOnFront, - pageForPosts, - } = useSelect( ( select ) => { - const { canUser, getEntityRecord, getEntityRecords } = - select( coreStore ); - const siteSettings = canUser( 'read', 'settings' ) - ? getEntityRecord( 'root', 'site' ) - : undefined; - return { - widgetAreas: select( editWidgetsStore ).getWidgetAreas(), - widgets: select( editWidgetsStore ).getWidgets(), - reusableBlocks: ALLOW_REUSABLE_BLOCKS - ? getEntityRecords( 'postType', 'wp_block' ) - : [], - isFixedToolbarActive: !! select( preferencesStore ).get( - 'core/edit-widgets', - 'fixedToolbar' - ), - keepCaretInsideBlock: !! select( preferencesStore ).get( - 'core/edit-widgets', - 'keepCaretInsideBlock' - ), - pageOnFront: siteSettings?.page_on_front, - pageForPosts: siteSettings?.page_for_posts, - }; - }, [] ); + const { reusableBlocks, isFixedToolbarActive, keepCaretInsideBlock } = + useSelect( ( select ) => { + const { getEntityRecords } = select( coreStore ); + return { + widgetAreas: select( editWidgetsStore ).getWidgetAreas(), + widgets: select( editWidgetsStore ).getWidgets(), + reusableBlocks: ALLOW_REUSABLE_BLOCKS + ? getEntityRecords( 'postType', 'wp_block' ) + : [], + isFixedToolbarActive: !! select( preferencesStore ).get( + 'core/edit-widgets', + 'fixedToolbar' + ), + keepCaretInsideBlock: !! select( preferencesStore ).get( + 'core/edit-widgets', + 'keepCaretInsideBlock' + ), + }; + }, [] ); const { setIsInserterOpened } = useDispatch( editWidgetsStore ); const settings = useMemo( () => { @@ -85,8 +78,8 @@ export default function WidgetAreasBlockEditorProvider( { mediaUpload: mediaUploadBlockEditor, templateLock: 'all', __experimentalSetIsInserterOpened: setIsInserterOpened, - pageOnFront, - pageForPosts, + [ settingsKeys.useLinkControlEntitySearch ]: + useLinkControlEntitySearch, }; }, [ blockEditorSettings, @@ -96,8 +89,6 @@ export default function WidgetAreasBlockEditorProvider( { mediaPermissions.canCreate, reusableBlocks, setIsInserterOpened, - pageOnFront, - pageForPosts, ] ); const widgetAreaId = useLastSelectedWidgetArea(); diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 49f61815f663d..b457962606d57 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -5,10 +5,12 @@ import { Platform, useMemo, useCallback } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore, + __experimentalUseLinkControlEntitySearch as useLinkControlEntitySearch, __experimentalFetchLinkSuggestions as fetchLinkSuggestions, __experimentalFetchUrlData as fetchUrlData, } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies @@ -16,6 +18,9 @@ import { __ } from '@wordpress/i18n'; import inserterMediaCategories from '../media-categories'; import { mediaUpload } from '../../utils'; import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +const { settingsKeys } = unlock( blockEditorPrivateApis ); const EMPTY_BLOCKS_LIST = []; @@ -93,8 +98,6 @@ function useBlockEditorSettings( settings, postType, postId ) { hasUploadPermissions, canUseUnfilteredHTML, userCanCreatePages, - pageOnFront, - pageForPosts, userPatternCategories, restBlockPatterns, restBlockPatternCategories, @@ -104,17 +107,12 @@ function useBlockEditorSettings( settings, postType, postId ) { const { canUser, getRawEntityRecord, - getEntityRecord, getUserPatternCategories, getEntityRecords, getBlockPatterns, getBlockPatternCategories, } = select( coreStore ); - const siteSettings = canUser( 'read', 'settings' ) - ? getEntityRecord( 'root', 'site' ) - : undefined; - return { canUseUnfilteredHTML: getRawEntityRecord( 'postType', @@ -128,8 +126,6 @@ function useBlockEditorSettings( settings, postType, postId ) { : EMPTY_BLOCKS_LIST, // Reusable blocks are fetched in the native version of this hook. hasUploadPermissions: canUser( 'create', 'media' ) ?? true, userCanCreatePages: canUser( 'create', 'pages' ), - pageOnFront: siteSettings?.page_on_front, - pageForPosts: siteSettings?.page_for_posts, userPatternCategories: getUserPatternCategories(), restBlockPatterns: getBlockPatterns(), restBlockPatternCategories: getBlockPatternCategories(), @@ -214,6 +210,7 @@ function useBlockEditorSettings( settings, postType, postId ) { __experimentalBlockPatterns: blockPatterns, __experimentalBlockPatternCategories: blockPatternCategories, __experimentalUserPatternCategories: userPatternCategories, + // We still need this for mobile and URLInput. __experimentalFetchLinkSuggestions: ( search, searchOptions ) => fetchLinkSuggestions( search, searchOptions, settings ), inserterMediaCategories, @@ -229,8 +226,8 @@ function useBlockEditorSettings( settings, postType, postId ) { // Check these two properties: they were not present in the site editor. __experimentalCreatePageEntity: createPageEntity, __experimentalUserCanCreatePages: userCanCreatePages, - pageOnFront, - pageForPosts, + [ settingsKeys.useLinkControlEntitySearch ]: + useLinkControlEntitySearch, __experimentalPreferPatternsOnRoot: postType === 'wp_template', templateLock: postType === 'wp_navigation' ? 'insert' : settings.templateLock, @@ -251,8 +248,6 @@ function useBlockEditorSettings( settings, postType, postId ) { undo, createPageEntity, userCanCreatePages, - pageOnFront, - pageForPosts, postType, setIsInserterOpened, ]