diff --git a/lib/compat/wordpress-6.0/blocks.php b/lib/compat/wordpress-6.0/blocks.php new file mode 100644 index 00000000000000..5b158cbc551a84 --- /dev/null +++ b/lib/compat/wordpress-6.0/blocks.php @@ -0,0 +1,94 @@ + 'post', + 'order' => 'DESC', + 'orderby' => 'date', + 'post__not_in' => array(), + ); + + if ( isset( $block->context['query'] ) ) { + if ( ! empty( $block->context['query']['postType'] ) ) { + $post_type_param = $block->context['query']['postType']; + if ( is_post_type_viewable( $post_type_param ) ) { + $query['post_type'] = $post_type_param; + } + } + if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { + $sticky = get_option( 'sticky_posts' ); + if ( 'only' === $block->context['query']['sticky'] ) { + $query['post__in'] = $sticky; + } else { + $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); + } + } + if ( ! empty( $block->context['query']['exclude'] ) ) { + $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); + $excluded_post_ids = array_filter( $excluded_post_ids ); + $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); + } + if ( + isset( $block->context['query']['perPage'] ) && + is_numeric( $block->context['query']['perPage'] ) + ) { + $per_page = absint( $block->context['query']['perPage'] ); + $offset = 0; + + if ( + isset( $block->context['query']['offset'] ) && + is_numeric( $block->context['query']['offset'] ) + ) { + $offset = absint( $block->context['query']['offset'] ); + } + + $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; + $query['posts_per_page'] = $per_page; + } + if ( ! empty( $block->context['query']['categoryIds'] ) ) { + $term_ids = array_map( 'intval', $block->context['query']['categoryIds'] ); + $term_ids = array_filter( $term_ids ); + $query['category__in'] = $term_ids; + } + if ( ! empty( $block->context['query']['tagIds'] ) ) { + $term_ids = array_map( 'intval', $block->context['query']['tagIds'] ); + $term_ids = array_filter( $term_ids ); + $query['tag__in'] = $term_ids; + } + if ( + isset( $block->context['query']['order'] ) && + in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) + ) { + $query['order'] = strtoupper( $block->context['query']['order'] ); + } + if ( isset( $block->context['query']['orderBy'] ) ) { + $query['orderby'] = $block->context['query']['orderBy']; + } + if ( ! empty( $block->context['query']['author'] ) ) { + $query['author'] = $block->context['query']['author']; + } + if ( ! empty( $block->context['query']['search'] ) ) { + $query['s'] = $block->context['query']['search']; + } + } + return $query; +} diff --git a/lib/load.php b/lib/load.php index f1cd5e3bf7692c..2b1b5d4839162a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -127,6 +127,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-5.9/rest-active-global-styles.php'; require __DIR__ . '/compat/wordpress-5.9/move-theme-editor-menu-item.php'; require __DIR__ . '/compat/wordpress-6.0/post-lock.php'; +require __DIR__ . '/compat/wordpress-6.0/blocks.php'; require __DIR__ . '/compat/experimental/blocks.php'; require __DIR__ . '/blocks.php'; diff --git a/packages/block-library/src/query/edit/inspector-controls/author-control.js b/packages/block-library/src/query/edit/inspector-controls/author-control.js new file mode 100644 index 00000000000000..aeb7a46506d6e3 --- /dev/null +++ b/packages/block-library/src/query/edit/inspector-controls/author-control.js @@ -0,0 +1,78 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { FormTokenField } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { getEntitiesInfo } from '../../utils'; + +const AUTHORS_QUERY = { + who: 'authors', + per_page: -1, + _fields: 'id,name', + context: 'view', +}; + +function AuthorControl( { value, onChange } ) { + const authorsList = useSelect( ( select ) => { + const { getUsers } = select( coreStore ); + return getUsers( AUTHORS_QUERY ); + }, [] ); + + if ( ! authorsList ) { + return null; + } + const authorsInfo = getEntitiesInfo( authorsList ); + /** + * We need to normalize the value because the block operates on a + * comma(`,`) separated string value and `FormTokenFiels` needs an + * array. + */ + const normalizedValue = ! value ? [] : value.toString().split( ',' ); + // Returns only the existing authors ids. This prevents the component + // from crashing in the editor, when non existing ids are provided. + const sanitizedValue = normalizedValue.reduce( + ( accumulator, authorId ) => { + const author = authorsInfo.mapById[ authorId ]; + if ( author ) { + accumulator.push( { + id: authorId, + value: author.name, + } ); + } + return accumulator; + }, + [] + ); + + const getIdByValue = ( entitiesMappedByName, authorValue ) => { + const id = authorValue?.id || entitiesMappedByName[ authorValue ]?.id; + if ( id ) return id; + }; + const onAuthorChange = ( newValue ) => { + const ids = Array.from( + newValue.reduce( ( accumulator, author ) => { + // Verify that new values point to existing entities. + const id = getIdByValue( authorsInfo.mapByName, author ); + if ( id ) accumulator.add( id ); + return accumulator; + }, new Set() ) + ); + onChange( { author: ids.join( ',' ) } ); + }; + return ( + + ); +} + +export default AuthorControl; diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 0b9c4bd4974176..4984434d42ac04 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -8,7 +8,6 @@ import { debounce } from 'lodash'; */ import { PanelBody, - QueryControls, TextControl, FormTokenField, SelectControl, @@ -26,7 +25,8 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import OrderControl from './order-control'; -import { getTermsInfo, usePostTypes } from '../../utils'; +import AuthorControl from './author-control'; +import { getEntitiesInfo, usePostTypes } from '../../utils'; import { MAX_FETCHED_TERMS } from '../../constants'; const stickyOptions = [ @@ -65,7 +65,7 @@ export default function QueryInspectorControls( { const { order, orderBy, - author: selectedAuthorId, + author: authorIds, postType, sticky, inherit, @@ -74,7 +74,7 @@ export default function QueryInspectorControls( { const [ showTags, setShowTags ] = useState( true ); const [ showSticky, setShowSticky ] = useState( postType === 'post' ); const { postTypesTaxonomiesMap, postTypesSelectOptions } = usePostTypes(); - const { authorList, categories, tags } = useSelect( ( select ) => { + const { categories, tags } = useSelect( ( select ) => { const { getEntityRecords } = select( coreStore ); const termsQuery = { per_page: MAX_FETCHED_TERMS }; const _categories = getEntityRecords( @@ -84,11 +84,8 @@ export default function QueryInspectorControls( { ); const _tags = getEntityRecords( 'taxonomy', 'post_tag', termsQuery ); return { - categories: getTermsInfo( _categories ), - tags: getTermsInfo( _tags ), - authorList: getEntityRecords( 'root', 'user', { - per_page: -1, - } ), + categories: getEntitiesInfo( _categories ), + tags: getEntitiesInfo( _tags ), }; }, [] ); useEffect( () => { @@ -237,7 +234,7 @@ export default function QueryInspectorControls( { { ! inherit && ( - { showCategories && categories?.terms?.length > 0 && ( + { showCategories && categories?.entities?.length > 0 && ( ) } - { showTags && tags?.terms?.length > 0 && ( + { showTags && tags?.entities?.length > 0 && ( ) } - - setQuery( { - author: value !== '' ? +value : undefined, - } ) - } - /> + { - describe( 'getTermsInfo', () => { + describe( 'getEntitiesInfo', () => { it( 'should return an empty object when no terms provided', () => { - expect( getTermsInfo() ).toEqual( { terms: undefined } ); + expect( getEntitiesInfo() ).toEqual( { + terms: undefined, + } ); } ); it( 'should return proper terms info object', () => { - expect( getTermsInfo( terms ) ).toEqual( + expect( getEntitiesInfo( terms ) ).toEqual( expect.objectContaining( { mapById: expect.objectContaining( { 4: expect.objectContaining( { name: 'nba' } ), diff --git a/packages/block-library/src/query/utils.js b/packages/block-library/src/query/utils.js index 6ff6e026e08b3b..f47921047d78cd 100644 --- a/packages/block-library/src/query/utils.js +++ b/packages/block-library/src/query/utils.js @@ -6,53 +6,43 @@ import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; /** - * WordPress term object from REST API. - * Categories ref: https://developer.wordpress.org/rest-api/reference/categories/ - * Tags ref: https://developer.wordpress.org/rest-api/reference/tags/ - * - * @typedef {Object} WPTerm - * @property {number} id Unique identifier for the term. - * @property {number} count Number of published posts for the term. - * @property {string} description HTML description of the term. - * @property {string} link URL of the term. - * @property {string} name HTML title for the term. - * @property {string} slug An alphanumeric identifier for the term unique to its type. - * @property {string} taxonomy Type attribution for the term. - * @property {Object} meta Meta fields - * @property {number} [parent] The parent term ID. + * @typedef IHasNameAndId + * @property {string|number} id The entity's id. + * @property {string} name The entity's name. */ /** * The object used in Query block that contains info and helper mappings - * from an array of WPTerm. + * from an array of IHasNameAndId objects. * - * @typedef {Object} QueryTermsInfo - * @property {WPTerm[]} terms The array of terms. - * @property {Object} mapById Object mapping with the term id as key and the term as value. - * @property {Object} mapByName Object mapping with the term name as key and the term as value. - * @property {string[]} names Array with the terms' names. + * @typedef {Object} QueryEntitiesInfo + * @property {IHasNameAndId[]} entities The array of entities. + * @property {Object} mapById Object mapping with the id as key and the entity as value. + * @property {Object} mapByName Object mapping with the name as key and the entity as value. + * @property {string[]} names Array with the entities' names. */ /** - * Returns a helper object with mapping from WPTerms. + * Returns a helper object with mapping from Objects that implement + * the `IHasNameAndId` interface. The returned object is used for + * integration with `FormTokenField` component. * - * @param {WPTerm[]} terms The terms to extract of helper object. - * @return {QueryTermsInfo} The object with the terms information. + * @param {IHasNameAndId[]} entities The entities to extract of helper object. + * @return {QueryEntitiesInfo} The object with the entities information. */ -export const getTermsInfo = ( terms ) => { - const mapping = terms?.reduce( - ( accumulator, term ) => { +export const getEntitiesInfo = ( entities ) => { + const mapping = entities?.reduce( + ( accumulator, entity ) => { const { mapById, mapByName, names } = accumulator; - mapById[ term.id ] = term; - mapByName[ term.name ] = term; - names.push( term.name ); + mapById[ entity.id ] = entity; + mapByName[ entity.name ] = entity; + names.push( entity.name ); return accumulator; }, { mapById: {}, mapByName: {}, names: [] } ); - return { - terms, + entities, ...mapping, }; }; diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js index 136ee4b08b8369..11d4635af96324 100644 --- a/tools/webpack/blocks.js +++ b/tools/webpack/blocks.js @@ -22,6 +22,13 @@ const { baseConfig, plugins, stylesTransform } = require( './shared' ); */ const blockNameRegex = new RegExp( /(?<=build-module\/).*(?=(\/view))/g ); +/** + * We need to automatically rename some functions when they are called inside block files, + * but have been declared elsewhere. This way we can call Gutenberg override functions, but + * the block will still call the core function when updates are back ported. + */ +const prefixFunctions = [ 'build_query_vars_from_query_block' ]; + const createEntrypoints = () => { /* * Returns an array of paths to view.js files within the `@wordpress/block-library` package. @@ -109,8 +116,19 @@ module.exports = { return join( to, `${ dirname }.php` ); }, transform: ( content ) => { + const prefix = 'gutenberg_'; content = content.toString(); + // Within content, search and prefix any function calls from + // `prefixFunctions` list. This is needed because some functions + // are called inside block files, but have been declared elsewhere. + // So with the rename we can call Gutenberg override functions, but the + // block will still call the core function when updates are back ported. + content = content.replace( + new RegExp( prefixFunctions.join( '|' ), 'g' ), + ( match ) => `${ prefix }${ match }` + ); + // Within content, search for any function definitions. For // each, replace every other reference to it in the file. return ( @@ -125,7 +143,7 @@ module.exports = { return result.replace( new RegExp( functionName, 'g' ), ( match ) => - 'gutenberg_' + + prefix + match.replace( /^wp_/, '' ) ); }, content )