Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Block Library - Query Loop]: Add parents filter #40933

Merged
merged 5 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/compat/wordpress-6.0/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ function gutenberg_build_query_vars_from_query_block( $block, $page ) {
if ( ! empty( $block->context['query']['search'] ) ) {
$query['s'] = $block->context['query']['search'];
}
if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) {
$query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) );
}
}
return $query;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/block-library/src/post-template/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default function PostTemplateEdit( {
sticky,
inherit,
taxQuery,
parents,
} = {},
queryContext = [ { page: 1 } ],
templateSlug,
Expand Down Expand Up @@ -138,6 +139,9 @@ export default function PostTemplateEdit( {
if ( exclude?.length ) {
query.exclude = exclude;
}
if ( parents?.length ) {
query.parent = parents;
}
// If sticky is not set, it will return all posts in the results.
// If sticky is set to `only`, it will limit the results to sticky posts only.
// If it is anything else, it will exclude sticky posts from results. For the record the value stored is `exclude`.
Expand Down Expand Up @@ -172,6 +176,7 @@ export default function PostTemplateEdit( {
inherit,
templateSlug,
taxQuery,
parents,
]
);
const blockContexts = useMemo(
Expand Down
3 changes: 2 additions & 1 deletion packages/block-library/src/query/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"exclude": [],
"sticky": "",
"inherit": true,
"taxQuery": null
"taxQuery": null,
"parents": []
}
},
"tagName": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,28 @@ import {
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import { useEffect, useState, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import OrderControl from './order-control';
import AuthorControl from './author-control';
import ParentControl from './parent-control';
import TaxonomyControls from './taxonomy-controls';
import { usePostTypes } from '../../utils';

function useIsPostTypeHierarchical( postType ) {
return useSelect(
( select ) => {
const type = select( coreStore ).getPostType( postType );
return type?.viewable && type?.hierarchical;
},
[ postType ]
);
}

const stickyOptions = [
{ label: __( 'Include' ), value: '' },
{ label: __( 'Exclude' ), value: 'exclude' },
Expand All @@ -45,9 +58,11 @@ export default function QueryInspectorControls( {
sticky,
inherit,
taxQuery,
parents,
} = query;
const [ showSticky, setShowSticky ] = useState( postType === 'post' );
const { postTypesTaxonomiesMap, postTypesSelectOptions } = usePostTypes();
const isPostTypeHierarchical = useIsPostTypeHierarchical( postType );
useEffect( () => {
setShowSticky( postType === 'post' );
}, [ postType ] );
Expand All @@ -72,6 +87,8 @@ export default function QueryInspectorControls( {
if ( newValue !== 'post' ) {
updateQuery.sticky = '';
}
// We need to reset `parents` because they are tied to each post type.
updateQuery.parents = [];
setQuery( updateQuery );
};
const [ querySearch, setQuerySearch ] = useState( query.search );
Expand Down Expand Up @@ -156,6 +173,13 @@ export default function QueryInspectorControls( {
value={ querySearch }
onChange={ setQuerySearch }
/>
{ isPostTypeHierarchical && (
<ParentControl
parents={ parents }
postType={ postType }
onChange={ setQuery }
/>
) }
</PanelBody>
) }
</InspectorControls>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { FormTokenField } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useState, useEffect, useMemo } from '@wordpress/element';
import { useDebounce } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { getEntitiesInfo, mapToIHasNameAndId } from '../../utils';

const EMPTY_ARRAY = [];
const BASE_QUERY = {
order: 'asc',
_fields: 'id,title',
context: 'view',
};

function ParentControl( { parents, postType, onChange } ) {
const [ search, setSearch ] = useState( '' );
const [ value, setValue ] = useState( EMPTY_ARRAY );
const [ suggestions, setSuggestions ] = useState( EMPTY_ARRAY );
const debouncedSearch = useDebounce( setSearch, 250 );
const { searchResults, searchHasResolved } = useSelect(
( select ) => {
if ( ! search ) {
return { searchResults: EMPTY_ARRAY, searchHasResolved: true };
}
const { getEntityRecords, hasFinishedResolution } = select(
coreStore
);
const selectorArgs = [
'postType',
postType,
{
...BASE_QUERY,
search,
orderby: 'relevance',
exclude: parents,
per_page: 20,
},
];
return {
searchResults: getEntityRecords( ...selectorArgs ),
searchHasResolved: hasFinishedResolution(
'getEntityRecords',
selectorArgs
),
};
},
[ search, parents ]
);
const currentParents = useSelect(
( select ) => {
if ( ! parents?.length ) return EMPTY_ARRAY;
const { getEntityRecords } = select( coreStore );
return getEntityRecords( 'postType', postType, {
...BASE_QUERY,
include: parents,
Mamaduka marked this conversation as resolved.
Show resolved Hide resolved
per_page: parents.length,
} );
},
[ parents ]
);
// Update the `value` state only after the selectors are resolved
// to avoid emptying the input when we're changing parents.
useEffect( () => {
if ( ! parents?.length ) {
setValue( EMPTY_ARRAY );
}
if ( ! currentParents?.length ) return;
const currentParentsInfo = getEntitiesInfo(
mapToIHasNameAndId( currentParents, 'title.rendered' )
);
// Returns only the existing entity ids. This prevents the component
// from crashing in the editor, when non existing ids are provided.
const sanitizedValue = parents.reduce( ( accumulator, id ) => {
const entity = currentParentsInfo.mapById[ id ];
if ( entity ) {
accumulator.push( {
id,
value: entity.name,
} );
}
return accumulator;
}, [] );
setValue( sanitizedValue );
}, [ parents, currentParents ] );

const entitiesInfo = useMemo( () => {
if ( ! searchResults?.length ) return EMPTY_ARRAY;
return getEntitiesInfo(
mapToIHasNameAndId( searchResults, 'title.rendered' )
);
}, [ searchResults ] );
// Update suggestions only when the query has resolved.
useEffect( () => {
if ( ! searchHasResolved ) return;
setSuggestions( entitiesInfo.names );
}, [ entitiesInfo.names, searchHasResolved ] );

const getIdByValue = ( entitiesMappedByName, entity ) => {
const id = entity?.id || entitiesMappedByName?.[ entity ]?.id;
if ( id ) return id;
};
const onParentChange = ( newValue ) => {
const ids = Array.from(
newValue.reduce( ( accumulator, entity ) => {
// Verify that new values point to existing entities.
const id = getIdByValue( entitiesInfo.mapByName, entity );
if ( id ) accumulator.add( id );
return accumulator;
}, new Set() )
);
setSuggestions( EMPTY_ARRAY );
onChange( { parents: ids } );
};
return (
<FormTokenField
label={ __( 'Parents' ) }
value={ value }
onInputChange={ debouncedSearch }
suggestions={ suggestions }
onChange={ onParentChange }
/>
);
}

export default ParentControl;
22 changes: 22 additions & 0 deletions packages/block-library/src/query/utils.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';

/**
* @typedef IHasNameAndId
Expand Down Expand Up @@ -47,6 +53,22 @@ export const getEntitiesInfo = ( entities ) => {
};
};

/**
* Helper util to map records to add a `name` prop from a
* provided path, in order to handle all entities in the same
* fashion(implementing`IHasNameAndId` interface).
*
* @param {Object[]} entities The array of entities.
* @param {string} path The path to map a `name` property from the entity.
* @return {IHasNameAndId[]} An array of enitities that now implement the `IHasNameAndId` interface.
*/
export const mapToIHasNameAndId = ( entities, path ) => {
return ( entities || [] ).map( ( entity ) => ( {
...entity,
name: decodeEntities( get( entity, path ) ),
} ) );
};

/**
* Returns a helper object that contains:
* 1. An `options` object from the available post types, to be passed to a `SelectControl`.
Expand Down
3 changes: 2 additions & 1 deletion test/integration/fixtures/blocks/core__query.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"exclude": [],
"sticky": "",
"inherit": true,
"taxQuery": null
"taxQuery": null,
"parents": []
},
"tagName": "div",
"displayLayout": {
Expand Down