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} List of suggestions - */ - -export default function fetchLinkSuggestions( - search, - { isInitialSuggestions, type, subtype } = {}, - { disablePostFormats = false } = {} -) { - const perPage = isInitialSuggestions ? 3 : 20; - - const linkTypes = [ 'post', 'term', 'post-format' ]; - - linkTypes.forEach( ( linkType ) => { - if ( ! type || type === linkType ) { - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - search, - per_page: perPage, - type: 'post', - subtype, - } ), - } ).catch( () => [] ); // fail by returning no results - } - } ); - const queries = []; - - if ( ! type || type === 'post' ) { - queries.push( - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - search, - 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, - per_page: perPage, - type: 'term', - subtype, - } ), - } ).catch( () => [] ) - ); - } - - if ( ! disablePostFormats && ( ! type || type === 'post-format' ) ) { - queries.push( - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - search, - per_page: perPage, - type: 'post-format', - subtype, - } ), - } ).catch( () => [] ) - ); - } - - return Promise.all( queries ).then( ( results ) => { - return map( flatten( results ).slice( 0, perPage ), ( result ) => ( { - id: result.id, - url: result.url, - title: decodeEntities( result.title ) || __( '(no title)' ), - type: result.subtype || result.type, - } ) ); - } ); -} diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 29ad1c6a801d99..fe42ad69af26c6 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -1,14 +1,12 @@ /** * WordPress dependencies */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; -import { __ } from '@wordpress/i18n'; import { registerCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; import { render } from '@wordpress/element'; +import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data'; /** * Internal dependencies @@ -18,32 +16,6 @@ import './hooks'; import './store'; import Editor from './components/editor'; -const fetchLinkSuggestions = ( search, { perPage = 20 } = {} ) => - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - per_page: perPage, - search, - type: 'post', - subtype: 'post', - } ), - } ) - .then( ( posts ) => - Promise.all( - posts.map( ( post ) => - apiFetch( { url: post._links.self[ 0 ].href } ) - ) - ) - ) - .then( ( posts ) => - posts.map( ( post ) => ( { - url: post.link, - type: post.type, - id: post.id, - slug: post.slug, - title: post.title.rendered || __( '(no title)' ), - } ) ) - ); - /** * Initializes the site editor screen. * @@ -51,7 +23,8 @@ const fetchLinkSuggestions = ( search, { perPage = 20 } = {} ) => * @param {Object} settings Editor settings. */ export function initialize( id, settings ) { - settings.__experimentalFetchLinkSuggestions = fetchLinkSuggestions; + settings.__experimentalFetchLinkSuggestions = ( search, searchOptions ) => + fetchLinkSuggestions( search, searchOptions, settings ); settings.__experimentalSpotlightEntityBlocks = [ 'core/template-part' ]; registerCoreBlocks(); diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 57ffb98bde4f54..521c997ec791b3 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -11,6 +11,7 @@ import { __experimentalGetCoreBlocks, __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; +import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data'; /** * Internal dependencies @@ -39,6 +40,8 @@ export function initialize( id, settings ) { } registerLegacyWidgetVariations( settings ); registerBlock( widgetArea ); + settings.__experimentalFetchLinkSuggestions = ( search, searchOptions ) => + fetchLinkSuggestions( search, searchOptions, settings ); render( , document.getElementById( id ) 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 6638f091994172..64fb570b5d945c 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -1,18 +1,17 @@ /** * External dependencies */ -import { map, pick, defaultTo, flatten, partialRight } from 'lodash'; +import { pick, defaultTo } from 'lodash'; /** * WordPress dependencies */ import { Platform, useMemo } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; -import { store as coreStore } from '@wordpress/core-data'; -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; -import { decodeEntities } from '@wordpress/html-entities'; +import { + store as coreStore, + __experimentalFetchLinkSuggestions as fetchLinkSuggestions, +} from '@wordpress/core-data'; /** * Internal dependencies @@ -20,91 +19,6 @@ import { decodeEntities } from '@wordpress/html-entities'; import { mediaUpload } from '../../utils'; import { store as editorStore } from '../../store'; -/** - * Fetches link suggestions from the API. This function is an exact copy of a function found at: - * - * packages/edit-navigation/src/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 {number} [searchArguments.page] - * @param {Object} [editorSettings] - * @param {boolean} [editorSettings.disablePostFormats=false] - * @return {Promise} List of suggestions - */ - -const fetchLinkSuggestions = async ( - search, - { isInitialSuggestions, type, subtype, page, perPage: perPageArg } = {}, - { disablePostFormats = false } = {} -) => { - const perPage = perPageArg || isInitialSuggestions ? 3 : 20; - - 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 map( - flatten( results ) - .filter( ( result ) => !! result.id ) - .slice( 0, perPage ), - ( result ) => ( { - id: result.id, - url: result.url, - title: decodeEntities( result.title ) || __( '(no title)' ), - type: result.subtype || result.type, - } ) - ); - } ); -}; - /** * React hook used to compute the block editor settings to use for the post editor. * @@ -191,10 +105,8 @@ function useBlockEditorSettings( settings, hasTemplate ) { ] ), mediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalReusableBlocks: reusableBlocks, - __experimentalFetchLinkSuggestions: partialRight( - fetchLinkSuggestions, - settings - ), + __experimentalFetchLinkSuggestions: ( search, searchOptions ) => + fetchLinkSuggestions( search, searchOptions, settings ), __experimentalCanUserUseUnfilteredHTML: canUseUnfilteredHTML, __experimentalUndo: undo, __experimentalShouldInsertAtTheTop: isTitleSelected,