diff --git a/package-lock.json b/package-lock.json
index 81b7f2e4bb11da..f57d6e92cc0fd8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12531,6 +12531,7 @@
"@wordpress/data": "file:packages/data",
"@wordpress/data-controls": "file:packages/data-controls",
"@wordpress/element": "file:packages/element",
+ "@wordpress/html-entities": "file:packages/html-entities",
"@wordpress/i18n": "file:packages/i18n",
"@wordpress/is-shallow-equal": "file:packages/is-shallow-equal",
"@wordpress/url": "file:packages/url",
diff --git a/packages/core-data/package.json b/packages/core-data/package.json
index ce47b5d8b80ffe..cf26b01a42e066 100644
--- a/packages/core-data/package.json
+++ b/packages/core-data/package.json
@@ -32,6 +32,7 @@
"@wordpress/data": "file:../data",
"@wordpress/data-controls": "file:../data-controls",
"@wordpress/element": "file:../element",
+ "@wordpress/html-entities": "file:../html-entities",
"@wordpress/i18n": "file:../i18n",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"@wordpress/url": "file:../url",
diff --git a/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js
new file mode 100644
index 00000000000000..ef1a80c95c2437
--- /dev/null
+++ b/packages/core-data/src/fetch/__experimental-fetch-link-suggestions.js
@@ -0,0 +1,155 @@
+/**
+ * WordPress dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+import { decodeEntities } from '@wordpress/html-entities';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Filters the search by type
+ *
+ * @typedef { 'post' | 'term' | 'post-format' } WPLinkSearchType
+ */
+
+/**
+ * @typedef WPLinkSearchOptions
+ *
+ * @property {boolean} [isInitialSuggestions] Displays initial search suggestions, when true.
+ * @property {WPLinkSearchType} [type] Filters by search type.
+ * @property {string} [subtype] Slug of the post-type or taxonomy.
+ * @property {number} [page] Which page of results to return.
+ * @property {number} [perPage] Search results per page.
+ */
+
+/**
+ * @typedef WPLinkSearchResult
+ *
+ * @property {number} id Post or term id.
+ * @property {string} url Link url.
+ * @property {string} title Title of the link.
+ * @property {string} type The taxonomy or post type slug or type URL.
+ */
+
+/**
+ * @typedef WPEditorSettings
+ *
+ * @property {boolean} [ disablePostFormats ] Disables post formats, when true.
+ */
+
+/**
+ * Fetches link suggestions from the API.
+ *
+ * @async
+ * @param {string} search
+ * @param {WPLinkSearchOptions} [searchOptions]
+ * @param {WPEditorSettings} [settings]
+ *
+ * @example
+ * ```js
+ * import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data';
+ *
+ * //...
+ *
+ * export function initialize( id, settings ) {
+ *
+ * settings.__experimentalFetchLinkSuggestions = (
+ * search,
+ * searchOptions
+ * ) => fetchLinkSuggestions( search, searchOptions, settings );
+ * ```
+ * @return {Promise< WPLinkSearchResult[] >} List of search suggestions
+ */
+const fetchLinkSuggestions = async (
+ search,
+ searchOptions = {},
+ settings = {}
+) => {
+ const {
+ isInitialSuggestions = false,
+ type = undefined,
+ subtype = undefined,
+ page = undefined,
+ perPage = isInitialSuggestions ? 3 : 20,
+ } = searchOptions;
+
+ const { disablePostFormats = false } = settings;
+
+ const queries = [];
+
+ if ( ! type || type === 'post' ) {
+ queries.push(
+ apiFetch( {
+ path: addQueryArgs( '/wp/v2/search', {
+ search,
+ page,
+ per_page: perPage,
+ type: 'post',
+ subtype,
+ } ),
+ } ).catch( () => [] ) // fail by returning no results
+ );
+ }
+
+ if ( ! type || type === 'term' ) {
+ queries.push(
+ apiFetch( {
+ path: addQueryArgs( '/wp/v2/search', {
+ search,
+ page,
+ per_page: perPage,
+ type: 'term',
+ subtype,
+ } ),
+ } ).catch( () => [] )
+ );
+ }
+
+ if ( ! disablePostFormats && ( ! type || type === 'post-format' ) ) {
+ queries.push(
+ apiFetch( {
+ path: addQueryArgs( '/wp/v2/search', {
+ search,
+ page,
+ per_page: perPage,
+ type: 'post-format',
+ subtype,
+ } ),
+ } ).catch( () => [] )
+ );
+ }
+
+ return Promise.all( queries ).then( ( results ) => {
+ return results
+ .reduce(
+ ( accumulator, current ) => accumulator.concat( current ), //flatten list
+ []
+ )
+ .filter(
+ /**
+ * @param {{ id: number }} result
+ */
+ ( result ) => {
+ return !! result.id;
+ }
+ )
+ .slice( 0, perPage )
+ .map(
+ /**
+ * @param {{ id: number, url:string, title?:string, subtype?: string, type?: string }} result
+ */
+ ( result ) => {
+ return {
+ id: result.id,
+ url: result.url,
+ title:
+ decodeEntities( result.title || '' ) ||
+ __( '(no title)' ),
+ type: result.subtype || result.type,
+ };
+ }
+ );
+ } );
+};
+
+export default fetchLinkSuggestions;
diff --git a/packages/core-data/src/fetch/index.js b/packages/core-data/src/fetch/index.js
new file mode 100644
index 00000000000000..f01b9008b8196f
--- /dev/null
+++ b/packages/core-data/src/fetch/index.js
@@ -0,0 +1 @@
+export { default as __experimentalFetchLinkSuggestions } from './__experimental-fetch-link-suggestions';
diff --git a/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js b/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js
new file mode 100644
index 00000000000000..82a68cc7974396
--- /dev/null
+++ b/packages/core-data/src/fetch/test/__experimental-fetch-link-suggestions.js
@@ -0,0 +1,218 @@
+/**
+ * Internal dependencies
+ */
+import fetchLinkSuggestions from '../__experimental-fetch-link-suggestions';
+
+jest.mock( '@wordpress/api-fetch', () =>
+ jest.fn( ( { path } ) => {
+ switch ( path ) {
+ case '/wp/v2/search?search=&per_page=20&type=post':
+ case '/wp/v2/search?search=Contact&per_page=20&type=post&subtype=page':
+ return Promise.resolve( [
+ {
+ id: 37,
+ title: 'Contact Page',
+ url: 'http://wordpress.local/contact-page/',
+ type: 'post',
+ subtype: 'page',
+ },
+ ] );
+ case '/wp/v2/search?search=&per_page=20&type=term':
+ case '/wp/v2/search?search=cat&per_page=20&type=term&subtype=category':
+ return Promise.resolve( [
+ {
+ id: 9,
+ title: 'Cats',
+ url: 'http://wordpress.local/category/cats/',
+ type: 'category',
+ },
+ {
+ id: 1,
+ title: 'Uncategorized',
+ url: 'http://wordpress.local/category/uncategorized/',
+ type: 'category',
+ },
+ ] );
+ case '/wp/v2/search?search=&per_page=20&type=post-format':
+ return Promise.resolve( [
+ {
+ id: 'gallery',
+ title: 'Gallery',
+ url: 'http://wordpress.local/type/gallery/',
+ type: 'post-format',
+ },
+ {
+ id: 'quote',
+ title: 'Quote',
+ url: 'http://wordpress.local/type/quote/',
+ type: 'post-format',
+ },
+ ] );
+ case '/wp/v2/search?search=&per_page=3&type=post&subtype=page':
+ return Promise.resolve( [
+ {
+ id: 11,
+ title: 'Limit Case',
+ url: 'http://wordpress.local/limit-case/',
+ type: 'post',
+ subtype: 'page',
+ },
+ ] );
+ case '/wp/v2/search?search=&page=11&per_page=20&type=post&subtype=page':
+ return Promise.resolve( [
+ {
+ id: 22,
+ title: 'Page Case',
+ url: 'http://wordpress.local/page-case/',
+ type: 'post',
+ subtype: 'page',
+ },
+ ] );
+ default:
+ return Promise.resolve( [
+ {
+ id: -1,
+ title: 'missing case or failed',
+ url: path,
+ type: 'missing case or failed',
+ },
+ ] );
+ }
+ } )
+);
+
+describe( 'fetchLinkSuggestions', () => {
+ it( 'filters suggestions by post-type', () => {
+ return fetchLinkSuggestions( 'Contact', {
+ type: 'post',
+ subtype: 'page',
+ } ).then( ( suggestions ) =>
+ expect( suggestions ).toEqual( [
+ {
+ id: 37,
+ title: 'Contact Page',
+ type: 'page',
+ url: 'http://wordpress.local/contact-page/',
+ },
+ ] )
+ );
+ } );
+ it( 'filters suggestions by term', () => {
+ return fetchLinkSuggestions( 'cat', {
+ type: 'term',
+ subtype: 'category',
+ } ).then( ( suggestions ) =>
+ expect( suggestions ).toEqual( [
+ {
+ id: 9,
+ title: 'Cats',
+ url: 'http://wordpress.local/category/cats/',
+ type: 'category',
+ },
+ {
+ id: 1,
+ title: 'Uncategorized',
+ url: 'http://wordpress.local/category/uncategorized/',
+ type: 'category',
+ },
+ ] )
+ );
+ } );
+ it( 'filters suggestions by post-format', () => {
+ return fetchLinkSuggestions( '', {
+ type: 'post-format',
+ } ).then( ( suggestions ) =>
+ expect( suggestions ).toEqual( [
+ {
+ id: 'gallery',
+ title: 'Gallery',
+ url: 'http://wordpress.local/type/gallery/',
+ type: 'post-format',
+ },
+ {
+ id: 'quote',
+ title: 'Quote',
+ url: 'http://wordpress.local/type/quote/',
+ type: 'post-format',
+ },
+ ] )
+ );
+ } );
+ it( 'filters does not return post-format suggestions when formats are not supported', () => {
+ return fetchLinkSuggestions(
+ '',
+ {
+ type: 'post-format',
+ },
+ { disablePostFormats: true }
+ ).then( ( suggestions ) => expect( suggestions ).toEqual( [] ) );
+ } );
+ it( 'returns suggestions from post, term, and post-format', () => {
+ return fetchLinkSuggestions( '', {} ).then( ( suggestions ) =>
+ expect( suggestions ).toEqual( [
+ {
+ id: 37,
+ title: 'Contact Page',
+ url: 'http://wordpress.local/contact-page/',
+ type: 'page',
+ },
+ {
+ id: 9,
+ title: 'Cats',
+ url: 'http://wordpress.local/category/cats/',
+ type: 'category',
+ },
+ {
+ id: 1,
+ title: 'Uncategorized',
+ url: 'http://wordpress.local/category/uncategorized/',
+ type: 'category',
+ },
+ {
+ id: 'gallery',
+ title: 'Gallery',
+ url: 'http://wordpress.local/type/gallery/',
+ type: 'post-format',
+ },
+ {
+ id: 'quote',
+ title: 'Quote',
+ url: 'http://wordpress.local/type/quote/',
+ type: 'post-format',
+ },
+ ] )
+ );
+ } );
+ it( 'initial search suggestions limits results', () => {
+ return fetchLinkSuggestions( '', {
+ type: 'post',
+ subtype: 'page',
+ isInitialSuggestions: true,
+ } ).then( ( suggestions ) =>
+ expect( suggestions ).toEqual( [
+ {
+ id: 11,
+ title: 'Limit Case',
+ url: 'http://wordpress.local/limit-case/',
+ type: 'page',
+ },
+ ] )
+ );
+ } );
+ it( 'allows searching from a page', () => {
+ return fetchLinkSuggestions( '', {
+ type: 'post',
+ subtype: 'page',
+ page: 11,
+ } ).then( ( suggestions ) =>
+ expect( suggestions ).toEqual( [
+ {
+ id: 22,
+ title: 'Page Case',
+ url: 'http://wordpress.local/page-case/',
+ type: 'page',
+ },
+ ] )
+ );
+ } );
+} );
diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js
index 7d7ab99194c1d2..5f2a2035afbd2e 100644
--- a/packages/core-data/src/index.js
+++ b/packages/core-data/src/index.js
@@ -78,3 +78,4 @@ register( store );
export { default as EntityProvider } from './entity-provider';
export * from './entity-provider';
+export * from './fetch';
diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js
index 8421751159f90f..54ecb2df73c889 100644
--- a/packages/edit-navigation/src/index.js
+++ b/packages/edit-navigation/src/index.js
@@ -6,11 +6,12 @@ import {
__experimentalRegisterExperimentalCoreBlocks,
} from '@wordpress/block-library';
import { render } from '@wordpress/element';
+import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data';
+
/**
* Internal dependencies
*/
import { addFilters } from './filters';
-import fetchLinkSuggestions from './utils/fetch-link-suggestions';
/**
* Internal dependencies
@@ -26,10 +27,8 @@ export function initialize( id, settings ) {
__experimentalRegisterExperimentalCoreBlocks();
}
- settings.__experimentalFetchLinkSuggestions = (
- searchText,
- searchOptions
- ) => fetchLinkSuggestions( searchText, searchOptions, settings );
+ settings.__experimentalFetchLinkSuggestions = ( search, searchOptions ) =>
+ fetchLinkSuggestions( search, searchOptions, settings );
render(
,
diff --git a/packages/edit-navigation/src/utils/fetch-link-suggestions.js b/packages/edit-navigation/src/utils/fetch-link-suggestions.js
deleted file mode 100644
index 94a4e1587b15d7..00000000000000
--- a/packages/edit-navigation/src/utils/fetch-link-suggestions.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * WordPress dependencies
- */
-import apiFetch from '@wordpress/api-fetch';
-import { addQueryArgs } from '@wordpress/url';
-/**
- * External dependencies
- */
-import { flatten, map } from 'lodash';
-import { decodeEntities } from '@wordpress/html-entities';
-import { __ } from '@wordpress/i18n';
-
-/**
- * Fetches link suggestions from the API. This function is an exact copy of a function found at:
- *
- * packages/editor/src/components/provider/index.js
- *
- * It seems like there is no suitable package to import this from. Ideally it would be either part of core-data.
- * Until we refactor it, just copying the code is the simplest solution.
- *
- * @param {string} search
- * @param {Object} [searchArguments]
- * @param {number} [searchArguments.isInitialSuggestions]
- * @param {number} [searchArguments.type]
- * @param {number} [searchArguments.subtype]
- * @param {Object} [editorSettings]
- * @param {boolean} [editorSettings.disablePostFormats=false]
- * @return {Promise