From 7488070889019b0373ddf9b5e3da70423b27dcf3 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Thu, 12 Mar 2020 18:18:49 +0200 Subject: [PATCH] adds flat selector instead of select control --- .../block-library/src/latest-posts/edit.js | 31 +- .../src/latest-posts/flat-term-selector.js | 308 ++++++++++++++++++ .../block-library/src/latest-posts/index.php | 2 +- .../components/src/query-controls/index.js | 14 - 4 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 packages/block-library/src/latest-posts/flat-term-selector.js diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 06e757c8525ba..b6bda63c56085 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -32,15 +32,17 @@ import { import { withSelect } from '@wordpress/data'; import { pin, list, grid } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import FlatTermSelector from './flat-term-selector'; + /** * Module Constants */ const CATEGORIES_LIST_QUERY = { per_page: -1, }; -const TAGS_LIST_QUERY = { - per_page: -1, -}; const MAX_POSTS_COLUMNS = 6; class LatestPostsEdit extends Component { @@ -67,20 +69,6 @@ class LatestPostsEdit extends Component { this.setState( { categoriesList: [] } ); } } ); - - this.fetchRequest = apiFetch( { - path: addQueryArgs( `/wp/v2/tags`, TAGS_LIST_QUERY ), - } ) - .then( ( tagsList ) => { - if ( this.isStillMounted ) { - this.setState( { tagsList } ); - } - } ) - .catch( () => { - if ( this.isStillMounted ) { - this.setState( { tagsList: [] } ); - } - } ); } componentWillUnmount() { @@ -227,6 +215,15 @@ class LatestPostsEdit extends Component { + { + setAttributes( { + tags: '' !== value ? tags.push( value ) : tags, + } ); + } } + /> + termA.toLowerCase() === termB.toLowerCase(); + +/** + * Returns a term object with name unescaped. + * The unescape of the name property is done using lodash unescape function. + * + * @param {Object} term The term object to unescape. + * + * @return {Object} Term object with name property unescaped. + */ +const unescapeTerm = ( term ) => { + return { + ...term, + name: unescapeString( term.name ), + }; +}; + +/** + * Returns an array of term objects with names unescaped. + * The unescape of each term is performed using the unescapeTerm function. + * + * @param {Object[]} terms Array of term objects to unescape. + * + * @return {Object[]} Array of term objects unescaped. + */ +const unescapeTerms = ( terms ) => { + return map( terms, unescapeTerm ); +}; + +class FlatTermSelector extends Component { + constructor() { + super( ...arguments ); + this.onChange = this.onChange.bind( this ); + this.searchTerms = throttle( this.searchTerms.bind( this ), 500 ); + this.findOrCreateTerm = this.findOrCreateTerm.bind( this ); + this.state = { + loading: ! isEmpty( this.props.terms ), + availableTerms: [], + selectedTerms: [], + }; + } + + componentDidMount() { + if ( ! isEmpty( this.props.terms ) ) { + this.initRequest = this.fetchTerms( { + include: this.props.terms.join( ',' ), + per_page: -1, + } ); + this.initRequest.then( + () => { + this.setState( { loading: false } ); + }, + ( xhr ) => { + if ( xhr.statusText === 'abort' ) { + return; + } + this.setState( { + loading: false, + } ); + } + ); + } + } + + componentWillUnmount() { + invoke( this.initRequest, [ 'abort' ] ); + invoke( this.searchRequest, [ 'abort' ] ); + } + + componentDidUpdate( prevProps ) { + if ( prevProps.terms !== this.props.terms ) { + this.updateSelectedTerms( this.props.terms ); + } + } + + fetchTerms( params = {} ) { + const { taxonomy } = this.props; + const query = { ...DEFAULT_QUERY, ...params }; + const request = apiFetch( { + path: addQueryArgs( `/wp/v2/${ taxonomy.rest_base }`, query ), + } ); + request.then( unescapeTerms ).then( ( terms ) => { + this.setState( ( state ) => ( { + availableTerms: state.availableTerms.concat( + terms.filter( + ( term ) => + ! find( + state.availableTerms, + ( availableTerm ) => + availableTerm.id === term.id + ) + ) + ), + } ) ); + this.updateSelectedTerms( this.props.terms ); + } ); + + return request; + } + + updateSelectedTerms( terms = [] ) { + const selectedTerms = terms.reduce( ( accumulator, termId ) => { + const termObject = find( + this.state.availableTerms, + ( term ) => term.id === termId + ); + if ( termObject ) { + accumulator.push( termObject.name ); + } + + return accumulator; + }, [] ); + this.setState( { + selectedTerms, + } ); + } + + findOrCreateTerm( termName ) { + const { taxonomy } = this.props; + const termNameEscaped = escapeString( termName ); + // Tries to create a term or fetch it if it already exists. + return apiFetch( { + path: `/wp/v2/${ taxonomy.rest_base }`, + method: 'POST', + data: { name: termNameEscaped }, + } ) + .catch( ( error ) => { + const errorCode = error.code; + if ( errorCode === 'term_exists' ) { + // If the terms exist, fetch it instead of creating a new one. + this.addRequest = apiFetch( { + path: addQueryArgs( `/wp/v2/${ taxonomy.rest_base }`, { + ...DEFAULT_QUERY, + search: termNameEscaped, + } ), + } ).then( unescapeTerms ); + return this.addRequest.then( ( searchResult ) => { + return find( searchResult, ( result ) => + isSameTermName( result.name, termName ) + ); + } ); + } + return Promise.reject( error ); + } ) + .then( unescapeTerm ); + } + + onChange( termNames ) { + const uniqueTerms = uniqBy( termNames, ( term ) => term.toLowerCase() ); + this.setState( { selectedTerms: uniqueTerms } ); + const newTermNames = uniqueTerms.filter( + ( termName ) => + ! find( this.state.availableTerms, ( term ) => + isSameTermName( term.name, termName ) + ) + ); + const termNamesToIds = ( names, availableTerms ) => { + return names.map( + ( termName ) => + find( availableTerms, ( term ) => + isSameTermName( term.name, termName ) + ).id + ); + }; + + if ( newTermNames.length === 0 ) { + return this.props.onTagChange( + termNamesToIds( uniqueTerms, this.state.availableTerms ), + this.props.taxonomy.rest_base + ); + } + Promise.all( newTermNames.map( this.findOrCreateTerm ) ).then( + ( newTerms ) => { + const newAvailableTerms = this.state.availableTerms.concat( + newTerms + ); + this.setState( { availableTerms: newAvailableTerms } ); + return this.props.onTagChange( + termNamesToIds( uniqueTerms, newAvailableTerms ), + this.props.taxonomy.rest_base + ); + } + ); + } + + searchTerms( search = '' ) { + invoke( this.searchRequest, [ 'abort' ] ); + this.searchRequest = this.fetchTerms( { search } ); + } + + render() { + const { slug, taxonomy, hasAssignAction, terms } = this.props; + + if ( ! hasAssignAction ) { + return null; + } + + const { loading, availableTerms } = this.state; + const termNames = availableTerms.map( ( term ) => term.name ); + const newTermLabel = get( + taxonomy, + [ 'labels', 'add_new_item' ], + slug === 'post_tag' ? __( 'Add new tag' ) : __( 'Add new Term' ) + ); + const singularName = get( + taxonomy, + [ 'labels', 'singular_name' ], + slug === 'post_tag' ? __( 'Tag' ) : __( 'Term' ) + ); + const termAddedLabel = sprintf( + _x( '%s added', 'term' ), + singularName + ); + const termRemovedLabel = sprintf( + _x( '%s removed', 'term' ), + singularName + ); + const removeTermLabel = sprintf( + _x( 'Remove %s', 'term' ), + singularName + ); + + return ( + + ); + } +} + +export default compose( + withSelect( ( select, { slug } ) => { + const { getCurrentPost } = select( 'core/editor' ); + const { getTaxonomy } = select( 'core' ); + const taxonomy = getTaxonomy( slug ); + return { + hasCreateAction: taxonomy + ? get( + getCurrentPost(), + [ '_links', 'wp:action-create-' + taxonomy.rest_base ], + false + ) + : false, + hasAssignAction: taxonomy + ? get( + getCurrentPost(), + [ '_links', 'wp:action-assign-' + taxonomy.rest_base ], + false + ) + : false, + terms: taxonomy + ? select( 'core/editor' ).getEditedPostAttribute( + taxonomy.rest_base + ) + : [], + taxonomy, + }; + } ), + withFilters( 'editor.PostTaxonomyType' ) +)( FlatTermSelector ); diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index d9ae54a5d1c09..74424551b6415 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -189,7 +189,7 @@ function register_block_core_latest_posts() { 'type' => 'string', ), 'tags' => array( - 'type' => 'string', + 'type' => 'array', ), 'postsToShow' => array( 'type' => 'number', diff --git a/packages/components/src/query-controls/index.js b/packages/components/src/query-controls/index.js index aa3c4246b84d9..e0b54e3e8a027 100644 --- a/packages/components/src/query-controls/index.js +++ b/packages/components/src/query-controls/index.js @@ -8,7 +8,6 @@ import { __ } from '@wordpress/i18n'; */ import { RangeControl, SelectControl } from '../'; import CategorySelect from './category-select'; -import TagSelect from './tag-select'; const DEFAULT_MIN_ITEMS = 1; const DEFAULT_MAX_ITEMS = 100; @@ -16,15 +15,12 @@ const DEFAULT_MAX_ITEMS = 100; export default function QueryControls( { categoriesList, selectedCategoryId, - tagsList, - selectedTagId, numberOfItems, order, orderBy, maxItems = DEFAULT_MAX_ITEMS, minItems = DEFAULT_MIN_ITEMS, onCategoryChange, - onTagChange, onNumberOfItemsChange, onOrderChange, onOrderByChange, @@ -76,16 +72,6 @@ export default function QueryControls( { onChange={ onCategoryChange } /> ), - onTagChange && ( - - ), onNumberOfItemsChange && (