diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index ebbc303649e17..37709525c59d3 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1769,15 +1769,6 @@ Returns an action object used to signal that post saving is unlocked. * lockName: The lock name. -### addTermToEditedPost - -Returns an action object signaling that a new term is added to the edited post. - -*Parameters* - - * slug: Taxonomy slug. - * term: Term object. - ### createNotice ### fetchReusableBlocks \ No newline at end of file diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js index 7d3e60d17dcf2..419af0dd4f5c9 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, unescape as unescapeString, without, find, some } from 'lodash'; +import { get, unescape as unescapeString, without, find, some, invoke } from 'lodash'; /** * WordPress dependencies @@ -11,6 +11,8 @@ import { Component } from '@wordpress/element'; import { TreeSelect, withSpokenMessages, withFilters, Button } from '@wordpress/components'; import { withSelect, withDispatch } from '@wordpress/data'; import { withInstanceId, compose } from '@wordpress/compose'; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -24,6 +26,7 @@ const DEFAULT_QUERY = { per_page: -1, orderby: 'name', order: 'asc', + _fields: 'id,name,parent', }; const MIN_TERMS_COUNT_FOR_FILTER = 8; @@ -38,7 +41,12 @@ class HierarchicalTermSelector extends Component { this.onAddTerm = this.onAddTerm.bind( this ); this.onToggleForm = this.onToggleForm.bind( this ); this.setFilterValue = this.setFilterValue.bind( this ); + this.sortBySelected = this.sortBySelected.bind( this ); this.state = { + loading: true, + availableTermsTree: [], + availableTerms: [], + adding: false, formName: '', formParent: '', showForm: false, @@ -48,13 +56,13 @@ class HierarchicalTermSelector extends Component { } onChange( event ) { - const { onUpdateTerms, terms = [] } = this.props; + const { onUpdateTerms, terms = [], taxonomy } = 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 ); + onUpdateTerms( newTerms, taxonomy.rest_base ); } onChangeFormName( event ) { @@ -81,9 +89,9 @@ class HierarchicalTermSelector extends Component { onAddTerm( event ) { event.preventDefault(); - const { onUpdateTerms, addTermToEditedPost, terms, availableTerms } = this.props; - const { formName, formParent } = this.state; - if ( formName === '' ) { + const { onUpdateTerms, taxonomy, terms, slug } = this.props; + const { formName, formParent, adding, availableTerms } = this.state; + if ( formName === '' || adding ) { return; } @@ -92,7 +100,7 @@ class HierarchicalTermSelector extends Component { if ( existingTerm ) { // if the term we are adding exists but is not selected select it if ( ! some( terms, ( term ) => term === existingTerm.id ) ) { - onUpdateTerms( [ ...terms, existingTerm.id ] ); + onUpdateTerms( [ ...terms, existingTerm.id ], taxonomy.rest_base ); } this.setState( { formName: '', @@ -101,14 +109,113 @@ class HierarchicalTermSelector extends Component { return; } - addTermToEditedPost( { - name: formName, - parent: formParent ? formParent : undefined, - } ); this.setState( { - formName: '', - formParent: '', + adding: true, + } ); + this.addRequest = apiFetch( { + path: `/wp/v2/${ taxonomy.rest_base }`, + method: 'POST', + data: { + name: formName, + parent: formParent ? formParent : undefined, + }, + } ); + // Tries to create a term or fetch it if it already exists + const findOrCreatePromise = this.addRequest + .catch( ( error ) => { + const errorCode = error.code; + if ( errorCode === 'term_exists' ) { + // search the new category created since last fetch + this.addRequest = apiFetch( { + path: addQueryArgs( + `/wp/v2/${ taxonomy.rest_base }`, + { ...DEFAULT_QUERY, parent: formParent || 0, search: formName } + ), + } ); + return this.addRequest + .then( ( searchResult ) => { + return this.findTerm( searchResult, formParent, formName ); + } ); + } + return Promise.reject( error ); + } ); + findOrCreatePromise + .then( ( term ) => { + const hasTerm = !! find( this.state.availableTerms, ( availableTerm ) => availableTerm.id === term.id ); + const newAvailableTerms = hasTerm ? this.state.availableTerms : [ term, ...this.state.availableTerms ]; + const termAddedMessage = sprintf( + _x( '%s added', 'term' ), + get( + this.props.taxonomy, + [ 'labels', 'singular_name' ], + slug === 'category' ? __( 'Category' ) : __( 'Term' ) + ) + ); + this.props.speak( termAddedMessage, 'assertive' ); + this.addRequest = null; + this.setState( { + adding: false, + formName: '', + formParent: '', + availableTerms: newAvailableTerms, + availableTermsTree: this.sortBySelected( buildTermsTree( newAvailableTerms ) ), + } ); + onUpdateTerms( [ ...terms, term.id ], taxonomy.rest_base ); + }, ( xhr ) => { + if ( xhr.statusText === 'abort' ) { + return; + } + this.addRequest = null; + this.setState( { + adding: false, + } ); + } ); + } + + componentDidMount() { + this.fetchTerms(); + } + + componentWillUnmount() { + invoke( this.fetchRequest, [ 'abort' ] ); + invoke( this.addRequest, [ 'abort' ] ); + } + + componentDidUpdate( prevProps ) { + if ( this.props.taxonomy !== prevProps.taxonomy ) { + this.fetchTerms(); + } + } + + fetchTerms() { + const { taxonomy } = this.props; + if ( ! taxonomy ) { + return; + } + this.fetchRequest = apiFetch( { + path: addQueryArgs( `/wp/v2/${ taxonomy.rest_base }`, DEFAULT_QUERY ), } ); + this.fetchRequest.then( + ( terms ) => { // resolve + const availableTermsTree = this.sortBySelected( buildTermsTree( terms ) ); + + this.fetchRequest = null; + this.setState( { + loading: false, + availableTermsTree, + availableTerms: terms, + } ); + }, + ( xhr ) => { // reject + if ( xhr.statusText === 'abort' ) { + return; + } + this.fetchRequest = null; + this.setState( { + loading: false, + } ); + } + ); } sortBySelected( termsTree ) { @@ -149,7 +256,7 @@ class HierarchicalTermSelector extends Component { } setFilterValue( event ) { - const { availableTermsTree } = this.props; + const { availableTermsTree } = this.state; const filterValue = event.target.value; const filteredTermsTree = availableTermsTree.map( this.getFilterMatcher( filterValue ) ).filter( ( term ) => term ); const getResultCount = ( terms ) => { @@ -162,10 +269,12 @@ class HierarchicalTermSelector extends Component { } return count; }; - this.setState( { - filterValue, - filteredTermsTree, - } ); + this.setState( + { + filterValue, + filteredTermsTree, + } + ); const resultCount = getResultCount( filteredTermsTree ); const resultsFoundMessage = sprintf( @@ -230,21 +339,13 @@ class HierarchicalTermSelector extends Component { } render() { - const { - slug, - taxonomy, - instanceId, - hasCreateAction, - hasAssignAction, - availableTermsTree, - availableTerms, - } = this.props; + const { slug, taxonomy, instanceId, hasCreateAction, hasAssignAction } = this.props; if ( ! hasAssignAction ) { return null; } - const { filteredTermsTree, formName, formParent, isRequestingTerms, showForm, filterValue } = this.state; + const { availableTermsTree, availableTerms, filteredTermsTree, formName, formParent, loading, showForm, filterValue } = this.state; const labelWithFallback = ( labelProperty, fallbackIsCategory, fallbackIsNotCategory ) => get( taxonomy, [ 'labels', labelProperty ], @@ -285,7 +386,7 @@ class HierarchicalTermSelector extends Component { slug === 'category' ? __( 'Categories' ) : __( 'Terms' ) ) ); - const showFilter = availableTerms && ( availableTerms.length >= MIN_TERMS_COUNT_FOR_FILTER ); + const showFilter = availableTerms.length >= MIN_TERMS_COUNT_FOR_FILTER; return [ showFilter &&