diff --git a/packages/editor/src/components/post-taxonomies/flat-term-selector.js b/packages/editor/src/components/post-taxonomies/flat-term-selector.js
index decc8953ec9c5..12d800c72da3f 100644
--- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js
+++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js
@@ -7,9 +7,7 @@ import {
get,
invoke,
isEmpty,
- map,
throttle,
- unescape as lodashUnescapeString,
uniqBy,
} from 'lodash';
@@ -18,12 +16,24 @@ import {
*/
import { __, _x, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
-import { FormTokenField, withFilters } from '@wordpress/components';
+import {
+ FormTokenField,
+ withFilters,
+ withSpokenMessages,
+} from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
import { compose } from '@wordpress/compose';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { store as editorStore } from '../../store';
+import { unescapeString, unescapeTerm, unescapeTerms } from '../../utils/terms';
+import MostUsedTerms from './most-used-terms';
+
/**
* Module constants
*/
@@ -32,42 +42,18 @@ const DEFAULT_QUERY = {
per_page: MAX_TERMS_SUGGESTIONS,
orderby: 'count',
order: 'desc',
- _fields: 'id,name',
+ _fields: 'id,name,count',
};
-// Lodash unescape function handles ' but not ' which may be return in some API requests.
-const unescapeString = ( arg ) => {
- return lodashUnescapeString( arg.replace( ''', "'" ) );
-};
const isSameTermName = ( termA, termB ) =>
unescapeString( termA ).toLowerCase() ===
unescapeString( 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 );
+const termNamesToIds = ( names, terms ) => {
+ return names.map(
+ ( termName ) =>
+ find( terms, ( term ) => isSameTermName( term.name, termName ) ).id
+ );
};
class FlatTermSelector extends Component {
@@ -76,6 +62,7 @@ class FlatTermSelector extends Component {
this.onChange = this.onChange.bind( this );
this.searchTerms = throttle( this.searchTerms.bind( this ), 500 );
this.findOrCreateTerm = this.findOrCreateTerm.bind( this );
+ this.appendTerm = this.appendTerm.bind( this );
this.state = {
loading: ! isEmpty( this.props.terms ),
availableTerms: [],
@@ -197,14 +184,6 @@ class FlatTermSelector extends Component {
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.onUpdateTerms(
@@ -233,6 +212,34 @@ class FlatTermSelector extends Component {
}
}
+ appendTerm( newTerm ) {
+ const { onUpdateTerms, taxonomy, terms = [], slug, speak } = this.props;
+
+ if ( terms.includes( newTerm.id ) ) {
+ return;
+ }
+
+ const newTerms = [ ...terms, newTerm.id ];
+
+ const termAddedMessage = sprintf(
+ /* translators: %s: term name. */
+ _x( '%s added', 'term' ),
+ get(
+ taxonomy,
+ [ 'labels', 'singular_name' ],
+ slug === 'post_tag' ? __( 'Tag' ) : __( 'Term' )
+ )
+ );
+
+ speak( termAddedMessage, 'assertive' );
+
+ this.setState( {
+ availableTerms: [ ...this.state.availableTerms, newTerm ],
+ } );
+
+ onUpdateTerms( newTerms, taxonomy.rest_base );
+ }
+
render() {
const { slug, taxonomy, hasAssignAction } = this.props;
@@ -269,28 +276,34 @@ class FlatTermSelector extends Component {
);
return (
-
+ <>
+
+
+ >
);
}
}
export default compose(
withSelect( ( select, { slug } ) => {
- const { getCurrentPost } = select( 'core/editor' );
- const { getTaxonomy } = select( 'core' );
+ const { getCurrentPost } = select( editorStore );
+ const { getTaxonomy } = select( coreStore );
const taxonomy = getTaxonomy( slug );
return {
hasCreateAction: taxonomy
@@ -308,7 +321,7 @@ export default compose(
)
: false,
terms: taxonomy
- ? select( 'core/editor' ).getEditedPostAttribute(
+ ? select( editorStore ).getEditedPostAttribute(
taxonomy.rest_base
)
: [],
@@ -318,9 +331,10 @@ export default compose(
withDispatch( ( dispatch ) => {
return {
onUpdateTerms( terms, restBase ) {
- dispatch( 'core/editor' ).editPost( { [ restBase ]: terms } );
+ dispatch( editorStore ).editPost( { [ restBase ]: terms } );
},
};
} ),
+ withSpokenMessages,
withFilters( 'editor.PostTaxonomyType' )
)( FlatTermSelector );
diff --git a/packages/editor/src/components/post-taxonomies/most-used-terms.js b/packages/editor/src/components/post-taxonomies/most-used-terms.js
new file mode 100644
index 0000000000000..da3539b05457e
--- /dev/null
+++ b/packages/editor/src/components/post-taxonomies/most-used-terms.js
@@ -0,0 +1,71 @@
+/**
+ * External dependencies
+ */
+import { get } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Button } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import { unescapeTerms } from '../../utils/terms';
+
+const MAX_MOST_USED_TERMS = 10;
+const DEFAULT_QUERY = {
+ per_page: MAX_MOST_USED_TERMS,
+ orderby: 'count',
+ order: 'desc',
+ _fields: 'id,name,count',
+};
+
+export default function MostUsedTerms( { onSelect, taxonomy } ) {
+ const { _terms, showTerms } = useSelect( ( select ) => {
+ const mostUsedTerms = select( coreStore ).getEntityRecords(
+ 'taxonomy',
+ taxonomy.slug,
+ DEFAULT_QUERY
+ );
+ return {
+ _terms: mostUsedTerms,
+ showTerms: mostUsedTerms?.length >= MAX_MOST_USED_TERMS,
+ };
+ }, [] );
+
+ if ( ! showTerms ) {
+ return null;
+ }
+
+ const terms = unescapeTerms( _terms );
+ const label = get( taxonomy, [ 'labels', 'most_used' ] );
+
+ return (
+
+
+ { label }
+
+ { /*
+ * Disable reason: The `list` ARIA role is redundant but
+ * Safari+VoiceOver won't announce the list otherwise.
+ */
+ /* eslint-disable jsx-a11y/no-redundant-roles */ }
+
+ { terms.map( ( term ) => (
+ -
+
+
+ ) ) }
+
+ { /* eslint-enable jsx-a11y/no-redundant-roles */ }
+
+ );
+}
diff --git a/packages/editor/src/components/post-taxonomies/style.scss b/packages/editor/src/components/post-taxonomies/style.scss
index 232ac77059769..089c549111b4d 100644
--- a/packages/editor/src/components/post-taxonomies/style.scss
+++ b/packages/editor/src/components/post-taxonomies/style.scss
@@ -36,3 +36,23 @@
margin-bottom: 8px;
width: 100%;
}
+
+.editor-post-taxonomies__flat-term-most-used {
+ .editor-post-taxonomies__flat-term-most-used-label {
+ font-weight: 400;
+ margin-bottom: $grid-unit-15;
+ }
+}
+
+.editor-post-taxonomies__flat-term-most-used-list {
+ margin: 0;
+
+ li {
+ display: inline-block;
+ margin-right: $grid-unit-10;
+ }
+
+ .components-button {
+ font-size: 12px;
+ }
+}
diff --git a/packages/editor/src/utils/terms.js b/packages/editor/src/utils/terms.js
index 60cfdd8563d0b..75c37b2714efa 100644
--- a/packages/editor/src/utils/terms.js
+++ b/packages/editor/src/utils/terms.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { groupBy } from 'lodash';
+import { groupBy, map, unescape as lodashUnescapeString } from 'lodash';
/**
* Returns terms in a tree form.
@@ -38,3 +38,35 @@ export function buildTermsTree( flatTerms ) {
return fillWithChildren( termsByParent[ '0' ] || [] );
}
+
+// Lodash unescape function handles ' but not ' which may be return in some API requests.
+export const unescapeString = ( arg ) => {
+ return lodashUnescapeString( arg.replace( ''', "'" ) );
+};
+
+/**
+ * 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.
+ */
+export 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.
+ */
+export const unescapeTerms = ( terms ) => {
+ return map( terms, unescapeTerm );
+};