From fadaa6cf7eddb04a621a3b70b8b20457a39c249e Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Fri, 7 Feb 2020 14:02:03 -0500 Subject: [PATCH 1/9] Create Query Block The Query Block allows users to create a search on the site and choose blocks to display content from the matching posts. This allows users to highlight specific content or areas of their sites. --- lib/blocks.php | 1 + packages/block-library/src/editor.scss | 1 + packages/block-library/src/index.js | 8 + packages/block-library/src/query/block.json | 4 + packages/block-library/src/query/edit.js | 262 ++++++++++++++++++ packages/block-library/src/query/editor.scss | 5 + packages/block-library/src/query/index.js | 72 +++++ packages/block-library/src/query/index.php | 123 ++++++++ .../block-library/src/query/query-panel.js | 214 ++++++++++++++ packages/block-library/src/query/store.js | 161 +++++++++++ 10 files changed, 851 insertions(+) create mode 100644 packages/block-library/src/query/block.json create mode 100644 packages/block-library/src/query/edit.js create mode 100644 packages/block-library/src/query/editor.scss create mode 100644 packages/block-library/src/query/index.js create mode 100644 packages/block-library/src/query/index.php create mode 100644 packages/block-library/src/query/query-panel.js create mode 100644 packages/block-library/src/query/store.js diff --git a/lib/blocks.php b/lib/blocks.php index c751656881501..0f803aed65e5a 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -42,6 +42,7 @@ function gutenberg_reregister_core_block_types() { 'post-excerpt.php' => 'core/post-excerpt', 'post-featured-image.php' => 'core/post-featured-image', 'post-tags.php' => 'core/post-tags', + 'query.php' => 'core/query', ); $registry = WP_Block_Type_Registry::get_instance(); diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 6dc247b6d744a..575d6f551434e 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -30,6 +30,7 @@ @import "./paragraph/editor.scss"; @import "./post-excerpt/editor.scss"; @import "./pullquote/editor.scss"; +@import "./query/editor.scss"; @import "./quote/editor.scss"; @import "./rss/editor.scss"; @import "./search/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 206c705651e9a..32c53cfe1086b 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -75,6 +75,8 @@ import * as postDate from './post-date'; import * as postExcerpt from './post-excerpt'; import * as postFeaturedImage from './post-featured-image'; import * as postTags from './post-tags'; +import * as query from './query'; +import { registerQueryStore, registerDeduplicatedBlock } from './query/store'; /** * Function to register an individual block. @@ -207,8 +209,14 @@ export const __experimentalRegisterExperimentalCoreBlocks = postExcerpt, postFeaturedImage, postTags, + query, ] : [] ), ].forEach( registerBlock ); + + if ( __experimentalEnableFullSiteEditing ) { + registerQueryStore(); + registerDeduplicatedBlock( `core/${ query.name }` ); + } } : undefined; diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json new file mode 100644 index 0000000000000..55b19a44b8d6f --- /dev/null +++ b/packages/block-library/src/query/block.json @@ -0,0 +1,4 @@ +{ + "name": "core/query", + "category": "layout" +} diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js new file mode 100644 index 0000000000000..8971b6b514d53 --- /dev/null +++ b/packages/block-library/src/query/edit.js @@ -0,0 +1,262 @@ +/** + * Internal dependencies + */ +import QueryPanel from './query-panel'; +import { STORE_NAMESPACE } from './store'; + +/** + * External dependencies + */ +import classNames from 'classnames'; +import { debounce, isUndefined, pickBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component, Fragment } from '@wordpress/element'; +import { + BlockList, + BlockEditorProvider, + InspectorControls, + WritingFlow, +} from '@wordpress/block-editor'; +import { cloneBlock, createBlock } from '@wordpress/blocks'; +import { PanelBody, Placeholder, Spinner } from '@wordpress/components'; +import { compose } from '@wordpress/compose'; +import { EntityProvider } from '@wordpress/core-data'; +import { withSelect, withDispatch } from '@wordpress/data'; + +const defaultFields = [ + 'core/post-title', + 'core/post-date', + 'core/post-author', + 'core/post-excerpt', +]; + +class Edit extends Component { + constructor( props ) { + super( props ); + this.state = { + editingPost: null, + blocksTree: {}, + }; + + this.debouncedCreateBlockTree = debounce( + this.createBlockTree.bind( this ), + 1000 + ); + } + + componentDidMount() { + this.createBlockTree(); + this.updateBlocks( defaultFields.map( ( f ) => createBlock( f ) ) ); + } + + componentDidUpdate( prevProps ) { + const { query } = this.props; + if ( prevProps.query !== query ) { + this.createBlockTree(); + } + } + + createBlockTree() { + const { editingPost, blocksTree } = this.state; + const { attributes, query } = this.props; + const { blocks } = attributes; + const newBlocksTree = ( query || [] ).reduce( + ( accumulator, post ) => ( { + ...accumulator, + [ post.id ]: + post.id === editingPost + ? blocksTree[ post.id ] + : blocks.map( ( block ) => + cloneBlock( block, { post } ) + ), + } ), + {} + ); + this.setState( { blocksTree: newBlocksTree } ); + } + + cleanBlock( block ) { + const { name, isValid, attributes, innerBlocks } = block; + return { + name, + attributes: { ...attributes, post: {} }, + innerBlocks: innerBlocks.map( ( b ) => this.cleanBlock( b ) ), + isValid, + }; + } + + updateBlocks( blocks, postId ) { + const { setAttributes } = this.props; + const { blocksTree } = this.state; + const cleanBlocks = blocks.map( this.cleanBlock ); + this.setState( + { + blocksTree: { ...( blocksTree || [] ), [ postId ]: blocks }, + editingPost: postId, + }, + () => { + setAttributes( { blocks: cleanBlocks } ); + this.debouncedCreateBlockTree(); + } + ); + } + + render() { + const { + attributes, + className, + query, + setAttributes, + clientId, + postList, + markPostsAsDisplayed, + } = this.props; + + const { criteria } = attributes; + + const { editingPost, blocksTree } = this.state; + const settings = {}; + const classes = classNames( + className, + editingPost ? 'is-editing' : '' + ); + markPostsAsDisplayed( clientId, query ); + + return ( +
+ + + + setAttributes( { criteria: newCriteria } ) + } + /> + + + + { ! query && ( + + + + ) } + { query && ! query.length && ( + + { __( + 'Sorry, no posts were found.', + 'newspack-blocks' + ) } + + ) } + { query && + !! query.length && + query.map( ( post ) => { + if ( ! blocksTree[ post.id ] ) return null; + return ( +
+ + + this.updateBlocks( + blocks, + post.id + ) + } + settings={ settings } + > + + + + + +
+ ); + } ) } +
+
+ ); + } +} + +const isSpecificPostModeActive = ( { specificMode, specificPosts } ) => + specificMode && specificPosts && specificPosts.length; + +const queryCriteriaFromAttributes = ( criteria ) => { + const { + per_page: perPage, + authors, + categories, + tags, + specificPosts, + } = criteria; + const queryCriteria = pickBy( + isSpecificPostModeActive( criteria ) + ? { + include: specificPosts, + orderby: 'include', + per_page: specificPosts.length, + } + : { + per_page: perPage, + categories, + author: authors, + tags, + }, + ( value ) => ! isUndefined( value ) + ); + return queryCriteria; +}; + +export default compose( + withSelect( ( select, props ) => { + const { attributes, clientId } = props; + const { criteria } = attributes; + const queryCriteria = queryCriteriaFromAttributes( criteria ); + + if ( ! isSpecificPostModeActive( criteria ) ) { + const postIdsToExclude = select( STORE_NAMESPACE ).previousPostIds( + clientId + ); + queryCriteria.exclude = postIdsToExclude.join( ',' ); + } + + return { + query: select( 'core' ).getEntityRecords( + 'postType', + 'post', + queryCriteria + ), + }; + } ), + withDispatch( ( dispatch, props ) => { + const { attributes } = props; + const { criteria } = attributes; + const markPostsAsDisplayed = isSpecificPostModeActive( criteria ) + ? dispatch( STORE_NAMESPACE ).markSpecificPostsAsDisplayed + : dispatch( STORE_NAMESPACE ).markPostsAsDisplayed; + + return { + markPostsAsDisplayed, + }; + } ) +)( Edit ); diff --git a/packages/block-library/src/query/editor.scss b/packages/block-library/src/query/editor.scss new file mode 100644 index 0000000000000..70a66a126c535 --- /dev/null +++ b/packages/block-library/src/query/editor.scss @@ -0,0 +1,5 @@ +.wp-block-query { + .block-editor-writing-flow__click-redirect { + display: none; + } +} diff --git a/packages/block-library/src/query/index.js b/packages/block-library/src/query/index.js new file mode 100644 index 0000000000000..4a469dc5e5974 --- /dev/null +++ b/packages/block-library/src/query/index.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { InnerBlocks } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const title = __( 'Query' ); + +/* From https://material.io/tools/icons */ +export const icon = ( + + + + +); + +export const settings = { + title, + icon, + category: 'layout', + keywords: [], + description: __( 'A collection of posts.' ), + attributes: { + className: { + type: 'string', + }, + criteria: { + type: 'object', + default: { + per_page: 3, + offset: 0, + tags: [], + categories: [], + author: [], + specificPosts: [], + }, + }, + blocks: { + type: 'array', + default: [ + { + isValid: true, + clientId: null, + name: 'post-title', + attributes: {}, + innerBlocks: [], + }, + ], + }, + }, + supports: { + html: false, + align: false, + }, + edit, + save: () => , +}; diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php new file mode 100644 index 0000000000000..fbde090e7fdf0 --- /dev/null +++ b/packages/block-library/src/query/index.php @@ -0,0 +1,123 @@ + array( + 'className' => array( + 'type' => 'string', + ), + 'criteria' => array( + 'type' => 'object', + 'default' => array( + 'perPage' => 3, + ), + ), + ), + 'render_callback' => 'render_block_core_query', + ) + ); +} + +/** + * Convert criteria object into args ready for use in WP_Query + * + * @param array $criteria A criteria object. + * + * @return array Return an array of args. + */ +function core_query_attributes_to_critera( $criteria ) { + if ( $criteria[ 'specificMode' ] == 1 ) { + $args = array( + 'post_status' => 'publish', + 'p' => $criteria[ 'singleId' ], + ); + return $args; + } + + $args = array( + 'posts_per_page' => ! empty( $criteria['per_page'] ) ? intval( $criteria['per_page'] ) : 3, + 'post_status' => 'publish', + 'suppress_filters' => false, + 'ignore_sticky_posts' => true, + ); + + if ( ! empty( $criteria['author'] ) ) { + $args['author'] = implode( ",", $criteria['author'] ); + } + if ( ! empty( $criteria['categories'] ) ) { + $args['cat'] = implode( ",", $criteria['categories'] ); + } + if ( ! empty( $criteria['tags'] ) ) { + $args['tag_in'] = intval( $criteria['tags'] ); + } + if ( ! empty( $criteria['search'] ) ) { + $args['s'] = sanitize_text_field( $criteria['search'] ); + } + return $args; +} + +function render_block_core_query( $attributes ) { + $blocks = ! empty( $attributes['blocks'] ) ? $attributes['blocks'] : array(); + $args = core_query_attributes_to_critera( $attributes['criteria'] ); + $args['posts_per_page'] = $args['posts_per_page'] + count( Blocks_Query::$displayedPostIds ); + + $query = new WP_Query( $args ); + + ob_start(); + ?> +
+ have_posts() ) : ?> + have_posts() ) : ?> + the_post(); + $id = get_the_ID(); + if ( in_array( $id, Blocks_Query::$displayedPostIds ) ) { + continue; + } else { + array_push( Blocks_Query::$displayedPostIds, $id ); + } + ?> +
+ $block['name'], + 'attrs' => $block['attributes'], + 'innerContent' => array(), + ); + + $allowed_html = wp_kses_allowed_html( 'post' ); + $allowed_html['time'] = array( + 'class' => true, + 'datetime' => true, + ); + + echo wp_kses( render_block( $block_data ), $allowed_html ); + } + ?> +
+ + +
+ { + const { + entityKind, + entityName, + entityToString, + onChange, + setState, + value, + input, + } = props; + + let currentValues = []; + if ( value.length ) { + const serverValues = + select( 'core' ).getEntityRecords( entityKind, entityName, { + per_page: value.length, + include: value.join( ',' ), + } ) || []; + currentValues = value + .map( ( id ) => { + const entity = serverValues.find( ( e ) => e.id === id ); + return entity + ? { value: entityToString( entity ), id: entity.id } + : undefined; + } ) + .filter( ( v ) => !! v ); + } + + const rawSuggestions = + select( 'core' ).getEntityRecords( entityKind, entityName, { + per_page: 100, + search: input, + exclude: value.join( ',' ), + } ) || []; + + const updateValue = ( entityValues ) => { + if ( onChange ) { + const entityIds = entityValues + .map( ( newValue ) => + typeof newValue === 'object' + ? newValue.id + : rawSuggestions.find( + ( entity ) => + entityToString( entity ) === newValue + )?.id + ) + .filter( ( catId ) => typeof catId !== 'undefined' ); + onChange( entityIds ); + } + }; + + return { + ...props, + value: currentValues, + suggestions: rawSuggestions.map( ( c ) => entityToString( c ) ), + onChange: updateValue, + onInputChange: ( newInput ) => setState( { input: newInput } ), + }; + } ) +)( FormTokenField ); + +export default class QueryPanel extends Component { + updateCriteria( newCriteria ) { + const { criteria, onChange } = this.props; + const { + per_page: perPage, + offset, + categories, + tags, + search, + author, + specificMode, + specificPosts, + } = { + ...criteria, + ...newCriteria, + }; + + const sanitizedCriteria = { + per_page: parseInt( perPage ), + offset: parseInt( offset ), + specificMode: !! specificMode, + specificPosts, + }; + + if ( author ) { + sanitizedCriteria.author = author.map( ( n ) => parseInt( n ) ); + } + + if ( categories ) { + sanitizedCriteria.categories = categories.map( ( n ) => + parseInt( n ) + ); + } + + if ( tags ) { + sanitizedCriteria.tags = tags.map( ( n ) => parseInt( n ) ); + } + + if ( search && search.trim().length > 0 ) { + sanitizedCriteria.search = search; + } + + return onChange( sanitizedCriteria ); + } + + render() { + const { criteria } = this.props; + const { + author, + per_page: perPage, + specificMode, + specificPosts, + categories, + tags, + } = criteria; + + return ( + + + this.updateCriteria( { specificMode: newValue } ) + } + label={ __( 'Choose Specific Posts', 'newspack-blocks' ) } + /> + { specificMode && ( + e.title.rendered } + label="Entity Posts" + onChange={ ( newValue ) => + this.updateCriteria( { specificPosts: newValue } ) + } + value={ specificPosts } + /> + ) } + { ! specificMode && ( + + + this.updateCriteria( { per_page: newValue } ) + } + /> + e.name } + label={ __( 'Author', 'newspack-blocks' ) } + onChange={ ( newValue ) => + this.updateCriteria( { author: newValue } ) + } + value={ author } + /> + e.name } + label={ __( 'Category', 'newspack-blocks' ) } + onChange={ ( newValue ) => + this.updateCriteria( { categories: newValue } ) + } + value={ categories } + /> + e.name } + label={ __( 'tags', 'newspack-blocks' ) } + onChange={ ( newVolue ) => + this.updateCriteria( { tags: newVolue } ) + } + value={ tags } + /> + + ) } + + ); + } +} + +QueryPanel.defaultProps = { + criteria: { + per_page: 3, + offset: 0, + specificMode: false, + specificPosts: [], + author: [], + categories: [], + tags: [], + search: '', + }, + onChange: () => null, +}; diff --git a/packages/block-library/src/query/store.js b/packages/block-library/src/query/store.js new file mode 100644 index 0000000000000..75574ababbeea --- /dev/null +++ b/packages/block-library/src/query/store.js @@ -0,0 +1,161 @@ +/** + * External dependencies + */ +import { uniq } from 'lodash'; + +/** + * WordPress dependencies + */ +import { registerStore, select, subscribe, dispatch } from '@wordpress/data'; + +export const STORE_NAMESPACE = 'core/post-deduplication'; + +const blockNames = new Set(); + +/** + * Add a block to be watched for deduplication + * + * @param {string} blockName + */ +export const registerDeduplicatedBlock = ( blockName ) => + blockNames.add( blockName ); + +const initialState = { + queryBlocks: [], // list of Query blocks in the order they are on the page + postsByBlock: {}, // map of returned posts to block clientId + specificPostsByBlock: {}, // posts displayed by specific-mode, which always return in the selector +}; + +const UPDATE_BLOCKS = 'UPDATE_BLOCKS'; +const MARK_POSTS_DISPLAYED = 'MARK_POSTS_DISPLAYED'; +const MARK_SPECIFIC_POSTS_DISPLAYED = 'MARK_SPECIFIC_POSTS_DISPLAYED'; + +const actions = { + updateBlocks( blocks ) { + return { + type: UPDATE_BLOCKS, + blocks, + }; + }, + markPostsAsDisplayed( clientId, posts ) { + return { + type: MARK_POSTS_DISPLAYED, + clientId, + posts, + }; + }, + markSpecificPostsAsDisplayed( clientId, posts ) { + return { + type: MARK_SPECIFIC_POSTS_DISPLAYED, + clientId, + posts, + }; + }, +}; + +/** + * Returns the Query blocks that appear before the current one on the page + * + * @param {Array} orderedBlocks An array of Block objects in the order on the page + * @param {string} clientId Client ID of the Block to find blocks before + */ +const blocksBefore = ( orderedBlocks, clientId ) => { + const ourBlockIdx = orderedBlocks.findIndex( + ( b ) => b.clientId === clientId + ); + return orderedBlocks.slice( 0, ourBlockIdx ); +}; + +const selectors = { + previousPostIds( state, clientId ) { + const { queryBlocks, specificPostsByBlock, postsByBlock } = state; + + const postIdsFromSpecificMode = queryBlocks + .filter( ( b ) => specificPostsByBlock[ b.clientId ] ) + .flatMap( ( b ) => + specificPostsByBlock[ b.clientId ].map( ( p ) => p.id ) + ); + + const previousPostIds = blocksBefore( queryBlocks, clientId ) + .filter( ( b ) => postsByBlock[ b.clientId ] ) + .flatMap( ( b ) => + postsByBlock[ b.clientId ].map( ( p ) => p.id ) + ); + + return uniq( postIdsFromSpecificMode.concat( previousPostIds ) ).sort(); + }, +}; + +/** + * Returns an array of all registered blocks in the order they are on + * the page. This is needed to be able to show the editor blocks in the order + * that PHP will render them. + * + * @param {Array} blocks an array of all blocks in orders + */ +const getQueryBlocksInOrder = ( blocks ) => + blocks.flatMap( ( block ) => { + const queryBlocks = []; + if ( blockNames.has( block.name ) ) { + queryBlocks.push( block ); + } + return queryBlocks.concat( getQueryBlocksInOrder( block.innerBlocks ) ); + } ); + +const reducer = ( state = initialState, action ) => { + switch ( action.type ) { + case UPDATE_BLOCKS: + const updateBlocksState = { + ...state, + queryBlocks: getQueryBlocksInOrder( action.blocks ), + }; + return updateBlocksState; + case MARK_POSTS_DISPLAYED: + return { + ...state, + postsByBlock: { + ...state.postsByBlock, + [ action.clientId ]: action.posts, + }, + }; + case MARK_SPECIFIC_POSTS_DISPLAYED: + return { + ...state, + specificPostsByBlock: { + ...state.specificPostsByBlock, + [ action.clientId ]: action.posts, + }, + }; + } + return state; +}; + +let registered = false; +export const registerQueryStore = () => { + if ( registered ) return; + + registerStore( STORE_NAMESPACE, { + reducer, + actions, + selectors, + initialState, + } ); + + const { getClientIdsWithDescendants, getBlocks } = select( + 'core/block-editor' + ); + const { updateBlocks } = dispatch( STORE_NAMESPACE ); + + let currentBlocksIds; + subscribe( () => { + const newBlocksIds = getClientIdsWithDescendants(); + const blocksChanged = newBlocksIds !== currentBlocksIds; + currentBlocksIds = newBlocksIds; + + if ( blocksChanged ) { + updateBlocks( getBlocks() ); + } + } ); + + registered = true; +}; From 72edd01e829f69fe0adee6d886269b8ad93293c6 Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Tue, 11 Feb 2020 15:02:35 -0500 Subject: [PATCH 2/9] Fix author criteria --- packages/block-library/src/query/edit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js index 8971b6b514d53..22b352456d64d 100644 --- a/packages/block-library/src/query/edit.js +++ b/packages/block-library/src/query/edit.js @@ -204,7 +204,7 @@ const isSpecificPostModeActive = ( { specificMode, specificPosts } ) => const queryCriteriaFromAttributes = ( criteria ) => { const { per_page: perPage, - authors, + author, categories, tags, specificPosts, @@ -219,7 +219,7 @@ const queryCriteriaFromAttributes = ( criteria ) => { : { per_page: perPage, categories, - author: authors, + author, tags, }, ( value ) => ! isUndefined( value ) From 1419bd6f7d2310a872487b0868f2e5f3683bacb3 Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Mon, 2 Mar 2020 16:11:10 -0500 Subject: [PATCH 3/9] Remove deduplication logic --- packages/block-library/src/index.js | 6 - packages/block-library/src/query/edit.js | 28 +--- packages/block-library/src/query/store.js | 161 ---------------------- 3 files changed, 3 insertions(+), 192 deletions(-) delete mode 100644 packages/block-library/src/query/store.js diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 32c53cfe1086b..a8625d4e01826 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -76,7 +76,6 @@ import * as postExcerpt from './post-excerpt'; import * as postFeaturedImage from './post-featured-image'; import * as postTags from './post-tags'; import * as query from './query'; -import { registerQueryStore, registerDeduplicatedBlock } from './query/store'; /** * Function to register an individual block. @@ -213,10 +212,5 @@ export const __experimentalRegisterExperimentalCoreBlocks = ] : [] ), ].forEach( registerBlock ); - - if ( __experimentalEnableFullSiteEditing ) { - registerQueryStore(); - registerDeduplicatedBlock( `core/${ query.name }` ); - } } : undefined; diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js index 22b352456d64d..b84886e49261b 100644 --- a/packages/block-library/src/query/edit.js +++ b/packages/block-library/src/query/edit.js @@ -2,7 +2,6 @@ * Internal dependencies */ import QueryPanel from './query-panel'; -import { STORE_NAMESPACE } from './store'; /** * External dependencies @@ -25,7 +24,7 @@ import { cloneBlock, createBlock } from '@wordpress/blocks'; import { PanelBody, Placeholder, Spinner } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { EntityProvider } from '@wordpress/core-data'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { withSelect } from '@wordpress/data'; const defaultFields = [ 'core/post-title', @@ -109,11 +108,9 @@ class Edit extends Component { const { attributes, className, + postList, query, setAttributes, - clientId, - postList, - markPostsAsDisplayed, } = this.props; const { criteria } = attributes; @@ -124,7 +121,6 @@ class Edit extends Component { className, editingPost ? 'is-editing' : '' ); - markPostsAsDisplayed( clientId, query ); return (
@@ -229,17 +225,10 @@ const queryCriteriaFromAttributes = ( criteria ) => { export default compose( withSelect( ( select, props ) => { - const { attributes, clientId } = props; + const { attributes } = props; const { criteria } = attributes; const queryCriteria = queryCriteriaFromAttributes( criteria ); - if ( ! isSpecificPostModeActive( criteria ) ) { - const postIdsToExclude = select( STORE_NAMESPACE ).previousPostIds( - clientId - ); - queryCriteria.exclude = postIdsToExclude.join( ',' ); - } - return { query: select( 'core' ).getEntityRecords( 'postType', @@ -247,16 +236,5 @@ export default compose( queryCriteria ), }; - } ), - withDispatch( ( dispatch, props ) => { - const { attributes } = props; - const { criteria } = attributes; - const markPostsAsDisplayed = isSpecificPostModeActive( criteria ) - ? dispatch( STORE_NAMESPACE ).markSpecificPostsAsDisplayed - : dispatch( STORE_NAMESPACE ).markPostsAsDisplayed; - - return { - markPostsAsDisplayed, - }; } ) )( Edit ); diff --git a/packages/block-library/src/query/store.js b/packages/block-library/src/query/store.js deleted file mode 100644 index 75574ababbeea..0000000000000 --- a/packages/block-library/src/query/store.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * External dependencies - */ -import { uniq } from 'lodash'; - -/** - * WordPress dependencies - */ -import { registerStore, select, subscribe, dispatch } from '@wordpress/data'; - -export const STORE_NAMESPACE = 'core/post-deduplication'; - -const blockNames = new Set(); - -/** - * Add a block to be watched for deduplication - * - * @param {string} blockName - */ -export const registerDeduplicatedBlock = ( blockName ) => - blockNames.add( blockName ); - -const initialState = { - queryBlocks: [], // list of Query blocks in the order they are on the page - postsByBlock: {}, // map of returned posts to block clientId - specificPostsByBlock: {}, // posts displayed by specific-mode, which always return in the selector -}; - -const UPDATE_BLOCKS = 'UPDATE_BLOCKS'; -const MARK_POSTS_DISPLAYED = 'MARK_POSTS_DISPLAYED'; -const MARK_SPECIFIC_POSTS_DISPLAYED = 'MARK_SPECIFIC_POSTS_DISPLAYED'; - -const actions = { - updateBlocks( blocks ) { - return { - type: UPDATE_BLOCKS, - blocks, - }; - }, - markPostsAsDisplayed( clientId, posts ) { - return { - type: MARK_POSTS_DISPLAYED, - clientId, - posts, - }; - }, - markSpecificPostsAsDisplayed( clientId, posts ) { - return { - type: MARK_SPECIFIC_POSTS_DISPLAYED, - clientId, - posts, - }; - }, -}; - -/** - * Returns the Query blocks that appear before the current one on the page - * - * @param {Array} orderedBlocks An array of Block objects in the order on the page - * @param {string} clientId Client ID of the Block to find blocks before - */ -const blocksBefore = ( orderedBlocks, clientId ) => { - const ourBlockIdx = orderedBlocks.findIndex( - ( b ) => b.clientId === clientId - ); - return orderedBlocks.slice( 0, ourBlockIdx ); -}; - -const selectors = { - previousPostIds( state, clientId ) { - const { queryBlocks, specificPostsByBlock, postsByBlock } = state; - - const postIdsFromSpecificMode = queryBlocks - .filter( ( b ) => specificPostsByBlock[ b.clientId ] ) - .flatMap( ( b ) => - specificPostsByBlock[ b.clientId ].map( ( p ) => p.id ) - ); - - const previousPostIds = blocksBefore( queryBlocks, clientId ) - .filter( ( b ) => postsByBlock[ b.clientId ] ) - .flatMap( ( b ) => - postsByBlock[ b.clientId ].map( ( p ) => p.id ) - ); - - return uniq( postIdsFromSpecificMode.concat( previousPostIds ) ).sort(); - }, -}; - -/** - * Returns an array of all registered blocks in the order they are on - * the page. This is needed to be able to show the editor blocks in the order - * that PHP will render them. - * - * @param {Array} blocks an array of all blocks in orders - */ -const getQueryBlocksInOrder = ( blocks ) => - blocks.flatMap( ( block ) => { - const queryBlocks = []; - if ( blockNames.has( block.name ) ) { - queryBlocks.push( block ); - } - return queryBlocks.concat( getQueryBlocksInOrder( block.innerBlocks ) ); - } ); - -const reducer = ( state = initialState, action ) => { - switch ( action.type ) { - case UPDATE_BLOCKS: - const updateBlocksState = { - ...state, - queryBlocks: getQueryBlocksInOrder( action.blocks ), - }; - return updateBlocksState; - case MARK_POSTS_DISPLAYED: - return { - ...state, - postsByBlock: { - ...state.postsByBlock, - [ action.clientId ]: action.posts, - }, - }; - case MARK_SPECIFIC_POSTS_DISPLAYED: - return { - ...state, - specificPostsByBlock: { - ...state.specificPostsByBlock, - [ action.clientId ]: action.posts, - }, - }; - } - return state; -}; - -let registered = false; -export const registerQueryStore = () => { - if ( registered ) return; - - registerStore( STORE_NAMESPACE, { - reducer, - actions, - selectors, - initialState, - } ); - - const { getClientIdsWithDescendants, getBlocks } = select( - 'core/block-editor' - ); - const { updateBlocks } = dispatch( STORE_NAMESPACE ); - - let currentBlocksIds; - subscribe( () => { - const newBlocksIds = getClientIdsWithDescendants(); - const blocksChanged = newBlocksIds !== currentBlocksIds; - currentBlocksIds = newBlocksIds; - - if ( blocksChanged ) { - updateBlocks( getBlocks() ); - } - } ); - - registered = true; -}; From 361560d8ab25ef544645d2d1dcc2b8fa059fcdc7 Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Fri, 6 Mar 2020 14:10:41 -0500 Subject: [PATCH 4/9] Remove newspack mention and InnerBlocks.save --- packages/block-library/src/query/edit.js | 5 +---- packages/block-library/src/query/index.js | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js index b84886e49261b..9dc3ac4a5cebd 100644 --- a/packages/block-library/src/query/edit.js +++ b/packages/block-library/src/query/edit.js @@ -146,10 +146,7 @@ class Edit extends Component { ) } { query && ! query.length && ( - { __( - 'Sorry, no posts were found.', - 'newspack-blocks' - ) } + { __( 'Sorry, no posts were found.' ) } ) } { query && diff --git a/packages/block-library/src/query/index.js b/packages/block-library/src/query/index.js index 4a469dc5e5974..321544cc578bb 100644 --- a/packages/block-library/src/query/index.js +++ b/packages/block-library/src/query/index.js @@ -3,7 +3,6 @@ */ import { Path, SVG } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { InnerBlocks } from '@wordpress/block-editor'; /** * Internal dependencies @@ -68,5 +67,4 @@ export const settings = { align: false, }, edit, - save: () => , }; From 4bbec33a76ac1deb38c1f5b948340ee902c28c11 Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Mon, 9 Mar 2020 16:02:12 -0400 Subject: [PATCH 5/9] - Use global editor settings for `BlockEditorProvider` - DRY block attributes and move to `block.json` --- packages/block-library/src/query/block.json | 30 ++++++++++++++++++++- packages/block-library/src/query/edit.js | 3 ++- packages/block-library/src/query/index.js | 29 -------------------- packages/block-library/src/query/index.php | 22 ++++++--------- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index 55b19a44b8d6f..2d02ba1eac27b 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -1,4 +1,32 @@ { "name": "core/query", - "category": "layout" + "category": "layout", + "attributes": { + "className": { + "type": "string" + }, + "criteria": { + "type": "object", + "default": { + "per_page": 3, + "offset": 0, + "tags": [], + "categories": [], + "author": [], + "specificPosts": [] + } + }, + "blocks": { + "type": "array", + "default": [ + { + "isValid": true, + "clientId": null, + "name": "post-title", + "attributes": {}, + "innerBlocks": [] + } + ] + } + } } diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js index 9dc3ac4a5cebd..41cafabd67b56 100644 --- a/packages/block-library/src/query/edit.js +++ b/packages/block-library/src/query/edit.js @@ -111,12 +111,12 @@ class Edit extends Component { postList, query, setAttributes, + settings, } = this.props; const { criteria } = attributes; const { editingPost, blocksTree } = this.state; - const settings = {}; const classes = classNames( className, editingPost ? 'is-editing' : '' @@ -232,6 +232,7 @@ export default compose( 'post', queryCriteria ), + settings: select( 'core/block-editor' ).getSettings(), }; } ) )( Edit ); diff --git a/packages/block-library/src/query/index.js b/packages/block-library/src/query/index.js index 321544cc578bb..24cf1db0b90b0 100644 --- a/packages/block-library/src/query/index.js +++ b/packages/block-library/src/query/index.js @@ -31,37 +31,8 @@ export const icon = ( export const settings = { title, icon, - category: 'layout', keywords: [], description: __( 'A collection of posts.' ), - attributes: { - className: { - type: 'string', - }, - criteria: { - type: 'object', - default: { - per_page: 3, - offset: 0, - tags: [], - categories: [], - author: [], - specificPosts: [], - }, - }, - blocks: { - type: 'array', - default: [ - { - isValid: true, - clientId: null, - name: 'post-title', - attributes: {}, - innerBlocks: [], - }, - ], - }, - }, supports: { html: false, align: false, diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index fbde090e7fdf0..19b710c03d27d 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -16,21 +16,15 @@ class Blocks_Query { * Registers the `core/query` block on server. */ function register_block_core_query() { + $path = __DIR__ . '/query/block.json'; + $metadata = json_decode( file_get_contents( $path), true); register_block_type( - 'core/query', - array( - 'attributes' => array( - 'className' => array( - 'type' => 'string', - ), - 'criteria' => array( - 'type' => 'object', - 'default' => array( - 'perPage' => 3, - ), - ), - ), - 'render_callback' => 'render_block_core_query', + $metadata['name'], + array_merge( + $metadata, + array( + 'render_callback' => 'render_block_core_query', + ) ) ); } From f390c6ac5b0c7595137012f258e263b706e4a5ec Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Mon, 16 Mar 2020 16:12:34 -0400 Subject: [PATCH 6/9] Set default blocks as default attributes --- packages/block-library/src/query/block.json | 23 ++++++++++++++++----- packages/block-library/src/query/edit.js | 14 ++----------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index 2d02ba1eac27b..6c04338978baa 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -20,11 +20,24 @@ "type": "array", "default": [ { - "isValid": true, - "clientId": null, - "name": "post-title", - "attributes": {}, - "innerBlocks": [] + "name": "core/post-title", + "innerBlocks": [], + "isValid": true + }, + { + "name": "core/post-date", + "innerBlocks": [], + "isValid": true + }, + { + "name": "core/post-author", + "innerBlocks": [], + "isValid": true + }, + { + "name": "core/post-excerpt", + "innerBlocks": [], + "isValid": true } ] } diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js index 41cafabd67b56..5ce6a74a06354 100644 --- a/packages/block-library/src/query/edit.js +++ b/packages/block-library/src/query/edit.js @@ -20,19 +20,12 @@ import { InspectorControls, WritingFlow, } from '@wordpress/block-editor'; -import { cloneBlock, createBlock } from '@wordpress/blocks'; +import { cloneBlock } from '@wordpress/blocks'; import { PanelBody, Placeholder, Spinner } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { EntityProvider } from '@wordpress/core-data'; import { withSelect } from '@wordpress/data'; -const defaultFields = [ - 'core/post-title', - 'core/post-date', - 'core/post-author', - 'core/post-excerpt', -]; - class Edit extends Component { constructor( props ) { super( props ); @@ -49,7 +42,6 @@ class Edit extends Component { componentDidMount() { this.createBlockTree(); - this.updateBlocks( defaultFields.map( ( f ) => createBlock( f ) ) ); } componentDidUpdate( prevProps ) { @@ -69,9 +61,7 @@ class Edit extends Component { [ post.id ]: post.id === editingPost ? blocksTree[ post.id ] - : blocks.map( ( block ) => - cloneBlock( block, { post } ) - ), + : blocks.map( ( block ) => cloneBlock( block ) ), } ), {} ); From 291787c97ccb9a917328cad0c4ba596b876bdd94 Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Mon, 16 Mar 2020 16:53:04 -0400 Subject: [PATCH 7/9] Replace cleanBlocks with standard cloneBlocks --- packages/block-library/src/query/edit.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/block-library/src/query/edit.js b/packages/block-library/src/query/edit.js index 5ce6a74a06354..ab75484b67adb 100644 --- a/packages/block-library/src/query/edit.js +++ b/packages/block-library/src/query/edit.js @@ -68,20 +68,10 @@ class Edit extends Component { this.setState( { blocksTree: newBlocksTree } ); } - cleanBlock( block ) { - const { name, isValid, attributes, innerBlocks } = block; - return { - name, - attributes: { ...attributes, post: {} }, - innerBlocks: innerBlocks.map( ( b ) => this.cleanBlock( b ) ), - isValid, - }; - } - updateBlocks( blocks, postId ) { const { setAttributes } = this.props; const { blocksTree } = this.state; - const cleanBlocks = blocks.map( this.cleanBlock ); + const cleanBlocks = blocks.map( ( block ) => cloneBlock( block ) ); this.setState( { blocksTree: { ...( blocksTree || [] ), [ postId ]: blocks }, From 382325512da0ad4d6feac609dbe0fdc5b00de0f2 Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Fri, 20 Mar 2020 14:18:25 -0400 Subject: [PATCH 8/9] Fix PHP warning --- packages/block-library/src/query/index.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 19b710c03d27d..d834ed040877d 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -93,9 +93,11 @@ function render_block_core_query( $attributes ) { foreach ( $blocks as $block ) { $block_data = array( 'blockName' => $block['name'], - 'attrs' => $block['attributes'], 'innerContent' => array(), ); + if ( isset( $block['attributes'])) { + $block_data['attrs'] = $block['attributes']; + } $allowed_html = wp_kses_allowed_html( 'post' ); $allowed_html['time'] = array( From 699885e193513b645ba6fa3868888a78b456a86b Mon Sep 17 00:00:00 2001 From: George Hotelling Date: Tue, 14 Apr 2020 16:11:43 -0400 Subject: [PATCH 9/9] Fixup a bug with server rendering count --- packages/block-library/src/query/index.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index d834ed040877d..ef9fd400c5366 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -37,7 +37,7 @@ function register_block_core_query() { * @return array Return an array of args. */ function core_query_attributes_to_critera( $criteria ) { - if ( $criteria[ 'specificMode' ] == 1 ) { + if ( isset( $criteria[ 'specificMode' ] ) && $criteria[ 'specificMode' ] == 1 ) { $args = array( 'post_status' => 'publish', 'p' => $criteria[ 'singleId' ], @@ -70,15 +70,17 @@ function core_query_attributes_to_critera( $criteria ) { function render_block_core_query( $attributes ) { $blocks = ! empty( $attributes['blocks'] ) ? $attributes['blocks'] : array(); $args = core_query_attributes_to_critera( $attributes['criteria'] ); + $posts_to_show = $args['posts_per_page']; $args['posts_per_page'] = $args['posts_per_page'] + count( Blocks_Query::$displayedPostIds ); $query = new WP_Query( $args ); + $posts_shown = 0; ob_start(); ?>
have_posts() ) : ?> - have_posts() ) : ?> + have_posts() && $posts_shown < $posts_to_show) : ?> the_post(); $id = get_the_ID(); @@ -86,6 +88,7 @@ function render_block_core_query( $attributes ) { continue; } else { array_push( Blocks_Query::$displayedPostIds, $id ); + $posts_shown++; } ?>
@@ -109,8 +112,8 @@ function render_block_core_query( $attributes ) { } ?>
- - + +