Skip to content

Commit

Permalink
[Block Library - Query Loop]: Add parents filter
Browse files Browse the repository at this point in the history
  • Loading branch information
ntsekouras committed May 9, 2022
1 parent 709aabc commit dde8d1d
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 2 deletions.
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 @@ -23,6 +23,7 @@ import { useEffect, useState, useCallback } from '@wordpress/element';
*/
import OrderControl from './order-control';
import AuthorControl from './author-control';
import ParentControl from './parent-control';
import TaxonomyControls from './taxonomy-controls';
import { usePostTypes } from '../../utils';

Expand All @@ -45,6 +46,7 @@ export default function QueryInspectorControls( {
sticky,
inherit,
taxQuery,
parents,
} = query;
const [ showSticky, setShowSticky ] = useState( postType === 'post' );
const { postTypesTaxonomiesMap, postTypesSelectOptions } = usePostTypes();
Expand Down Expand Up @@ -156,6 +158,11 @@ export default function QueryInspectorControls( {
value={ querySearch }
onChange={ setQuerySearch }
/>
<ParentControl
parents={ parents }
postType={ postType }
onChange={ setQuery }
/>
</PanelBody>
) }
</InspectorControls>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* 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';

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

const EMPTY_ARRAY = [];
const SUGGESTIONS_QUERY = {
per_page: -1,
order: 'asc',
orderby: 'title',
_fields: 'id,title',
context: 'view',
};

function ParentControl( { parents, postType, onChange } ) {
const isHierarchical = useIsPostTypeHierarchical( postType );
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,
{
...SUGGESTIONS_QUERY,
search,
exclude: parents,
},
];
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, {
...SUGGESTIONS_QUERY,
include: parents,
} );
},
[ 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?.join(), searchHasResolved ] );

// Parent control is only needed for hierarchical post types.
if ( ! isHierarchical ) {
return null;
}

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

0 comments on commit dde8d1d

Please sign in to comment.