diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js
index 262b1bf7354c1..968e652d86505 100644
--- a/packages/block-library/src/index.native.js
+++ b/packages/block-library/src/index.native.js
@@ -152,6 +152,7 @@ export const registerCoreBlocks = () => {
button,
spacer,
shortcode,
+ latestPosts,
devOnly( verse ),
cover,
].forEach( registerBlock );
diff --git a/packages/block-library/src/latest-posts/block.json b/packages/block-library/src/latest-posts/block.json
new file mode 100644
index 0000000000000..e730d86162650
--- /dev/null
+++ b/packages/block-library/src/latest-posts/block.json
@@ -0,0 +1,72 @@
+{
+ "name": "core/latest-posts",
+ "category": "widgets",
+ "attributes": {
+ "align": {
+ "type": "string",
+ "enum": ["left", "center", "right", "wide", "full"]
+ },
+ "className": {
+ "type": "string"
+ },
+ "categories": {
+ "type": "array"
+ },
+ "postsToShow": {
+ "type": "number",
+ "default": 5
+ },
+ "displayPostContent": {
+ "type": "boolean",
+ "default": false
+ },
+ "displayPostContentRadio": {
+ "type": "string",
+ "default": "excerpt"
+ },
+ "excerptLength": {
+ "type": "number",
+ "default": 55
+ },
+ "displayPostDate": {
+ "type": "boolean",
+ "default": false
+ },
+ "postLayout": {
+ "type": "string",
+ "default": "list"
+ },
+ "columns": {
+ "type": "number",
+ "default": 3
+ },
+ "order": {
+ "type": "string",
+ "default": "desc"
+ },
+ "orderBy": {
+ "type": "string",
+ "default": "date"
+ },
+ "displayFeaturedImage": {
+ "type": "boolean",
+ "default": false
+ },
+ "featuredImageAlign": {
+ "type": "string",
+ "enum": ["left", "center", "right"]
+ },
+ "featuredImageSizeSlug": {
+ "type": "string",
+ "default":"thumbnail"
+ },
+ "featuredImageSizeWidth": {
+ "type": "number",
+ "default":null
+ },
+ "featuredImageSizeHeight": {
+ "type": "number",
+ "default":null
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/block-library/src/latest-posts/constants.js b/packages/block-library/src/latest-posts/constants.js
new file mode 100644
index 0000000000000..70c34448a3bff
--- /dev/null
+++ b/packages/block-library/src/latest-posts/constants.js
@@ -0,0 +1,3 @@
+export const MIN_EXCERPT_LENGTH = 10;
+export const MAX_EXCERPT_LENGTH = 100;
+export const MAX_POSTS_COLUMNS = 6;
diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js
index 2726b5e20d169..c7ca39a6cd09b 100644
--- a/packages/block-library/src/latest-posts/edit.js
+++ b/packages/block-library/src/latest-posts/edit.js
@@ -33,13 +33,21 @@ import {
import { withSelect } from '@wordpress/data';
import { pin, list, grid } from '@wordpress/icons';
+/**
+ * Internal dependencies
+ */
+import {
+ MIN_EXCERPT_LENGTH,
+ MAX_EXCERPT_LENGTH,
+ MAX_POSTS_COLUMNS,
+} from './constants';
+
/**
* Module Constants
*/
const CATEGORIES_LIST_QUERY = {
per_page: -1,
};
-const MAX_POSTS_COLUMNS = 6;
class LatestPostsEdit extends Component {
constructor() {
@@ -150,8 +158,8 @@ class LatestPostsEdit extends Component {
onChange={ ( value ) =>
setAttributes( { excerptLength: value } )
}
- min={ 10 }
- max={ 100 }
+ min={ MIN_EXCERPT_LENGTH }
+ max={ MAX_EXCERPT_LENGTH }
/>
) }
diff --git a/packages/block-library/src/latest-posts/edit.native.js b/packages/block-library/src/latest-posts/edit.native.js
new file mode 100644
index 0000000000000..53481203a135c
--- /dev/null
+++ b/packages/block-library/src/latest-posts/edit.native.js
@@ -0,0 +1,261 @@
+/**
+ * External dependencies
+ */
+import { TouchableWithoutFeedback, View, Text } from 'react-native';
+import { isEmpty } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { compose, withPreferredColorScheme } from '@wordpress/compose';
+import { withDispatch } from '@wordpress/data';
+import { coreBlocks } from '@wordpress/block-library';
+import { __ } from '@wordpress/i18n';
+import { postList as icon } from '@wordpress/icons';
+import { InspectorControls } from '@wordpress/block-editor';
+import { fetchRequest } from 'react-native-gutenberg-bridge';
+import {
+ Icon,
+ PanelBody,
+ ToggleControl,
+ RangeControl,
+ QueryControls,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+import { MIN_EXCERPT_LENGTH, MAX_EXCERPT_LENGTH } from './constants';
+
+class LatestPostsEdit extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ categoriesList: [],
+ };
+ this.onSetDisplayPostContent = this.onSetDisplayPostContent.bind(
+ this
+ );
+ this.onSetDisplayPostContentRadio = this.onSetDisplayPostContentRadio.bind(
+ this
+ );
+ this.onSetExcerptLength = this.onSetExcerptLength.bind( this );
+ this.onSetDisplayPostDate = this.onSetDisplayPostDate.bind( this );
+ this.onSetOrder = this.onSetOrder.bind( this );
+ this.onSetOrderBy = this.onSetOrderBy.bind( this );
+ this.onSetPostsToShow = this.onSetPostsToShow.bind( this );
+ this.onSetCategories = this.onSetCategories.bind( this );
+ this.getInspectorControls = this.getInspectorControls.bind( this );
+ }
+
+ componentDidMount() {
+ this.isStillMounted = true;
+ this.fetchRequest = fetchRequest( '/wp/v2/categories' )
+ .then( ( categoriesList ) => {
+ if ( this.isStillMounted ) {
+ let parsedCategoriesList = categoriesList;
+
+ // TODO: remove this check after `fetchRequest` types are made consist across platforms
+ // (see: https://github.com/wordpress-mobile/gutenberg-mobile/issues/1961)
+ if ( typeof categoriesList === 'string' ) {
+ parsedCategoriesList = JSON.parse( categoriesList );
+ }
+
+ if ( isEmpty( parsedCategoriesList ) ) {
+ parsedCategoriesList = [];
+ }
+
+ this.setState( {
+ categoriesList: parsedCategoriesList,
+ } );
+ }
+ } )
+ .catch( () => {
+ if ( this.isStillMounted ) {
+ this.setState( { categoriesList: [] } );
+ }
+ } );
+ }
+
+ componentWillUnmount() {
+ this.isStillMounted = false;
+ }
+
+ onSetDisplayPostContent( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( { displayPostContent: value } );
+ }
+
+ onSetDisplayPostContentRadio( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( {
+ displayPostContentRadio: value ? 'excerpt' : 'full_post',
+ } );
+ }
+
+ onSetExcerptLength( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( { excerptLength: value } );
+ }
+
+ onSetDisplayPostDate( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( { displayPostDate: value } );
+ }
+
+ onSetOrder( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( { order: value } );
+ }
+
+ onSetOrderBy( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( { orderBy: value } );
+ }
+
+ onSetPostsToShow( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( { postsToShow: value } );
+ }
+
+ onSetCategories( value ) {
+ const { setAttributes } = this.props;
+ setAttributes( {
+ categories: '' !== value ? value.toString() : undefined,
+ } );
+ }
+
+ getInspectorControls() {
+ const { attributes } = this.props;
+ const {
+ displayPostContent,
+ displayPostContentRadio,
+ excerptLength,
+ displayPostDate,
+ order,
+ orderBy,
+ postsToShow,
+ categories,
+ } = attributes;
+
+ const { categoriesList } = this.state;
+ const displayExcerptPostContent = displayPostContentRadio === 'excerpt';
+
+ return (
+
+
+
+ { displayPostContent && (
+
+ ) }
+ { displayPostContent && displayExcerptPostContent && (
+
+ ) }
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ const {
+ getStylesFromColorScheme,
+ name,
+ openGeneralSidebar,
+ isSelected,
+ } = this.props;
+
+ const blockType = coreBlocks[ name ];
+
+ const blockStyle = getStylesFromColorScheme(
+ styles.latestPostBlock,
+ styles.latestPostBlockDark
+ );
+
+ const iconStyle = getStylesFromColorScheme(
+ styles.latestPostBlockIcon,
+ styles.latestPostBlockIconDark
+ );
+
+ const titleStyle = getStylesFromColorScheme(
+ styles.latestPostBlockMessage,
+ styles.latestPostBlockMessageDark
+ );
+
+ return (
+
+
+ { this.getInspectorControls() }
+
+
+ { blockType.settings.title }
+
+
+ { __( 'CUSTOMIZE' ) }
+
+
+
+ );
+ }
+}
+
+export default compose( [
+ withDispatch( ( dispatch ) => {
+ const { openGeneralSidebar } = dispatch( 'core/edit-post' );
+
+ return {
+ openGeneralSidebar: () => openGeneralSidebar( 'edit-post/block' ),
+ };
+ } ),
+ withPreferredColorScheme,
+] )( LatestPostsEdit );
diff --git a/packages/block-library/src/latest-posts/index.js b/packages/block-library/src/latest-posts/index.js
index bbbacbb40128f..63f7b7bfae638 100644
--- a/packages/block-library/src/latest-posts/index.js
+++ b/packages/block-library/src/latest-posts/index.js
@@ -8,14 +8,15 @@ import { postList as icon } from '@wordpress/icons';
* Internal dependencies
*/
import edit from './edit';
+import metadata from './block.json';
-export const name = 'core/latest-posts';
+const { name } = metadata;
+export { metadata, name };
export const settings = {
title: __( 'Latest Posts' ),
description: __( 'Display a list of your most recent posts.' ),
icon,
- category: 'widgets',
keywords: [ __( 'recent posts' ) ],
supports: {
align: true,
diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php
index 346c4085b3c42..2245ae53930af 100644
--- a/packages/block-library/src/latest-posts/index.php
+++ b/packages/block-library/src/latest-posts/index.php
@@ -170,78 +170,16 @@ function render_block_core_latest_posts( $attributes ) {
* Registers the `core/latest-posts` block on server.
*/
function register_block_core_latest_posts() {
+ $path = __DIR__ . '/latest-posts/block.json';
+ $metadata = json_decode( file_get_contents( $path ), true );
+
register_block_type(
- 'core/latest-posts',
- array(
- 'attributes' => array(
- 'align' => array(
- 'type' => 'string',
- 'enum' => array( 'left', 'center', 'right', 'wide', 'full' ),
- ),
- 'className' => array(
- 'type' => 'string',
- ),
- 'categories' => array(
- 'type' => 'array',
- ),
- 'postsToShow' => array(
- 'type' => 'number',
- 'default' => 5,
- ),
- 'displayPostContent' => array(
- 'type' => 'boolean',
- 'default' => false,
- ),
- 'displayPostContentRadio' => array(
- 'type' => 'string',
- 'default' => 'excerpt',
- ),
- 'excerptLength' => array(
- 'type' => 'number',
- 'default' => 55,
- ),
- 'displayPostDate' => array(
- 'type' => 'boolean',
- 'default' => false,
- ),
- 'postLayout' => array(
- 'type' => 'string',
- 'default' => 'list',
- ),
- 'columns' => array(
- 'type' => 'number',
- 'default' => 3,
- ),
- 'order' => array(
- 'type' => 'string',
- 'default' => 'desc',
- ),
- 'orderBy' => array(
- 'type' => 'string',
- 'default' => 'date',
- ),
- 'displayFeaturedImage' => array(
- 'type' => 'boolean',
- 'default' => false,
- ),
- 'featuredImageAlign' => array(
- 'type' => 'string',
- 'enum' => array( 'left', 'center', 'right' ),
- ),
- 'featuredImageSizeSlug' => array(
- 'type' => 'string',
- 'default' => 'thumbnail',
- ),
- 'featuredImageSizeWidth' => array(
- 'type' => 'number',
- 'default' => null,
- ),
- 'featuredImageSizeHeight' => array(
- 'type' => 'number',
- 'default' => null,
- ),
- ),
- 'render_callback' => 'render_block_core_latest_posts',
+ $metadata['name'],
+ array_merge(
+ $metadata,
+ array(
+ 'render_callback' => 'render_block_core_latest_posts',
+ )
)
);
}
diff --git a/packages/block-library/src/latest-posts/style.native.scss b/packages/block-library/src/latest-posts/style.native.scss
new file mode 100644
index 0000000000000..cba3877e8904b
--- /dev/null
+++ b/packages/block-library/src/latest-posts/style.native.scss
@@ -0,0 +1,47 @@
+.latestPostBlock {
+ height: 142;
+ background-color: $gray-lighten-30;
+ padding-top: 12;
+ padding-bottom: 12;
+ padding-left: 12;
+ padding-right: 12;
+ border-top-left-radius: 4;
+ border-top-right-radius: 4;
+ border-bottom-left-radius: 4;
+ border-bottom-right-radius: 4;
+ align-items: center;
+ justify-content: center;
+}
+
+.latestPostBlockDark {
+ background-color: $background-dark-secondary;
+}
+
+.latestPostBlockIcon {
+ fill: $gray-dark;
+ width: 24px;
+ height: 24px;
+}
+
+.latestPostBlockIconDark {
+ fill: $white;
+}
+
+.latestPostBlockMessage {
+ text-align: center;
+ margin-top: 8;
+ font-size: 14;
+ color: #2e4453;
+}
+
+.latestPostBlockMessageDark {
+ color: $white;
+}
+
+.latestPostBlockSubtitle {
+ margin-top: 10;
+ text-align: center;
+ font-size: 14;
+ font-weight: 500;
+ color: $blue-wordpress;
+}
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index 58a0ad7765cbc..ca4c120d4fa71 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -35,6 +35,7 @@ export { default as ToggleControl } from './toggle-control';
export { default as SelectControl } from './select-control';
export { default as RangeControl } from './range-control';
export { default as UnsupportedFooterControl } from './unsupported-footer-control';
+export { default as QueryControls } from './query-controls';
// Higher-Order Components
export { default as withConstrainedTabbing } from './higher-order/with-constrained-tabbing';
diff --git a/packages/components/src/query-controls/category-select.js b/packages/components/src/query-controls/category-select.js
index b6e11839de990..e0b2291b6e9f0 100644
--- a/packages/components/src/query-controls/category-select.js
+++ b/packages/components/src/query-controls/category-select.js
@@ -10,6 +10,7 @@ export default function CategorySelect( {
categoriesList,
selectedCategoryId,
onChange,
+ ...props
} ) {
const termsTree = buildTermsTree( categoriesList );
return (
@@ -17,6 +18,7 @@ export default function CategorySelect( {
{ ...{ label, noOptionLabel, onChange } }
tree={ termsTree }
selectedId={ selectedCategoryId }
+ { ...props }
/>
);
}
diff --git a/packages/components/src/query-controls/index.js b/packages/components/src/query-controls/index.js
index e0b54e3e8a027..f9a340354399b 100644
--- a/packages/components/src/query-controls/index.js
+++ b/packages/components/src/query-controls/index.js
@@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
+import { Platform } from '@wordpress/element';
/**
* Internal dependencies
@@ -12,6 +13,17 @@ import CategorySelect from './category-select';
const DEFAULT_MIN_ITEMS = 1;
const DEFAULT_MAX_ITEMS = 100;
+// currently this is needed for consistent controls UI on mobile
+// this can be removed after control components settle on consistent defaults
+const MOBILE_CONTROL_PROPS = Platform.select( {
+ web: {},
+ native: { separatorType: 'fullWidth' },
+} );
+const MOBILE_CONTROL_PROPS_SEPARATOR_NONE = Platform.select( {
+ web: {},
+ native: { separatorType: 'none' },
+} );
+
export default function QueryControls( {
categoriesList,
selectedCategoryId,
@@ -60,6 +72,7 @@ export default function QueryControls( {
onOrderByChange( newOrderBy );
}
} }
+ { ...MOBILE_CONTROL_PROPS }
/>
),
onCategoryChange && (
@@ -70,6 +83,7 @@ export default function QueryControls( {
noOptionLabel={ __( 'All' ) }
selectedCategoryId={ selectedCategoryId }
onChange={ onCategoryChange }
+ { ...MOBILE_CONTROL_PROPS }
/>
),
onNumberOfItemsChange && (
@@ -81,6 +95,7 @@ export default function QueryControls( {
min={ minItems }
max={ maxItems }
required
+ { ...MOBILE_CONTROL_PROPS_SEPARATOR_NONE }
/>
),
];