Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor: Display ten most used terms #30598

Merged
merged 6 commits into from
May 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 73 additions & 59 deletions packages/editor/src/components/post-taxonomies/flat-term-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {
get,
invoke,
isEmpty,
map,
throttle,
unescape as lodashUnescapeString,
uniqBy,
} from 'lodash';

Expand All @@ -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
*/
Expand All @@ -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 {
Expand All @@ -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: [],
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -269,28 +276,34 @@ class FlatTermSelector extends Component {
);

return (
<FormTokenField
value={ selectedTerms }
suggestions={ termNames }
onChange={ this.onChange }
onInputChange={ this.searchTerms }
maxSuggestions={ MAX_TERMS_SUGGESTIONS }
disabled={ loading }
label={ newTermLabel }
messages={ {
added: termAddedLabel,
removed: termRemovedLabel,
remove: removeTermLabel,
} }
/>
<>
<FormTokenField
value={ selectedTerms }
suggestions={ termNames }
onChange={ this.onChange }
onInputChange={ this.searchTerms }
maxSuggestions={ MAX_TERMS_SUGGESTIONS }
disabled={ loading }
label={ newTermLabel }
messages={ {
added: termAddedLabel,
removed: termRemovedLabel,
remove: removeTermLabel,
} }
/>
<MostUsedTerms
taxonomy={ taxonomy }
onSelect={ this.appendTerm }
/>
</>
);
}
}

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
Expand All @@ -308,7 +321,7 @@ export default compose(
)
: false,
terms: taxonomy
? select( 'core/editor' ).getEditedPostAttribute(
? select( editorStore ).getEditedPostAttribute(
taxonomy.rest_base
)
: [],
Expand All @@ -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 );
71 changes: 71 additions & 0 deletions packages/editor/src/components/post-taxonomies/most-used-terms.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="editor-post-taxonomies__flat-term-most-used">
<h3 className="editor-post-taxonomies__flat-term-most-used-label">
{ label }
</h3>
{ /*
* Disable reason: The `list` ARIA role is redundant but
* Safari+VoiceOver won't announce the list otherwise.
*/
/* eslint-disable jsx-a11y/no-redundant-roles */ }
<ul
role="list"
className="editor-post-taxonomies__flat-term-most-used-list"
>
{ terms.map( ( term ) => (
<li key={ term.id }>
<Button isLink onClick={ () => onSelect( term ) }>
{ term.name }
</Button>
</li>
) ) }
</ul>
{ /* eslint-enable jsx-a11y/no-redundant-roles */ }
</div>
);
}
20 changes: 20 additions & 0 deletions packages/editor/src/components/post-taxonomies/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
34 changes: 33 additions & 1 deletion packages/editor/src/utils/terms.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { groupBy } from 'lodash';
import { groupBy, map, unescape as lodashUnescapeString } from 'lodash';

/**
* Returns terms in a tree form.
Expand Down Expand Up @@ -38,3 +38,35 @@ export function buildTermsTree( flatTerms ) {

return fillWithChildren( termsByParent[ '0' ] || [] );
}

// Lodash unescape function handles &#39; but not &#039; which may be return in some API requests.
export const unescapeString = ( arg ) => {
return lodashUnescapeString( arg.replace( '&#039;', "'" ) );
};

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