From e713c0edcc2e83af58a0d7edc889de92513e5eea Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 10 Aug 2017 14:18:45 +0100 Subject: [PATCH] Chrome: Allow settings custom taxonomies --- .../post-taxonomies/categories-selector.js | 138 ------------- .../sidebar/post-taxonomies/flat-taxonomy.js | 189 ++++++++++++++++++ .../post-taxonomies/hierarchical-taxonomy.js | 137 +++++++++++++ editor/sidebar/post-taxonomies/index.js | 72 ++++++- editor/sidebar/post-taxonomies/style.scss | 14 +- .../sidebar/post-taxonomies/tags-selector.js | 188 ----------------- lib/client-assets.php | 22 ++ 7 files changed, 416 insertions(+), 344 deletions(-) delete mode 100644 editor/sidebar/post-taxonomies/categories-selector.js create mode 100644 editor/sidebar/post-taxonomies/flat-taxonomy.js create mode 100644 editor/sidebar/post-taxonomies/hierarchical-taxonomy.js delete mode 100644 editor/sidebar/post-taxonomies/tags-selector.js diff --git a/editor/sidebar/post-taxonomies/categories-selector.js b/editor/sidebar/post-taxonomies/categories-selector.js deleted file mode 100644 index 9ef35c914312df..00000000000000 --- a/editor/sidebar/post-taxonomies/categories-selector.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; -import { unescape as unescapeString, without, groupBy } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { getEditedPostAttribute } from '../../selectors'; -import { editPost } from '../../actions'; - -const DEFAULT_CATEGORIES_QUERY = { - per_page: 100, - orderby: 'count', - order: 'desc', -}; - -class CategoriesSelector extends Component { - constructor() { - super( ...arguments ); - this.onCategoriesChange = this.onCategoriesChange.bind( this ); - this.state = { - loading: true, - availableCategories: [], - }; - } - - buildCategoriesTree( flatCategories ) { - const categoriesByParent = groupBy( flatCategories, 'parent' ); - const fillWithChildren = ( categories ) => { - return categories.map( category => { - const children = categoriesByParent[ category.id ]; - return { - ...category, - children: children && children.length - ? fillWithChildren( children ) - : [], - }; - } ); - }; - - return fillWithChildren( categoriesByParent[ 0 ] || [] ); - } - - componentDidMount() { - this.fetchCategoriesRequest = new wp.api.collections.Categories() - .fetch( { data: DEFAULT_CATEGORIES_QUERY } ) - .done( ( categories ) => { - const availableCategories = this.buildCategoriesTree( categories ); - - this.setState( { - loading: false, - availableCategories, - } ); - } ) - .fail( ( xhr ) => { - if ( xhr.statusText === 'abort' ) { - return; - } - this.setState( { - loading: false, - } ); - } ); - } - - componentWillUnmount() { - if ( this.fetchCategoriesRequest ) { - this.fetchCategoriesRequest.abort(); - } - } - - onCategoriesChange( event ) { - const { onUpdateCategories, categories = [] } = this.props; - const categoryId = parseInt( event.target.value, 10 ); - const hasCategory = categories.indexOf( categoryId ) !== -1; - const newCategories = hasCategory - ? without( categories, categoryId ) - : [ ...categories, categoryId ]; - onUpdateCategories( newCategories ); - } - - renderCategories( renderedCategories ) { - const { categories = [] } = this.props; - return renderedCategories.map( ( category ) => { - const id = `editor-post-taxonomies-category-${ category.id }`; - return ( -
- - - { !! category.children.length && ( -
- { this.renderCategories( category.children ) } -
- ) } -
- ); - } ); - } - - render() { - const { availableCategories } = this.state; - - return ( -
-

{ __( 'Categories' ) }

- { this.renderCategories( availableCategories ) } -
- ); - } -} - -export default connect( - ( state ) => { - return { - categories: getEditedPostAttribute( state, 'categories' ), - }; - }, - { - onUpdateCategories( categories ) { - return editPost( { categories } ); - }, - } -)( CategoriesSelector ); - diff --git a/editor/sidebar/post-taxonomies/flat-taxonomy.js b/editor/sidebar/post-taxonomies/flat-taxonomy.js new file mode 100644 index 00000000000000..995aa17a965617 --- /dev/null +++ b/editor/sidebar/post-taxonomies/flat-taxonomy.js @@ -0,0 +1,189 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import { unescape, find, throttle } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; +import { FormTokenField } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { getEditedPostAttribute } from '../../selectors'; +import { editPost } from '../../actions'; + +const DEFAULT_QUERY = { + per_page: 100, + orderby: 'count', + order: 'desc', +}; +const MAX_TERMS_SUGGESTIONS = 20; + +class FlatTaxonomy 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: false, + availableTerms: [], + selectedTerms: [], + }; + } + + componentDidMount() { + if ( this.props.terms ) { + this.setState( { loading: false } ); + this.initRequest = this.fetchTerms( { include: this.props.terms } ); + this.initRequest.then( + () => { + this.setState( { loading: false } ); + }, + ( xhr ) => { + if ( xhr.statusText === 'abort' ) { + return; + } + this.setState( { + loading: false, + } ); + } + ); + } + this.searchTerms(); + } + + componentWillUnmount() { + this.initRequest.abort(); + if ( this.searchRequest ) { + this.searchRequest.abort(); + } + } + + componentWillReceiveProps( newProps ) { + if ( newProps.terms !== this.props.terms ) { + this.updateSelectedTerms( newProps.terms ); + } + } + + fetchTerms( params = {} ) { + const query = { ...DEFAULT_QUERY, ...params }; + const Collection = wp.api.getTaxonomyCollection( this.props.slug ); + const request = new Collection().fetch( { data: query } ); + request.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.map( ( termId ) => { + const termObject = find( this.state.availableTerms, ( term ) => term.id === termId ); + return termObject ? termObject.name : ''; + } ); + this.setState( { + selectedTerms, + } ); + } + + findOrCreateTerm( termName ) { + return new Promise( ( resolve, reject ) => { + // Tries to create a term or fetch it if it already exists + const Model = wp.api.getTaxonomyModel( this.props.slug ); + new Model( { name: termName } ).save() + .then( resolve, ( xhr ) => { + const errorCode = xhr.responseJSON && xhr.responseJSON.code; + if ( errorCode === 'term_exists' ) { + return new Model( { id: xhr.responseJSON.data } ) + .fetch().then( resolve, reject ); + } + reject( xhr ); + } ); + } ); + } + + onChange( termNames ) { + this.setState( { selectedTerms: termNames } ); + const newTermNames = termNames.filter( ( termName ) => + ! find( this.state.availableTerms, ( term ) => term.name === termName ) + ); + const termNamesToIds = ( names, availableTerms ) => { + return names + .map( ( termName ) => + find( availableTerms, ( term ) => term.name === termName ).id + ); + }; + + if ( newTermNames.length === 0 ) { + return this.props.onUpdateTerms( termNamesToIds( termNames, this.state.availableTerms ), this.props.restBase ); + } + Promise + .all( newTermNames.map( this.findOrCreateTerm ) ) + .then( ( newTerms ) => { + const newAvailableTerms = this.state.availableTerms.concat( newTerms ); + this.setState( { availableTerms: newAvailableTerms } ); + return this.props.onUpdateTerms( termNamesToIds( termNames, newAvailableTerms ), this.props.restBase ); + } ); + } + + searchTerms( search = '' ) { + if ( this.searchRequest ) { + this.searchRequest.abort(); + } + this.searchRequest = this.fetchTerms( { search } ); + } + + render() { + const { loading, availableTerms, selectedTerms } = this.state; + const { label } = this.props; + const termNames = availableTerms.map( ( term ) => term.name ); + + return ( +
+

{ label }

+ +
+ ); + } +} + +export default connect( + ( state, ownProps ) => { + return { + terms: getEditedPostAttribute( state, ownProps.restBase ), + }; + }, + ( dispatch ) => { + return { + onUpdateTerms( terms, restBase ) { + dispatch( editPost( { [ restBase ]: terms } ) ); + }, + }; + } +)( FlatTaxonomy ); + diff --git a/editor/sidebar/post-taxonomies/hierarchical-taxonomy.js b/editor/sidebar/post-taxonomies/hierarchical-taxonomy.js new file mode 100644 index 00000000000000..b7162e3cd5eb70 --- /dev/null +++ b/editor/sidebar/post-taxonomies/hierarchical-taxonomy.js @@ -0,0 +1,137 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import { unescape as unescapeString, without, groupBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { getEditedPostAttribute } from '../../selectors'; +import { editPost } from '../../actions'; + +const DEFAULT_QUERY = { + per_page: 100, + orderby: 'count', + order: 'desc', +}; + +class HierarchicalTaxonomy extends Component { + constructor() { + super( ...arguments ); + this.onChange = this.onChange.bind( this ); + this.state = { + loading: true, + availableTerms: [], + }; + } + + buildTermsTree( flatTerms ) { + const termsByParent = groupBy( flatTerms, 'parent' ); + const fillWithChildren = ( terms ) => { + return terms.map( ( term ) => { + const children = termsByParent[ term.id ]; + return { + ...term, + children: children && children.length + ? fillWithChildren( children ) + : [], + }; + } ); + }; + + return fillWithChildren( termsByParent[ 0 ] || [] ); + } + + componentDidMount() { + const Collection = wp.api.getTaxonomyCollection( this.props.slug ); + this.fetchRequest = new Collection() + .fetch( { data: DEFAULT_QUERY } ) + .done( ( terms ) => { + const availableTerms = this.buildTermsTree( terms ); + + this.setState( { + loading: false, + availableTerms, + } ); + } ) + .fail( ( xhr ) => { + if ( xhr.statusText === 'abort' ) { + return; + } + this.setState( { + loading: false, + } ); + } ); + } + + componentWillUnmount() { + this.fetchRequest.abort(); + } + + onChange( event ) { + const { onUpdateTerms, terms = [], restBase } = this.props; + const termId = parseInt( event.target.value, 10 ); + const hasTerm = terms.indexOf( termId ) !== -1; + const newTerms = hasTerm + ? without( terms, termId ) + : [ ...terms, termId ]; + onUpdateTerms( newTerms, restBase ); + } + + renderTerms( renderedTerms ) { + const { terms = [] } = this.props; + return renderedTerms.map( ( term ) => { + const id = `editor-post-taxonomies-hierarchical-term-${ term.id }`; + return ( +
+ + + { !! term.children.length && ( +
+ { this.renderTerms( term.children ) } +
+ ) } +
+ ); + } ); + } + + render() { + const { availableTerms } = this.state; + const { label } = this.props; + + return ( +
+

{ label }

+ { this.renderTerms( availableTerms ) } +
+ ); + } +} + +export default connect( + ( state, onwProps ) => { + return { + terms: getEditedPostAttribute( state, onwProps.restBase ), + }; + }, + { + onUpdateTerms( terms, restBase ) { + return editPost( { [ restBase ]: terms } ); + }, + } +)( HierarchicalTaxonomy ); + diff --git a/editor/sidebar/post-taxonomies/index.js b/editor/sidebar/post-taxonomies/index.js index 87d3afc69a4192..b36013d744859c 100644 --- a/editor/sidebar/post-taxonomies/index.js +++ b/editor/sidebar/post-taxonomies/index.js @@ -1,24 +1,74 @@ +/** + * External Dependencies + */ +import { connect } from 'react-redux'; + /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { PanelBody } from '@wordpress/components'; +import { Component } from '@wordpress/element'; /** * Internal dependencies */ import './style.scss'; -import TagsSelector from './tags-selector'; -import CategoriesSelector from './categories-selector'; - -function PostTaxonomies() { - return ( - - - - - ); +import HierarchicalTaxonomy from './hierarchical-taxonomy'; +import FlatTaxonomy from './flat-taxonomy'; +import { getCurrentPostType } from '../../selectors'; + +class PostTaxonomies extends Component { + constructor() { + super( ...arguments ); + this.state = { + taxonomies: [], + }; + } + + componentDidMount() { + this.fetchTaxonomies = new wp.api.collections.Taxonomies() + .fetch() + .done( ( taxonomies ) => { + this.setState( { taxonomies: Object.values( taxonomies ) } ); + } ); + } + + componentWillUnmout() { + this.fetchTaxonomies.abort(); + } + + render() { + const availableTaxonomies = this.state.taxonomies + .filter( ( taxonomy ) => taxonomy.types.indexOf( this.props.postType ) !== -1 ); + + if ( ! availableTaxonomies.length ) { + return null; + } + + return ( + + { availableTaxonomies.map( ( taxonomy ) => { + const TaxonomyComponent = taxonomy.hierarchical ? HierarchicalTaxonomy : FlatTaxonomy; + return ( + + ); + } ) } + + ); + } } -export default PostTaxonomies; +export default connect( + ( state ) => { + return { + postType: getCurrentPostType( state ), + }; + } +)( PostTaxonomies ); diff --git a/editor/sidebar/post-taxonomies/style.scss b/editor/sidebar/post-taxonomies/style.scss index 35f42e348c381c..78a1b8bcda27b7 100644 --- a/editor/sidebar/post-taxonomies/style.scss +++ b/editor/sidebar/post-taxonomies/style.scss @@ -1,23 +1,23 @@ -.editor-post-taxonomies__tags-selector, -.editor-post-taxonomies__categories-selector { +.editor-post-taxonomies__flat-terms-selector, +.editor-post-taxonomies__hierarchical-terms-selector { margin-top: $panel-padding; } -.editor-post-taxonomies__tags-selector-title, -.editor-post-taxonomies__categories-selector-title { +.editor-post-taxonomies__flat-terms-selector-title, +.editor-post-taxonomies__hierarchical-terms-selector-title { display: block; margin-bottom: 10px; } -.editor-post-taxonomies__categories-choice { +.editor-post-taxonomies__hierarchical-terms-choice { margin-bottom: 5px; } -.editor-post-taxonomies__categories-input[type=checkbox] { +.editor-post-taxonomies__hierarchical-terms-input[type=checkbox] { margin-top: 0; } -.editor-post-taxonomies__categories-subchoices { +.editor-post-taxonomies__hierarchical-terms-subchoices { margin-top: 5px; margin-left: $panel-padding; } diff --git a/editor/sidebar/post-taxonomies/tags-selector.js b/editor/sidebar/post-taxonomies/tags-selector.js deleted file mode 100644 index 78c3bb446d9a44..00000000000000 --- a/editor/sidebar/post-taxonomies/tags-selector.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * External dependencies - */ -import { connect } from 'react-redux'; -import { unescape, find, throttle } from 'lodash'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; -import { FormTokenField } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { getEditedPostAttribute } from '../../selectors'; -import { editPost } from '../../actions'; - -const DEFAULT_TAGS_QUERY = { - per_page: 100, - orderby: 'count', - order: 'desc', -}; -const MAX_TERMS_SUGGESTIONS = 20; - -class TagsSelector extends Component { - constructor() { - super( ...arguments ); - this.onTagsChange = this.onTagsChange.bind( this ); - this.searchTags = throttle( this.searchTags.bind( this ), 500 ); - this.findOrCreateTag = this.findOrCreateTag.bind( this ); - this.state = { - loading: false, - availableTags: [], - selectedTags: [], - }; - } - - componentDidMount() { - if ( this.props.tags ) { - this.setState( { loading: false } ); - this.initRequest = this.fetchTags( { include: this.props.tags } ); - this.initRequest.then( - () => { - this.setState( { loading: false } ); - }, - ( xhr ) => { - if ( xhr.statusText === 'abort' ) { - return; - } - this.setState( { - loading: false, - } ); - } - ); - } - this.searchTags(); - } - - componentWillUnmount() { - if ( this.initRequest ) { - this.initRequest.abort(); - } - if ( this.searchRequest ) { - this.searchRequest.abort(); - } - } - - componentWillReceiveProps( newProps ) { - if ( newProps.tags !== this.props.tags ) { - this.updateSelectedTags( newProps.tags ); - } - } - - fetchTags( params = {} ) { - const query = { ...DEFAULT_TAGS_QUERY, ...params }; - const request = new wp.api.collections.Tags().fetch( { data: query } ); - request.then( ( tags ) => { - this.setState( ( state ) => ( { - availableTags: state.availableTags.concat( - tags.filter( ( tag ) => ! find( state.availableTags, ( availableTag ) => availableTag.id === tag.id ) ) - ), - } ) ); - this.updateSelectedTags( this.props.tags ); - } ); - - return request; - } - - updateSelectedTags( tags = [] ) { - const selectedTags = tags.map( ( tagId ) => { - const tagObject = find( this.state.availableTags, ( tag ) => tag.id === tagId ); - return tagObject ? tagObject.name : ''; - } ); - this.setState( { - selectedTags, - } ); - } - - findOrCreateTag( tagName ) { - return new Promise( ( resolve, reject ) => { - // Tries to create a tag or fetch it if it already exists - new wp.api.models.Tag( { name: tagName } ).save() - .then( resolve, ( xhr ) => { - const errorCode = xhr.responseJSON && xhr.responseJSON.code; - if ( errorCode === 'term_exists' ) { - return new wp.api.models.Tag( { id: xhr.responseJSON.data } ) - .fetch().then( resolve, reject ); - } - reject( xhr ); - } ); - } ); - } - - onTagsChange( tagNames ) { - this.setState( { selectedTags: tagNames } ); - const newTagNames = tagNames.filter( ( tagName ) => - ! find( this.state.availableTags, ( tag ) => tag.name === tagName ) - ); - const tagNamesToIds = ( names, availableTags ) => { - return names - .map( ( tagName ) => - find( availableTags, ( tag ) => tag.name === tagName ).id - ); - }; - - if ( newTagNames.length === 0 ) { - return this.props.onUpdateTags( tagNamesToIds( tagNames, this.state.availableTags ) ); - } - Promise - .all( newTagNames.map( this.findOrCreateTag ) ) - .then( ( newTags ) => { - const newAvailableTags = this.state.availableTags.concat( newTags ); - this.setState( { availableTags: newAvailableTags } ); - return this.props.onUpdateTags( tagNamesToIds( tagNames, newAvailableTags ) ); - } ); - } - - searchTags( search = '' ) { - if ( this.searchRequest ) { - this.searchRequest.abort(); - } - this.searchRequest = this.fetchTags( { search } ); - } - - render() { - const { loading, availableTags, selectedTags } = this.state; - const tagNames = availableTags.map( ( tag ) => tag.name ); - - return ( -
-

{ __( 'Tags' ) }

- -
- ); - } -} - -export default connect( - ( state ) => { - return { - tags: getEditedPostAttribute( state, 'tags' ), - }; - }, - ( dispatch ) => { - return { - onUpdateTags( tags ) { - dispatch( editPost( { tags } ) ); - }, - }; - } -)( TagsSelector ); - diff --git a/lib/client-assets.php b/lib/client-assets.php index 6c702b1dd84b3a..1acd6337657330 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -356,12 +356,22 @@ function gutenberg_register_vendor_script( $handle, $src, $deps = array() ) { * @link https://core.trac.wordpress.org/ticket/41111 */ function gutenberg_extend_wp_api_backbone_client() { + // Post Types Mapping. $post_type_rest_base_mapping = array(); foreach ( get_post_types( array(), 'objects' ) as $post_type_object ) { $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; $post_type_rest_base_mapping[ $post_type_object->name ] = $rest_base; } + + // Taxonomies Mapping. + $taxonomy_rest_base_mapping = array(); + foreach ( get_taxonomies( array(), 'objects' ) as $taxonomy_object ) { + $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name; + $taxonomy_rest_base_mapping[ $taxonomy_object->name ] = $rest_base; + } + $script = sprintf( 'wp.api.postTypeRestBaseMapping = %s;', wp_json_encode( $post_type_rest_base_mapping ) ); + $script .= sprintf( 'wp.api.taxonomyRestBaseMapping = %s;', wp_json_encode( $taxonomy_rest_base_mapping ) ); $script .= <<[\\\\d]+)'; @@ -375,6 +385,18 @@ function gutenberg_extend_wp_api_backbone_client() { return model.prototype.route && route === model.prototype.route.index; } ) ); }; + wp.api.getTaxonomyModel = function( taxonomy ) { + var route = '/' + wpApiSettings.versionString + this.taxonomyRestBaseMapping[ taxonomy ] + '/(?P[\\\\d]+)'; + return _.first( _.filter( wp.api.models, function( model ) { + return model.prototype.route && route === model.prototype.route.index; + } ) ); + }; + wp.api.getTaxonomyCollection = function( taxonomy ) { + var route = '/' + wpApiSettings.versionString + this.taxonomyRestBaseMapping[ taxonomy ]; + return _.first( _.filter( wp.api.collections, function( model ) { + return model.prototype.route && route === model.prototype.route.index; + } ) ); + }; JS; wp_add_inline_script( 'wp-api', $script );