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 } /> ), ];