Skip to content

Commit

Permalink
Create Query Block
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
George Hotelling committed Feb 7, 2020
1 parent 525dd41 commit 0b95943
Show file tree
Hide file tree
Showing 10 changed files with 851 additions and 1 deletion.
1 change: 1 addition & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function gutenberg_reregister_core_block_types() {
'post-author.php' => 'core/post-author',
'post-date.php' => 'core/post-date',
'post-excerpt.php' => 'core/post-excerpt',
'query.php' => 'core/query',
);

$registry = WP_Block_Type_Registry::get_instance();
Expand Down
1 change: 1 addition & 0 deletions packages/block-library/src/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@import "./nextpage/editor.scss";
@import "./paragraph/editor.scss";
@import "./pullquote/editor.scss";
@import "./query/editor.scss";
@import "./quote/editor.scss";
@import "./rss/editor.scss";
@import "./search/editor.scss";
Expand Down
9 changes: 8 additions & 1 deletion packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ import * as postContent from './post-content';
import * as postAuthor from './post-author';
import * as postDate from './post-date';
import * as postExcerpt from './post-excerpt';

import * as query from './query';
import { registerQueryStore, registerDeduplicatedBlock } from './query/store';
/**
* Function to register an individual block.
*
Expand Down Expand Up @@ -197,8 +198,14 @@ export const __experimentalRegisterExperimentalCoreBlocks =
postAuthor,
postDate,
postExcerpt,
query,
]
: [] ),
].forEach( registerBlock );

if ( __experimentalEnableFullSiteEditing ) {
registerQueryStore();
registerDeduplicatedBlock( `core/${ query.name }` );
}
}
: undefined;
4 changes: 4 additions & 0 deletions packages/block-library/src/query/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "core/query",
"category": "layout"
}
262 changes: 262 additions & 0 deletions packages/block-library/src/query/edit.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={ classes }>
<InspectorControls>
<PanelBody
title={ __( 'Query Settings' ) }
initialOpen={ true }
>
<QueryPanel
criteria={ criteria }
postList={ postList }
onChange={ ( newCriteria ) =>
setAttributes( { criteria: newCriteria } )
}
/>
</PanelBody>
</InspectorControls>
<Fragment>
{ ! query && (
<Placeholder>
<Spinner />
</Placeholder>
) }
{ query && ! query.length && (
<Placeholder>
{ __(
'Sorry, no posts were found.',
'newspack-blocks'
) }
</Placeholder>
) }
{ query &&
!! query.length &&
query.map( ( post ) => {
if ( ! blocksTree[ post.id ] ) return null;
return (
<article
className={
post.id === editingPost
? 'is-editing'
: ''
}
key={ post.id }
>
<EntityProvider
kind="postType"
type="post"
id={ post.id }
>
<BlockEditorProvider
value={ blocksTree[ post.id ] }
onChange={ ( blocks ) =>
this.updateBlocks(
blocks,
post.id
)
}
settings={ settings }
>
<WritingFlow>
<BlockList />
</WritingFlow>
</BlockEditorProvider>
</EntityProvider>
</article>
);
} ) }
</Fragment>
</div>
);
}
}

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 );
5 changes: 5 additions & 0 deletions packages/block-library/src/query/editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.wp-block-query {
.block-editor-writing-flow__click-redirect {
display: none;
}
}
72 changes: 72 additions & 0 deletions packages/block-library/src/query/index.js
Original file line number Diff line number Diff line change
@@ -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 = (
<SVG
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<Path d="M0 0h24v24H0z" fill="none" />
<Path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" />
</SVG>
);

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: () => <InnerBlocks.Content />,
};
Loading

0 comments on commit 0b95943

Please sign in to comment.