From d3673a3c2f3af00e538f276b406c8a6dbc16f0b8 Mon Sep 17 00:00:00 2001 From: srallen Date: Wed, 12 Apr 2017 09:33:50 -0500 Subject: [PATCH] Fav button into ES6 (#3717) * Convert favorites button to ES6. Add unit tests * Fix TypeError when favorites collection doesnt have subjects --- app/collections/favorites-button.cjsx | 123 --------------- app/collections/favorites-button.jsx | 184 +++++++++++++++++++++++ app/collections/favorites-button.spec.js | 41 +++++ app/collections/get-favorites-name.cjsx | 7 - app/collections/show-list.cjsx | 2 +- app/components/subject-viewer.cjsx | 2 +- 6 files changed, 227 insertions(+), 132 deletions(-) delete mode 100644 app/collections/favorites-button.cjsx create mode 100644 app/collections/favorites-button.jsx create mode 100644 app/collections/favorites-button.spec.js delete mode 100644 app/collections/get-favorites-name.cjsx diff --git a/app/collections/favorites-button.cjsx b/app/collections/favorites-button.cjsx deleted file mode 100644 index d4879f6fe6..0000000000 --- a/app/collections/favorites-button.cjsx +++ /dev/null @@ -1,123 +0,0 @@ -React = require 'react' -apiClient = require 'panoptes-client/lib/api-client' -getFavoritesName = require './get-favorites-name' -alert = require '../lib/alert' -SignInPrompt = require '../partials/sign-in-prompt' - -module.exports = React.createClass - displayName: 'CollectionFavoritesButton' - - propTypes: - subject: React.PropTypes.object # a subject response from panoptes - project: React.PropTypes.object # a project response from panoptes - user: React.PropTypes.object - isFavorite: React.PropTypes.bool - - getDefaultProps: -> - subject: null - project: null - user: null - isFavorite: false - - getInitialState: -> - favorites: null - favorited: false - - contextTypes: - geordi: React.PropTypes.object - - logSubjLike: (liked) -> - @context.geordi?.logEvent - type: liked - - promptToSignIn: -> - alert (resolve) -> - -

You must be signed in to save your favorites.

-
- - findFavoriteCollection: -> - apiClient.type('collections') - .get({project_ids: @props.project?.id, favorite: true, owner: @props.user.login}) - .then ([favorites]) -> if favorites? then favorites else null - - componentWillMount: -> - @favoriteSubject(@props.isFavorite) - - componentDidUpdate: (prevProps) -> - if prevProps.subject isnt @props.subject - @favoriteSubject(@props.isFavorite) - - componentWillReceiveProps: (nextProps) -> - if @props.isFavorite isnt nextProps.isFavorite - @favoriteSubject(nextProps.isFavorite) - - favoriteSubject: (isFavorite) -> - favorited = isFavorite - @setState {favorited} - - addSubjectTo: (collection) -> - @setState favorited: true - collection.addLink('subjects', [@props.subject.id.toString()]) - - removeSubjectFrom: (collection) -> - @setState favorited: false - collection.removeLink('subjects', [@props.subject.id.toString()]) - - createFavorites: -> - new Promise (resolve, reject) => - @findFavoriteCollection() - .catch (err) -> - reject err - .then (favorites) => - if favorites? - @setState {favorites} - resolve favorites - else - display_name = getFavoritesName(@props.project) - project = @props.project?.id - subjects = [] - favorite = true - - links = {subjects} - links.projects = [ project ] if project? - collection = {favorite, display_name, links} - apiClient.type('collections') - .create(collection) - .save() - .catch (err) -> - reject err - .then (favorites) => - @setState {favorites} - resolve favorites - - toggleFavorite: -> - if @props.user? - if not @state.favorites? - @setState favorited: true - @createFavorites() - .then (favorites) => - @addSubjectTo(favorites) - @logSubjLike 'favorite' - else if @state.favorited - @removeSubjectFrom(@state.favorites) - @logSubjLike 'unfavorite' - else - @addSubjectTo(@state.favorites) - @logSubjLike 'favorite' - else - @promptToSignIn() - @logSubjLike 'favorite' - - render: -> - diff --git a/app/collections/favorites-button.jsx b/app/collections/favorites-button.jsx new file mode 100644 index 0000000000..f9292c1dbe --- /dev/null +++ b/app/collections/favorites-button.jsx @@ -0,0 +1,184 @@ +import React from 'react'; +import apiClient from 'panoptes-client/lib/api-client'; +import classnames from 'classnames'; +import alert from '../lib/alert'; +import SignInPrompt from '../partials/sign-in-prompt'; + +export default class FavoritesButton extends React.Component { + constructor(props) { + super(props); + + this.state = { + favorites: null, + favorited: false + }; + + this.findFavoriteCollection = this.findFavoriteCollection.bind(this); + this.favoriteSubject = this.favoriteSubject.bind(this); + this.addSubjectTo = this.addSubjectTo.bind(this); + this.removeSubjectFrom = this.removeSubjectFrom.bind(this); + this.logSubjLike = this.logSubjLike.bind(this); + this.createFavorites = this.createFavorites.bind(this); + this.toggleFavorite = this.toggleFavorite.bind(this); + } + + componentWillMount() { + this.favoriteSubject(this.props.isFavorite); + } + + componentWillReceiveProps(nextProps) { + if (this.props.isFavorite !== nextProps.isFavorite) { + this.favoriteSubject(nextProps.isFavorite); + } + } + + componentDidUpdate(prevProps) { + if (prevProps.subject !== this.props.subject) { + this.favoriteSubject(this.props.isFavorite); + } + } + + promptToSignIn() { + alert((resolve) => { + return ( + +

You must be signed in to save your favorites.

+
+ ); + }); + } + + findFavoriteCollection() { + return apiClient.type('collections') + .get({ project_ids: this.props.project.id, favorite: true, owner: this.props.user.login }) + .then(([favorites]) => { return (favorites || null); }); + } + + favoriteSubject(isFavorite) { + const favorited = isFavorite; + this.setState({ favorited }); + } + + addSubjectTo(collection) { + this.setState({ favorited: true }); + collection.addLink('subjects', [this.props.subject.id.toString()]); + } + + removeSubjectFrom(collection) { + this.setState({ favorited: false }); + collection.removeLink('subjects', [this.props.subject.id.toString()]); + } + + getFavoritesName(project) { + if (project) { + return `Favorites ${project.slug}`; + } + + return 'Favorites'; + } + + logSubjLike(liked) { + if (this.context.geordi) { + this.context.geordi.logEvent({ type: liked }); + } + } + + createFavorites() { + return ( + new Promise((resolve, reject) => { + this.findFavoriteCollection() + .catch((err) => { reject(err); }) + .then((favorites) => { + if (favorites) { + this.setState({ favorites }); + resolve(favorites); + } else { + const display_name = this.getFavoritesName(this.props.project); + const project = this.props.project.id; + const subjects = []; + const favorite = true; + + const links = { subjects }; + if (project) { + links.projects = [project]; + } + const collection = { favorite, display_name, links }; + apiClient.type('collections') + .create(collection) + .save() + .catch((err) => { reject(err); }) + .then((newFavorites) => { + this.setState({ favorites: newFavorites }); + resolve(newFavorites); + }); + } + }); + }) + ); + } + + toggleFavorite() { + if (this.props.user) { + if (!this.state.favorites) { + this.setState({ favorited: true }); + this.createFavorites() + .then((favorites) => { this.addSubjectTo(favorites); }); + this.logSubjLike('favorite'); + } else if (this.state.favorited) { + this.removeSubjectFrom(this.state.favorites); + this.logSubjLike('unfavorite'); + } else { + this.addSubjectTo(this.state.favorites); + this.logSubjLike('favorite'); + } + } else { + this.promptToSignIn(); + this.logSubjLike('favorite'); + } + } + + render() { + const iconClasses = classnames({ + 'favorited': this.state.favorited, + 'fa fa-heart': this.state.favorited, + 'fa fa-heart-o': !this.state.favorited, + 'fa-fw': true + }); + + return ( + + ); + } +} + +FavoritesButton.defaultProps = { + isFavorite: false, + subject: { id: '' }, + project: null, + user: null +}; + +FavoritesButton.propTypes = { + className: React.PropTypes.string, + isFavorite: React.PropTypes.bool, + subject: React.PropTypes.shape({ id: React.PropTypes.string }).isRequired, + project: React.PropTypes.shape({ + id: React.PropTypes.string, + slug: React.PropTypes.string + }), + user: React.PropTypes.shape({ + login: React.PropTypes.string + }) +}; + +FavoritesButton.contextTypes = { + geordi: React.PropTypes.object +}; diff --git a/app/collections/favorites-button.spec.js b/app/collections/favorites-button.spec.js new file mode 100644 index 0000000000..cd9c89e3f1 --- /dev/null +++ b/app/collections/favorites-button.spec.js @@ -0,0 +1,41 @@ +import React from 'react'; +import assert from 'assert'; +import sinon from 'sinon'; +import { shallow } from 'enzyme'; +import FavoritesButton from './favorites-button'; + +const subject = { id: '4' }; + +describe('', function() { + let wrapper; + let toggleFavoriteSpy; + let button; + let icon; + before(function() { + toggleFavoriteSpy = sinon.spy(FavoritesButton.prototype, 'toggleFavorite'); + wrapper = shallow(); + button = wrapper.find('button'); + icon = wrapper.find('i'); + }); + + it('should render a button and an icon', function() { + assert.equal(button.length, 1); + assert.equal(icon.length, 1); + }); + + it('should render an empty heart icon if subject is not a favorite', function() { + assert.equal(icon.props().className.includes('fa-heart-o'), true); + assert.equal(wrapper.state('favorited'), false); + }); + + it('should render a filled heart icon if subject is a favorite', function() { + wrapper.setProps({ isFavorite: true }); + assert.equal(icon.props().className.includes('fa-heart'), true); + assert.equal(wrapper.state('favorited'), true); + }); + + it('should call toggleFavorite on click', function() { + button.simulate('click'); + sinon.assert.calledOnce(toggleFavoriteSpy); + }); +}); \ No newline at end of file diff --git a/app/collections/get-favorites-name.cjsx b/app/collections/get-favorites-name.cjsx deleted file mode 100644 index 1c1b568108..0000000000 --- a/app/collections/get-favorites-name.cjsx +++ /dev/null @@ -1,7 +0,0 @@ -# Get the name of the favorites collection for a project -module.exports = (project) -> - #--> Naming Convention: "Favorites for #{project_id}" - # Prevents global uniqueness validation conflict under project scope - name = "Favorites" - name += " (#{project.slug})" if project? - name diff --git a/app/collections/show-list.cjsx b/app/collections/show-list.cjsx index 682e860a7b..33a254f0eb 100644 --- a/app/collections/show-list.cjsx +++ b/app/collections/show-list.cjsx @@ -61,7 +61,7 @@ SubjectNode = React.createClass apiClient.type('collections').get(query) .then ([favoritesCollection]) => - if favoritesCollection? + if favoritesCollection? and favoritesCollection.links.subjects? isFavorite = @props.subject.id in favoritesCollection.links.subjects @setState({ isFavorite }) diff --git a/app/components/subject-viewer.cjsx b/app/components/subject-viewer.cjsx index 23d060e564..01d19ced56 100644 --- a/app/components/subject-viewer.cjsx +++ b/app/components/subject-viewer.cjsx @@ -1,5 +1,5 @@ React = require 'react' -FavoritesButton = require '../collections/favorites-button' +`import FavoritesButton from '../collections/favorites-button';` Dialog = require 'modal-form/dialog' {Markdown} = require 'markdownz' classnames = require 'classnames'