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

Chrome: Allow adding, removing tags from posts #970

Merged
merged 8 commits into from
Jun 6, 2017
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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
],
"env": {
"browser": false,
"es6": true,
"node": true,
"mocha": true
},
Expand Down
32 changes: 16 additions & 16 deletions components/form-token-field/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
margin: 0;
padding: 0;
background-color: $white;
border-radius: 4px;
border: 1px solid $light-gray-500;
color: $dark-gray-800;
color: $dark-gray-700;
cursor: text;
transition: all .15s ease-in-out;

Expand All @@ -28,7 +29,7 @@
display: flex;
flex-wrap: wrap;
align-items: flex-start;
padding: 5px 14px 5px 0;
padding: 4px;
}

// Token input
Expand All @@ -37,13 +38,13 @@ input[type="text"].components-form-token-field__input {
width: auto;
max-width: 100%;
margin: 2px 0 2px 8px;
padding: 0 0 0 6px;
padding: 0;
line-height: 24px;
background: inherit;
border: 0;
outline: none;
font-family: inherit;
font-size: 14px;
font-size: $default-font-size;
color: $dark-gray-800;
box-shadow: none;

Expand All @@ -54,10 +55,10 @@ input[type="text"].components-form-token-field__input {

// Tokens
.components-form-token-field__token {
font-size: 14px;
font-size: $default-font-size;
display: flex;
margin: 2px 0 2px 8px;
color: $white;
margin: 2px 4px 2px 0;
color: $dark-gray-700;
overflow: hidden;

&.is-success {
Expand Down Expand Up @@ -127,32 +128,31 @@ input[type="text"].components-form-token-field__input {
}

.components-form-token-field__token-text,
.components-form-token-field__remove-token {
.components-form-token-field__remove-token.components-icon-button {
display: inline-block;
line-height: 24px;
background: $dark-gray-500;
background: $light-gray-500;
transition: all .2s cubic-bezier( .4, 1, .4, 1 );
}

.components-form-token-field__token-text {
border-radius: 4px 0 0 4px;
padding: 0 4px 0 6px;
border-radius: 12px 0 0 12px;
padding: 0 4px 0 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.components-form-token-field__remove-token {
.components-form-token-field__remove-token.components-icon-button {
cursor: pointer;
border-radius: 0 4px 4px 0;
border-radius: 0 12px 12px 0;
padding: 0 2px;
font-size: 10px;
color: $light-gray-500;
color: $dark-gray-500;
line-height: 10px;

&:hover {
color: white;
background: $dark-gray-600;
color: $dark-gray-700;
}
}

Expand Down
2 changes: 1 addition & 1 deletion components/form-token-field/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function Token( {
</span>
<IconButton
className="components-form-token-field__remove-token"
icon="no-alt"
icon="dismiss"
onClick={ ! disabled && onClick }
/>
</span>
Expand Down
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { default as Button } from './button';
export { default as Dashicon } from './dashicon';
export { default as FormToggle } from './form-toggle';
export { default as FormTokenField } from './form-token-field';
export { default as HtmlEmbed } from './html-embed';
export { default as IconButton } from './icon-button';
export { default as Panel } from './panel';
Expand Down
116 changes: 106 additions & 10 deletions editor/sidebar/post-taxonomies/tags-selector.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,132 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';
import { unescape, find } from 'lodash';

/**
* WordPress dependencies
*/
import { Component } from 'element';
import FormTokenField from 'components/form-token-field';
import { FormTokenField } from 'components';
import { getEditedPostAttribute } from '../../selectors';
import { editPost } from '../../actions';

const DEFAULT_TAGS_QUERY = {
number: -1,
orderby: 'count',
order: 'DESC',
};
const MAX_TERMS_SUGGESTIONS = 20;

class TagsSelector extends Component {
constructor() {
super( ...arguments );
this.onTokensChange = this.onTokensChange.bind( this );
this.onTagsChange = this.onTagsChange.bind( this );
this.state = {
tokens: [ 'React', 'Vue' ],
loading: true,
availableTags: [],
selectedTags: [],
};
}

onTokensChange( value ) {
this.setState( { tokens: value } );
componentDidMount() {
this.fetchTagsRequest = new wp.api.collections.Tags().fetch( DEFAULT_TAGS_QUERY )
.done( ( tags ) => {
this.setState( {
loading: false,
availableTags: tags,
} );
this.updateSelectedTags( this.props.tags );
} )
.fail( ( xhr ) => {
if ( xhr.statusText === 'abort' ) {
return;
}
this.setState( {
loading: false,
} );
} );
}

componentWillUnmount() {
if ( this.fetchTagsRequest ) {
this.fetchTagsRequest.abort();
}
}

componentWillReceiveProps( newProps ) {
if ( newProps.tags !== this.props.tags ) {
this.updateSelectedTags( newProps.tags );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we need to updateSelectedTags on initial mount? Wouldn't a post loaded from save already have tags assigned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the component won't have the availableTags yet

}
}

updateSelectedTags( tags = [] ) {
const selectedTags = tags.map( ( tagId ) => {
const tagObject = find( this.state.availableTags, ( tag ) => tag.id === tagId );
return tagObject ? tagObject.name : '';
} );
this.setState( {
selectedTags,
} );
}

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we ever worry the find could return undefined here and throw an error on property access?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not at this moment, because we ensure all the tagNames are loaded before calling this.

);
};

if ( newTagNames.length === 0 ) {
return this.props.onUpdateTags( tagNamesToIds( tagNames, this.state.availableTags ) );
}
const createTag = ( tagName ) => new wp.api.models.Tag( { name: tagName } ).save();
Promise
.all( newTagNames.map( createTag ) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need or want to save these tags when they're added? AFAIK the post endpoint is happy to create these for us when the post is saved if they don't yet exist, and may actually be the preferred behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried but failed, which shape should these tags have to do this? The endpoint was giving me an error like tags should be integers (which means ids for me)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.then( ( newTags ) => {
const newAvailableTags = this.state.availableTags.concat( newTags );
this.setState( { availableTags: newAvailableTags } );
return this.props.onUpdateTags( tagNamesToIds( tagNames, newAvailableTags ) );
} );
}

render() {
const suggestions = [ 'React', 'Vue', 'Angular', 'Cycle', 'PReact', 'Inferno' ];
const { loading, availableTags, selectedTags } = this.state;
const tagNames = availableTags.map( ( tag ) => tag.name );

return (
<div className="editor-post-taxonomies__tags-selector">
<FormTokenField
value={ this.state.tokens }
suggestions={ suggestions }
onChange={ this.onTokensChange }
value={ selectedTags }
displayTransform={ unescape }
suggestions={ tagNames }
onChange={ this.onTagsChange }
maxSuggestions={ MAX_TERMS_SUGGESTIONS }
disabled={ loading }
/>
</div>
);
}
}

export default TagsSelector;
export default connect(
( state ) => {
return {
tags: getEditedPostAttribute( state, 'tags' ),
};
},
( dispatch ) => {
return {
onUpdateTags( tags ) {
dispatch( editPost( { tags } ) );
},
};
}
)( TagsSelector );