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 )