diff options
Diffstat (limited to 'web/react/components')
102 files changed, 8131 insertions, 4414 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index a19e5c16e..a080150dd 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -4,48 +4,49 @@ var UserStore = require('../stores/user_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var LoadingScreen = require('./loading_screen.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -function getStateFromStoresForAudits() { - return { - audits: UserStore.getAudits() - }; -} +export default class AccessHistoryModal extends React.Component { + constructor(props) { + super(props); + + this.onAuditChange = this.onAuditChange.bind(this); + this.handleMoreInfo = this.handleMoreInfo.bind(this); -module.exports = React.createClass({ - displayName: 'AccessHistoryModal', - componentDidMount: function() { - UserStore.addAuditsChangeListener(this.onListenerChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function() { + this.state = this.getStateFromStoresForAudits(); + this.state.moreInfo = []; + } + getStateFromStoresForAudits() { + return { + audits: UserStore.getAudits() + }; + } + componentDidMount() { + UserStore.addAuditsChangeListener(this.onAuditChange); + $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function show() { AsyncClient.getAudits(); }); - var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function() { + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function hide() { $('#user_settings').modal('show'); - self.setState({moreInfo: []}); - }); - }, - componentWillUnmount: function() { - UserStore.removeAuditsChangeListener(this.onListenerChange); - }, - onListenerChange: function() { - var newState = getStateFromStoresForAudits(); - if (!utils.areStatesEqual(newState.audits, this.state.audits)) { + this.setState({moreInfo: []}); + }.bind(this)); + } + componentWillUnmount() { + UserStore.removeAuditsChangeListener(this.onAuditChange); + } + onAuditChange() { + var newState = this.getStateFromStoresForAudits(); + if (!Utils.areStatesEqual(newState.audits, this.state.audits)) { this.setState(newState); } - }, - handleMoreInfo: function(index) { + } + handleMoreInfo(index) { var newMoreInfo = this.state.moreInfo; newMoreInfo[index] = true; this.setState({moreInfo: newMoreInfo}); - }, - getInitialState: function() { - var initialState = getStateFromStoresForAudits(); - initialState.moreInfo = []; - return initialState; - }, - render: function() { + } + render() { var accessList = []; var currentHistoryDate = null; @@ -63,7 +64,16 @@ module.exports = React.createClass({ currentAudit.session_id = 'N/A (Login attempt)'; } - var moreInfo = (<a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); + var moreInfo = ( + <a + href='#' + className='theme' + onClick={this.handleMoreInfo.bind(this, i)} + > + More info + </a> + ); + if (this.state.moreInfo[i]) { moreInfo = ( <div> @@ -75,11 +85,14 @@ module.exports = React.createClass({ var divider = null; if (i < this.state.audits.length - 1) { - divider = (<div className='divider-light'></div>) + divider = (<div className='divider-light'></div>); } accessList[i] = ( - <div className='access-history__table'> + <div + key={'accessHistoryEntryKey' + i} + className='access-history__table' + > <div className='access__date'>{newDate}</div> <div className='access__report'> <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div> @@ -102,14 +115,36 @@ module.exports = React.createClass({ return ( <div> - <div className='modal fade' ref='modal' id='access-history' tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade' + ref='modal' + id='access-history' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog modal-lg'> <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' id='myModalLabel'>Access History</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + > + Access History + </h4> </div> - <div ref='modalBody' className='modal-body'> + <div + ref='modalBody' + className='modal-body' + > {content} </div> </div> @@ -118,4 +153,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 1192a72bc..7cbd4021e 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -1,136 +1,170 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var LoadingScreen = require('./loading_screen.jsx'); -var utils = require('../utils/utils.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const LoadingScreen = require('./loading_screen.jsx'); +const Utils = require('../utils/utils.jsx'); -function getStateFromStoresForSessions() { - return { - sessions: UserStore.getSessions(), - serverError: null, - clientError: null - }; -} +export default class ActivityLogModal extends React.Component { + constructor(props) { + super(props); + + this.submitRevoke = this.submitRevoke.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + this.handleMoreInfo = this.handleMoreInfo.bind(this); -module.exports = React.createClass({ - displayName: 'ActivityLogModal', - submitRevoke: function(altId) { + this.state = this.getStateFromStores(); + this.state.moreInfo = []; + } + getStateFromStores() { + return { + sessions: UserStore.getSessions(), + serverError: null, + clientError: null + }; + } + submitRevoke(altId) { Client.revokeSession(altId, - function(data) { + function handleRevokeSuccess() { AsyncClient.getSessions(); - }.bind(this), - function(err) { - var state = getStateFromStoresForSessions(); + }, + function handleRevokeError(err) { + let state = this.getStateFromStores(); state.serverError = err; this.setState(state); }.bind(this) ); - }, - componentDidMount: function() { + } + componentDidMount() { UserStore.addSessionsChangeListener(this.onListenerChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { + $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function handleShow() { AsyncClient.getSessions(); }); - var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function handleHide() { $('#user_settings').modal('show'); - self.setState({moreInfo: []}); - }); - }, - componentWillUnmount: function() { + this.setState({moreInfo: []}); + }.bind(this)); + } + componentWillUnmount() { UserStore.removeSessionsChangeListener(this.onListenerChange); - }, - onListenerChange: function() { - var newState = getStateFromStoresForSessions(); - if (!utils.areStatesEqual(newState.sessions, this.state.sessions)) { + } + onListenerChange() { + const newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(newState.sessions, this.state.sessions)) { this.setState(newState); } - }, - handleMoreInfo: function(index) { - var newMoreInfo = this.state.moreInfo; + } + handleMoreInfo(index) { + let newMoreInfo = this.state.moreInfo; newMoreInfo[index] = true; this.setState({moreInfo: newMoreInfo}); - }, - getInitialState: function() { - var initialState = getStateFromStoresForSessions(); - initialState.moreInfo = []; - return initialState; - }, - render: function() { - var activityList = []; - var serverError = this.state.serverError; - - // Squash any false-y value for server error into null - if (!serverError) { - serverError = null; - } + } + render() { + let activityList = []; - for (var i = 0; i < this.state.sessions.length; i++) { - var currentSession = this.state.sessions[i]; - var lastAccessTime = new Date(currentSession.last_activity_at); - var firstAccessTime = new Date(currentSession.create_at); - var devicePicture = ''; + for (let i = 0; i < this.state.sessions.length; i++) { + const currentSession = this.state.sessions[i]; + const lastAccessTime = new Date(currentSession.last_activity_at); + const firstAccessTime = new Date(currentSession.create_at); + let devicePicture = ''; if (currentSession.props.platform === 'Windows') { devicePicture = 'fa fa-windows'; - } - else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') { + } else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') { devicePicture = 'fa fa-apple'; - } - else if (currentSession.props.platform === 'Linux') { + } else if (currentSession.props.platform === 'Linux') { devicePicture = 'fa fa-linux'; } - var moreInfo; + let moreInfo; if (this.state.moreInfo[i]) { moreInfo = ( <div> - <div>{'First time active: ' + firstAccessTime.toDateString() + ', ' + lastAccessTime.toLocaleTimeString()}</div> - <div>{'OS: ' + currentSession.props.os}</div> - <div>{'Browser: ' + currentSession.props.browser}</div> - <div>{'Session ID: ' + currentSession.alt_id}</div> + <div>{`First time active: ${firstAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div> + <div>{`OS: ${currentSession.props.os}`}</div> + <div>{`Browser: ${currentSession.props.browser}`}</div> + <div>{`Session ID: ${currentSession.alt_id}`}</div> </div> ); } else { - moreInfo = (<a className='theme' href='#' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); + moreInfo = ( + <a + className='theme' + href='#' + onClick={this.handleMoreInfo.bind(this, i)} + > + More info + </a> + ); } activityList[i] = ( - <div className='activity-log__table'> + <div + key={'activityLogEntryKey' + i} + className='activity-log__table' + > <div className='activity-log__report'> <div className='report__platform'><i className={devicePicture} />{currentSession.props.platform}</div> <div className='report__info'> - <div>{'Last activity: ' + lastAccessTime.toDateString() + ', ' + lastAccessTime.toLocaleTimeString()}</div> + <div>{`Last activity: ${lastAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div> {moreInfo} </div> </div> - <div className='activity-log__action'><button onClick={this.submitRevoke.bind(this, currentSession.alt_id)} className='btn btn-primary'>Logout</button></div> + <div className='activity-log__action'> + <button + onClick={this.submitRevoke.bind(this, currentSession.alt_id)} + className='btn btn-primary' + > + Logout + </button> + </div> </div> ); } - var content; + let content; if (this.state.sessions.loading) { - content = (<LoadingScreen />); + content = <LoadingScreen />; } else { - content = (<form role='form'>{activityList}</form>); + content = <form role='form'>{activityList}</form>; } return ( <div> - <div className='modal fade' ref='modal' id='activity-log' tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade' + ref='modal' + id='activity-log' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog modal-lg'> <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' id='myModalLabel'>Active Sessions</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + > + Active Sessions + </h4> </div> <p className='session-help-text'>Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the 'Logout' button below to end a session.</p> - <div ref='modalBody' className='modal-body'> + <div + ref='modalBody' + className='modal-body' + > {content} </div> </div> @@ -139,4 +173,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 0254d0e82..db23a5831 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,139 +1,90 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var PostStore = require('../stores/post_store.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var NavbarSearchBox = require('./search_bar.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var Client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); -var MessageWrapper = require('./message_wrapper.jsx'); - -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; - -var PopoverListMembers = React.createClass({ - componentDidMount: function() { - var originalLeave = $.fn.popover.Constructor.prototype.leave; - $.fn.popover.Constructor.prototype.leave = function(obj) { - var selfObj; - if (obj instanceof this.constructor) { - selfObj = obj; - } else { - selfObj = $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type); - } - originalLeave.call(this, obj); - - if (obj.currentTarget && selfObj.$tip) { - selfObj.$tip.one('mouseenter', function() { - clearTimeout(selfObj.timeout); - selfObj.$tip.one('mouseleave', function() { - $.fn.popover.Constructor.prototype.leave.call(selfObj, selfObj); - }); - }); - } - }; +const ChannelStore = require('../stores/channel_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const PostStore = require('../stores/post_store.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const NavbarSearchBox = require('./search_bar.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); +const Utils = require('../utils/utils.jsx'); +const MessageWrapper = require('./message_wrapper.jsx'); +const PopoverListMembers = require('./popover_list_members.jsx'); - $('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true}); - $('body').on('click', function(e) { - if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) { - $('#member_popover').popover('hide'); - } - }); - }, - - render: function() { - var popoverHtml = ''; - var members = this.props.members; - var count; - if (members.length > 20) { - count = '20+'; - } else { - count = members.length || '-'; - } +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; - if (members) { - members.sort(function(a, b) { - return a.username.localeCompare(b.username); - }); +export default class ChannelHeader extends React.Component { + constructor(props) { + super(props); - members.forEach(function(m) { - popoverHtml += "<div class='text--nowrap'>" + m.username + '</div>'; - }); - } + this.onListenerChange = this.onListenerChange.bind(this); + this.onSocketChange = this.onSocketChange.bind(this); + this.handleLeave = this.handleLeave.bind(this); + this.searchMentions = this.searchMentions.bind(this); - return ( - <div id='member_popover' data-toggle='popover' data-content={popoverHtml} data-original-title='Members' > - <div id='member_tooltip' data-placement='left' data-toggle='tooltip' title='View Channel Members'> - {count} <span className='glyphicon glyphicon-user' aria-hidden='true'></span> - </div> - </div> - ); + this.state = this.getStateFromStores(); } -}); - -function getStateFromStores() { - return { - channel: ChannelStore.getCurrent(), - memberChannel: ChannelStore.getCurrentMember(), - memberTeam: UserStore.getCurrentUser(), - users: ChannelStore.getCurrentExtraInfo().members, - searchVisible: PostStore.getSearchResults() != null - }; -} - -module.exports = React.createClass({ - displayName: 'ChannelHeader', - componentDidMount: function() { + getStateFromStores() { + return { + channel: ChannelStore.getCurrent(), + memberChannel: ChannelStore.getCurrentMember(), + memberTeam: UserStore.getCurrentUser(), + users: ChannelStore.getCurrentExtraInfo().members, + searchVisible: PostStore.getSearchResults() !== null + }; + } + componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addExtraInfoChangeListener(this.onListenerChange); PostStore.addSearchChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); SocketStore.addChangeListener(this.onSocketChange); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); PostStore.removeSearchChangeListener(this.onListenerChange); UserStore.addChangeListener(this.onListenerChange); - }, - onListenerChange: function() { - var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + } + onListenerChange() { + const newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover click', html: true, delay: {show: 500, hide: 500}}); - }, - onSocketChange: function(msg) { + } + onSocketChange(msg) { if (msg.action === 'new_user') { AsyncClient.getChannelExtraInfo(true); } - }, - getInitialState: function() { - return getStateFromStores(); - }, - handleLeave: function() { + } + handleLeave() { Client.leaveChannel(this.state.channel.id, - function() { - var townsquare = ChannelStore.getByName('town-square'); - utils.switchChannel(townsquare); - }, - function(err) { + function handleLeaveSuccess() { + AppDispatcher.handleViewAction({ + type: ActionTypes.LEAVE_CHANNEL, + id: this.state.channel.id + }); + + const townsquare = ChannelStore.getByName('town-square'); + Utils.switchChannel(townsquare); + }.bind(this), + function handleLeaveError(err) { AsyncClient.dispatchError(err, 'handleLeave'); } ); - }, - searchMentions: function(e) { + } + searchMentions(e) { e.preventDefault(); - var user = UserStore.getCurrentUser(); + const user = UserStore.getCurrentUser(); - var terms = ''; + let terms = ''; if (user.notify_props && user.notify_props.mention_keys) { - var termKeys = UserStore.getCurrentMentionKeys(); + let termKeys = UserStore.getCurrentMentionKeys(); if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) { termKeys.splice(termKeys.indexOf('@all'), 1); } @@ -149,23 +100,23 @@ module.exports = React.createClass({ do_search: true, is_mention_search: true }); - }, - render: function() { - if (this.state.channel == null) { + } + render() { + if (this.state.channel === null) { return null; } - var channel = this.state.channel; - var description = utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true}); - var popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); - var channelTitle = channel.display_name; - var currentId = UserStore.getCurrentId(); - var isAdmin = this.state.memberChannel.roles.indexOf('admin') > -1 || this.state.memberTeam.roles.indexOf('admin') > -1; - var isDirect = (this.state.channel.type === 'D'); + const channel = this.state.channel; + const description = Utils.textToJsx(channel.description, {singleline: true, noMentionHighlight: true}); + const popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); + let channelTitle = channel.display_name; + const currentId = UserStore.getCurrentId(); + const isAdmin = this.state.memberChannel.roles.indexOf('admin') > -1 || this.state.memberTeam.roles.indexOf('admin') > -1; + const isDirect = (this.state.channel.type === 'D'); if (isDirect) { if (this.state.users.length > 1) { - var contact; + let contact; if (this.state.users[0].id === currentId) { contact = this.state.users[1]; } else { @@ -175,64 +126,244 @@ module.exports = React.createClass({ } } - var channelTerm = 'Channel'; + let channelTerm = 'Channel'; if (channel.type === 'P') { channelTerm = 'Group'; } + let dropdownContents = []; + if (!isDirect) { + dropdownContents.push( + <li + key='view_info' + role='presentation' + > + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_info' + data-channelid={channel.id} + href='#' + > + View Info + </a> + </li> + ); + + if (!ChannelStore.isDefault(channel)) { + dropdownContents.push( + <li + key='add_members' + role='presentation' + > + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_invite' + href='#' + > + Add Members + </a> + </li> + ); + + if (isAdmin) { + dropdownContents.push( + <li + key='manage_members' + role='presentation' + > + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_members' + href='#' + > + Manage Members + </a> + </li> + ); + } + } + + dropdownContents.push( + <li + key='set_channel_description' + role='presentation' + > + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + > + Set {channelTerm} Description... + </a> + </li> + ); + dropdownContents.push( + <li + key='notification_preferences' + role='presentation' + > + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#channel_notifications' + data-title={channel.display_name} + data-channelid={channel.id} + > + Notification Preferences + </a> + </li> + ); + + if (!ChannelStore.isDefault(channel)) { + if (isAdmin) { + dropdownContents.push( + <li + key='rename_channel' + role='presentation' + > + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#rename_channel' + data-display={channel.display_name} + data-name={channel.name} + data-channelid={channel.id} + > + Rename {channelTerm}... + </a> + </li> + ); + dropdownContents.push( + <li + key='delete_channel' + role='presentation' + > + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#delete_channel' + data-title={channel.display_name} + data-channelid={channel.id} + > + Delete {channelTerm}... + </a> + </li> + ); + } + + dropdownContents.push( + <li + key='leave_channel' + role='presentation' + > + <a + role='menuitem' + href='#' + onClick={this.handleLeave} + > + Leave {channelTerm} + </a> + </li> + ); + } + } else { + dropdownContents.push( + <li + key='edit_description_direct' + role='presentation' + > + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + > + Set Channel Description... + </a> + </li> + ); + } + return ( <table className='channel-header alt'> <tr> <th> <div className='channel-header__info'> <div className='dropdown'> - <a href='#' className='dropdown-toggle theme' type='button' id='channel_header_dropdown' data-toggle='dropdown' aria-expanded='true'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > <strong className='heading'>{channelTitle} </strong> - <span className='glyphicon glyphicon-chevron-down header-dropdown__icon'></span> + <span className='glyphicon glyphicon-chevron-down header-dropdown__icon' /> </a> - {!isDirect ? - <ul className='dropdown-menu' role='menu' aria-labelledby='channel_header_dropdown'> - <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_info' data-channelid={channel.id} href='#'>View Info</a></li> - {!ChannelStore.isDefault(channel) ? - <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_invite' href='#'>Add Members</a></li> - : null - } - {isAdmin && !ChannelStore.isDefault(channel) ? - <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_members' href='#'>Manage Members</a></li> - : null - } - <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set {channelTerm} Description...</a></li> - <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#channel_notifications' data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li> - {isAdmin && !ChannelStore.isDefault(channel) ? - <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#rename_channel' data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename {channelTerm}...</a></li> - : null - } - {isAdmin && !ChannelStore.isDefault(channel) ? - <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#delete_channel' data-title={channel.display_name} data-channelid={channel.id}>Delete {channelTerm}...</a></li> - : null - } - {!ChannelStore.isDefault(channel) ? - <li role='presentation'><a role='menuitem' href='#' onClick={this.handleLeave}>Leave {channelTerm}</a></li> - : null - } + <ul + className='dropdown-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {dropdownContents} </ul> - : - <ul className='dropdown-menu' role='menu' aria-labelledby='channel_header_dropdown'> - <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li> - </ul> - } </div> - <div data-toggle='popover' data-content={popoverContent} className='description'>{description}</div> + <div + data-toggle='popover' + data-content={popoverContent} + className='description' + > + {description} + </div> </div> </th> - <th><PopoverListMembers members={this.state.users} channelId={channel.id} /></th> + <th> + <PopoverListMembers + members={this.state.users} + channelId={channel.id} + /> + </th> <th className='search-bar__container'><NavbarSearchBox /></th> <th> <div className='dropdown channel-header__links'> - <a href='#' className='dropdown-toggle theme' type='button' id='channel_header_right_dropdown' data-toggle='dropdown' aria-expanded='true'> - <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> </a> - <ul className='dropdown-menu dropdown-menu-right' role='menu' aria-labelledby='channel_header_right_dropdown'> - <li role='presentation'><a role='menuitem' href='#' onClick={this.searchMentions}>Recent Mentions</a></li> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_right_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> + </a> + <ul + className='dropdown-menu dropdown-menu-right' + role='menu' + aria-labelledby='channel_header_right_dropdown' + > + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.searchMentions} + > + Recent Mentions + </a> + </li> </ul> </div> </th> @@ -240,4 +371,4 @@ module.exports = React.createClass({ </table> ); } -}); +} diff --git a/web/react/components/channel_info_modal.jsx b/web/react/components/channel_info_modal.jsx index 6d999870a..fdd9a5c17 100644 --- a/web/react/components/channel_info_modal.jsx +++ b/web/react/components/channel_info_modal.jsx @@ -3,57 +3,86 @@ var ChannelStore = require('../stores/channel_store.jsx'); -module.exports = React.createClass({ - componentDidMount: function() { +export default class CommandList extends React.Component { + constructor(props) { + super(props); + + this.state = { + channel_id: ChannelStore.getCurrentId() + }; + } + + componentDidMount() { var self = this; - if(this.refs.modal) { - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { - var button = e.relatedTarget; - self.setState({ channel_id: $(button).attr('data-channelid') }); - }); + if (this.refs.modal) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) { + var button = e.relatedTarget; + self.setState({channel_id: $(button).attr('data-channelid')}); + }); } - }, - getInitialState: function() { - return { channel_id: ChannelStore.getCurrentId() }; - }, - render: function() { + } + + render() { var channel = ChannelStore.get(this.state.channel_id); if (!channel) { channel = {}; - channel.display_name = "No Channel Found"; - channel.name = "No Channel Found"; - channel.id = "No Channel Found"; + channel.display_name = 'No Channel Found'; + channel.name = 'No Channel Found'; + channel.id = 'No Channel Found'; } return ( - <div className="modal fade" ref="modal" id="channel_info" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" id="myModalLabel"><span className="name">{channel.display_name}</span></h4> + <div + className='modal fade' + ref='modal' + id='channel_info' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + > + <span className='name'>{channel.display_name}</span> + </h4> </div> - <div className="modal-body"> - <div className="row form-group"> - <div className="col-sm-3 info__label">Channel Name: </div> - <div className="col-sm-9">{channel.display_name}</div> + <div className='modal-body'> + <div className='row form-group'> + <div className='col-sm-3 info__label'>Channel Name: </div> + <div className='col-sm-9'>{channel.display_name}</div> </div> - <div className="row form-group"> - <div className="col-sm-3 info__label">Channel Handle:</div> - <div className="col-sm-9">{channel.name}</div> + <div className='row form-group'> + <div className='col-sm-3 info__label'>Channel Handle:</div> + <div className='col-sm-9'>{channel.name}</div> </div> - <div className="row"> - <div className="col-sm-3 info__label">Channel ID:</div> - <div className="col-sm-9">{channel.id}</div> + <div className='row'> + <div className='col-sm-3 info__label'>Channel ID:</div> + <div className='col-sm-9'>{channel.id}</div> </div> </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + >Close</button> </div> </div> </div> </div> ); } -}); +} diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index b70811db1..5feeb4e88 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -9,7 +9,6 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); - export default class ChannelInviteModal extends React.Component { constructor() { super(); @@ -129,15 +128,37 @@ export default class ChannelInviteModal extends React.Component { if (this.state.loading) { content = (<LoadingScreen />); } else { - content = (<MemberList memberList={this.state.nonmembers} isAdmin={isAdmin} handleInvite={this.handleInvite} />); + content = ( + <MemberList + memberList={this.state.nonmembers} + isAdmin={isAdmin} + handleInvite={this.handleInvite} + /> + ); } return ( - <div className='modal fade' id='channel_invite' tabIndex='-1' role='dialog' aria-hidden='true'> - <div className='modal-dialog' role='document'> + <div + className='modal fade' + id='channel_invite' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div + className='modal-dialog' + role='document' + > <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> <h4 className='modal-title'>Add New Members to <span className='name'>{this.state.channelName}</span></h4> </div> <div className='modal-body'> @@ -145,7 +166,11 @@ export default class ChannelInviteModal extends React.Component { {content} </div> <div className='modal-footer'> - <button type='button' className='btn btn-default' data-dismiss='modal'>Close</button> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + >Close</button> </div> </div> </div> @@ -153,4 +178,3 @@ export default class ChannelInviteModal extends React.Component { ); } } -ChannelInviteModal.displayName = 'ChannelInviteModal'; diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 0fa433383..ce6f60f87 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -10,13 +10,20 @@ var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -var Constants = require('../utils/constants.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - componentDidMount: function() { +export default class ChannelLoader extends React.Component { + constructor(props) { + super(props); + this.intervalId = null; + + this.onSocketChange = this.onSocketChange.bind(this); + + this.state = {}; + } + componentDidMount() { /* Initial aysnc loads */ AsyncClient.getMe(); AsyncClient.getPosts(ChannelStore.getCurrentId()); @@ -30,10 +37,12 @@ module.exports = React.createClass({ PostStore.clearPendingPosts(); /* Set up interval functions */ - setInterval( + this.intervalId = setInterval( function pollStatuses() { AsyncClient.getStatuses(); - }, 30000); + }, + 30000 + ); /* Device tracking setup */ var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); @@ -44,12 +53,12 @@ module.exports = React.createClass({ /* Set up tracking for whether the window is active */ window.isActive = true; - $(window).focus(function windowFocus() { + $(window).on('focus', function windowFocus() { AsyncClient.updateLastViewedAt(); window.isActive = true; }); - $(window).blur(function windowBlur() { + $(window).on('blur', function windowBlur() { window.isActive = false; }); @@ -60,32 +69,80 @@ module.exports = React.createClass({ var user = UserStore.getCurrentUser(); if (user.props && user.props.theme) { - utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); - utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); - utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); - utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); - utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); - utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); - utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';'); + Utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); + Utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); + Utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); + Utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); + Utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); + Utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); + Utils.changeCss('.search-item-container:hover', 'background: ' + Utils.changeOpacity(user.props.theme, 0.05) + ';'); } if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';'); - utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); + Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, -10) + ';'); + Utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); } else if (user.props.theme === '#000000') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';'); + Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +50) + ';'); $('.team__header').addClass('theme--black'); } else if (user.props.theme === '#585858') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';'); + Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';'); $('.team__header').addClass('theme--gray'); } - }, - onSocketChange: function(msg) { + + /* Setup global mouse events */ + $('body').on('click.userpopover', function popOver(e) { + if ($(e.target).attr('data-toggle') !== 'popover' && + $(e.target).parents('.popover.in').length === 0) { + $('.user-popover').popover('hide'); + } + }); + + $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); + } + }); + + /* Setup modal events */ + $('.modal').on('show.bs.modal', function onShow() { + $('.modal-body').css('overflow-y', 'auto'); + $('.modal-body').css('max-height', $(window).height() * 0.7); + }); + } + componentWillUnmount() { + clearInterval(this.intervalId); + + $(window).off('focus'); + $(window).off('blur'); + + SocketStore.removeChangeListener(this.onSocketChange); + + $('body').off('click.userpopover'); + $('body').off('mouseenter mouseleave', '.post'); + $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); + + $('.modal').off('show.bs.modal'); + } + onSocketChange(msg) { if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { UserStore.setStatus(msg.user_id, 'online'); } - }, - render: function() { + } + render() { return <div/>; } -}); +} diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx index db4bec400..04fa2c7a2 100644 --- a/web/react/components/channel_members.jsx +++ b/web/react/components/channel_members.jsx @@ -1,154 +1,200 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var MemberList = require('./member_list.jsx'); -var client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); - -function getStateFromStores() { - var users = UserStore.getActiveOnlyProfiles(); - var member_list = ChannelStore.getCurrentExtraInfo().members; - - var nonmember_list = []; - for (var id in users) { - var found = false; - for (var i = 0; i < member_list.length; i++) { - if (member_list[i].id === id) { - found = true; - break; +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const MemberList = require('./member_list.jsx'); +const Client = require('../utils/client.jsx'); +const Utils = require('../utils/utils.jsx'); + +export default class ChannelMembers extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onChange = this.onChange.bind(this); + this.handleRemove = this.handleRemove.bind(this); + + this.state = this.getStateFromStores(); + } + getStateFromStores() { + const users = UserStore.getActiveOnlyProfiles(); + let memberList = ChannelStore.getCurrentExtraInfo().members; + + let nonmemberList = []; + for (let id in users) { + if (users.hasOwnProperty(id)) { + let found = false; + for (let i = 0; i < memberList.length; i++) { + if (memberList[i].id === id) { + found = true; + break; + } + } + if (!found) { + nonmemberList.push(users[id]); + } } } - if (!found) { - nonmember_list.push(users[id]); + + function compareByUsername(a, b) { + if (a.username < b.username) { + return -1; + } else if (a.username > b.username) { + return 1; + } + + return 0; } - } - member_list.sort(function(a,b) { - if (a.username < b.username) return -1; - if (a.username > b.username) return 1; - return 0; - }); - - nonmember_list.sort(function(a,b) { - if (a.username < b.username) return -1; - if (a.username > b.username) return 1; - return 0; - }); - - var channel_name = ChannelStore.getCurrent() ? ChannelStore.getCurrent().display_name : ""; - - return { - nonmember_list: nonmember_list, - member_list: member_list, - channel_name: channel_name - }; -} + memberList.sort(compareByUsername); + nonmemberList.sort(compareByUsername); + + const channel = ChannelStore.getCurrent(); + let channelName = ''; + if (channel) { + channelName = channel.display_name; + } -module.exports = React.createClass({ - componentDidMount: function() { - ChannelStore.addExtraInfoChangeListener(this._onChange); - ChannelStore.addChangeListener(this._onChange); - var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ render_members: false }); - }); - - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { - self.setState({ render_members: true }); - }); - }, - componentWillUnmount: function() { - ChannelStore.removeExtraInfoChangeListener(this._onChange); - ChannelStore.removeChangeListener(this._onChange); - }, - _onChange: function() { - var new_state = getStateFromStores(); - if (!utils.areStatesEqual(this.state, new_state)) { - this.setState(new_state); + return { + nonmemberList: nonmemberList, + memberList: memberList, + channelName: channelName + }; + } + componentDidMount() { + ChannelStore.addExtraInfoChangeListener(this.onChange); + ChannelStore.addChangeListener(this.onChange); + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function handleHide() { + this.setState({renderMembers: false}); + }.bind(this)); + + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow() { + this.setState({renderMembers: true}); + }.bind(this)); + } + componentWillUnmount() { + ChannelStore.removeExtraInfoChangeListener(this.onChange); + ChannelStore.removeChangeListener(this.onChange); + } + onChange() { + const newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(this.state, newState)) { + this.setState(newState); } - }, - handleRemove: function(user_id) { + } + handleRemove(userId) { // Make sure the user is a member of the channel - var member_list = this.state.member_list; - var found = false; - for (var i = 0; i < member_list.length; i++) { - if (member_list[i].id === user_id) { + let memberList = this.state.memberList; + let found = false; + for (let i = 0; i < memberList.length; i++) { + if (memberList[i].id === userId) { found = true; break; } } - if (!found) { return }; + if (!found) { + return; + } - var data = {}; - data['user_id'] = user_id; + let data = {}; + data.user_id = userId; - client.removeChannelMember(ChannelStore.getCurrentId(), data, - function(data) { - var old_member; - for (var i = 0; i < member_list.length; i++) { - if (user_id === member_list[i].id) { - old_member = member_list[i]; - member_list.splice(i, 1); + Client.removeChannelMember(ChannelStore.getCurrentId(), data, + function handleRemoveSuccess() { + let oldMember; + for (let i = 0; i < memberList.length; i++) { + if (userId === memberList[i].id) { + oldMember = memberList[i]; + memberList.splice(i, 1); break; } } - var nonmember_list = this.state.nonmember_list; - if (old_member) { - nonmember_list.push(old_member); + let nonmemberList = this.state.nonmemberList; + if (oldMember) { + nonmemberList.push(oldMember); } - this.setState({ member_list: member_list, nonmember_list: nonmember_list }); + this.setState({memberList: memberList, nonmemberList: nonmemberList}); AsyncClient.getChannelExtraInfo(true); }.bind(this), - function(err) { - this.setState({ invite_error: err.message }); + function handleRemoveError(err) { + this.setState({inviteError: err.message}); }.bind(this) ); - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { - var currentMember = ChannelStore.getCurrentMember(); - var isAdmin = false; + } + render() { + const currentMember = ChannelStore.getCurrentMember(); + let isAdmin = false; if (currentMember) { - isAdmin = currentMember.roles.indexOf("admin") > -1 || UserStore.getCurrentUser().roles.indexOf("admin") > -1; + isAdmin = currentMember.roles.indexOf('admin') > -1 || UserStore.getCurrentUser().roles.indexOf('admin') > -1; + } + + var memberList = null; + if (this.state.renderMembers) { + memberList = ( + <MemberList + memberList={this.state.memberList} + isAdmin={isAdmin} + handleRemove={this.handleRemove} + /> + ); } return ( - <div className="modal fade" ref="modal" id="channel_members" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title"><span className="name">{this.state.channel_name}</span> Members</h4> - <a className="btn btn-md btn-primary" data-toggle="modal" data-target="#channel_invite"><i className="glyphicon glyphicon-envelope"/> Add New Members</a> - </div> - <div ref="modalBody" className="modal-body"> - <div className="col-sm-12"> - <div className="team-member-list"> - { this.state.render_members ? - <MemberList - memberList={this.state.member_list} - isAdmin={isAdmin} - handleRemove={this.handleRemove} - /> - : "" } + <div + className='modal fade' + ref='modal' + id='channel_members' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 className='modal-title'><span className='name'>{this.state.channelName}</span> Members</h4> + <a + className='btn btn-md btn-primary' + data-toggle='modal' + data-target='#channel_invite' + > + <i className='glyphicon glyphicon-envelope'/> Add New Members + </a> + </div> + <div + ref='modalBody' + className='modal-body' + > + <div className='col-sm-12'> + <div className='team-member-list'> + {memberList} + </div> </div> </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Close + </button> + </div> </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> - </div> + </div> </div> - ); } -}); +} diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx index 884bad9d2..83067240d 100644 --- a/web/react/components/channel_notifications.jsx +++ b/web/react/components/channel_notifications.jsx @@ -4,26 +4,29 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +var Utils = require('../utils/utils.jsx'); +var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); export default class ChannelNotifications extends React.Component { constructor(props) { super(props); + this.onListenerChange = this.onListenerChange.bind(this); this.updateSection = this.updateSection.bind(this); this.handleUpdate = this.handleUpdate.bind(this); this.handleRadioClick = this.handleRadioClick.bind(this); this.handleQuietToggle = this.handleQuietToggle.bind(this); + this.createDesktopSection = this.createDesktopSection.bind(this); + this.createQuietSection = this.createQuietSection.bind(this); + this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''}; } componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); - var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function showModal(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) { var button = e.relatedTarget; var channelId = button.getAttribute('data-channelid'); @@ -34,8 +37,8 @@ export default class ChannelNotifications extends React.Component { quietMode = true; } - self.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId}); - }); + this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId}); + }.bind(this)); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); @@ -55,7 +58,7 @@ export default class ChannelNotifications extends React.Component { newState.notifyLevel = notifyLevel; newState.quietMode = quietMode; - if (!utils.areStatesEqual(this.state, newState)) { + if (!Utils.areStatesEqual(this.state, newState)) { this.setState(newState); } } @@ -78,7 +81,7 @@ export default class ChannelNotifications extends React.Component { return; } - client.updateNotifyLevel(data, + Client.updateNotifyLevel(data, function success() { var member = ChannelStore.getMember(channelId); member.notify_level = notifyLevel; @@ -92,25 +95,15 @@ export default class ChannelNotifications extends React.Component { } handleRadioClick(notifyLevel) { this.setState({notifyLevel: notifyLevel, quietMode: false}); - this.refs.modal.getDOMNode().focus(); + React.findDOMNode(this.refs.modal).focus(); } handleQuietToggle(quietMode) { this.setState({notifyLevel: 'none', quietMode: quietMode}); - this.refs.modal.getDOMNode().focus(); + React.findDOMNode(this.refs.modal).focus(); } - render() { - var serverError = null; - if (this.state.serverError) { - serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; - } - - var self = this; - var describe = ''; - var inputs = []; - + createDesktopSection(serverError) { var handleUpdateSection; - var desktopSection; if (this.state.activeSection === 'desktop') { var notifyActive = [false, false, false]; if (this.state.notifyLevel === 'mention') { @@ -121,6 +114,8 @@ export default class ChannelNotifications extends React.Component { notifyActive[2] = true; } + var inputs = []; + inputs.push( <div> <div className='radio'> @@ -128,7 +123,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={notifyActive[0]} - onChange={self.handleRadioClick.bind(this, 'all')} + onChange={this.handleRadioClick.bind(this, 'all')} > For all activity </input> @@ -140,7 +135,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={notifyActive[1]} - onChange={self.handleRadioClick.bind(this, 'mention')} + onChange={this.handleRadioClick.bind(this, 'mention')} > Only for mentions </input> @@ -152,7 +147,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={notifyActive[2]} - onChange={self.handleRadioClick.bind(this, 'none')} + onChange={this.handleRadioClick.bind(this, 'none')} > Never </input> @@ -162,44 +157,54 @@ export default class ChannelNotifications extends React.Component { ); handleUpdateSection = function updateSection(e) { - self.updateSection(''); - self.onListenerChange(); + this.updateSection(''); + this.onListenerChange(); e.preventDefault(); - }; + }.bind(this); + + let curChannel = ChannelStore.get(this.state.channelId); + let extraInfo = (<span>These settings will override the global notification settings</span>); - desktopSection = ( + if (curChannel && curChannel.display_name) { + extraInfo = (<span>These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel</span>); + } + + return ( <SettingItemMax title='Send desktop notifications' inputs={inputs} submit={this.handleUpdate} server_error={serverError} updateSection={handleUpdateSection} + extraInfo={extraInfo} /> ); - } else { - if (this.state.notifyLevel === 'mention') { - describe = 'Only for mentions'; - } else if (this.state.notifyLevel === 'all') { - describe = 'For all activity'; - } else { - describe = 'Never'; - } - - handleUpdateSection = function updateSection(e) { - self.updateSection('desktop'); - e.preventDefault(); - }; + } - desktopSection = ( - <SettingItemMin - title='Send desktop notifications' - describe={describe} - updateSection={handleUpdateSection} - /> - ); + var describe; + if (this.state.notifyLevel === 'mention') { + describe = 'Only for mentions'; + } else if (this.state.notifyLevel === 'all') { + describe = 'For all activity'; + } else { + describe = 'Never'; } - var quietSection; + handleUpdateSection = function updateSection(e) { + this.updateSection('desktop'); + e.preventDefault(); + }.bind(this); + + return ( + <SettingItemMin + title='Send desktop notifications' + describe={describe} + updateSection={handleUpdateSection} + /> + ); + } + createQuietSection(serverError) { + var handleUpdateSection; if (this.state.activeSection === 'quiet') { var quietActive = [false, false]; if (this.state.quietMode) { @@ -208,6 +213,8 @@ export default class ChannelNotifications extends React.Component { quietActive[1] = true; } + var inputs = []; + inputs.push( <div> <div className='radio'> @@ -215,7 +222,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={quietActive[0]} - onChange={self.handleQuietToggle.bind(this, true)} + onChange={this.handleQuietToggle.bind(this, true)} > On </input> @@ -227,7 +234,7 @@ export default class ChannelNotifications extends React.Component { <input type='radio' checked={quietActive[1]} - onChange={self.handleQuietToggle.bind(this, false)} + onChange={this.handleQuietToggle.bind(this, false)} > Off </input> @@ -245,12 +252,12 @@ export default class ChannelNotifications extends React.Component { ); handleUpdateSection = function updateSection(e) { - self.updateSection(''); - self.onListenerChange(); + this.updateSection(''); + this.onListenerChange(); e.preventDefault(); - }; + }.bind(this); - quietSection = ( + return ( <SettingItemMax title='Quiet mode' inputs={inputs} @@ -259,27 +266,38 @@ export default class ChannelNotifications extends React.Component { updateSection={handleUpdateSection} /> ); + } + + var describe; + if (this.state.quietMode) { + describe = 'On'; } else { - if (this.state.quietMode) { - describe = 'On'; - } else { - describe = 'Off'; - } + describe = 'Off'; + } - handleUpdateSection = function updateSection(e) { - self.updateSection('quiet'); - e.preventDefault(); - }; + handleUpdateSection = function updateSection(e) { + this.updateSection('quiet'); + e.preventDefault(); + }.bind(this); - quietSection = ( - <SettingItemMin - title='Quiet mode' - describe={describe} - updateSection={handleUpdateSection} - /> - ); + return ( + <SettingItemMin + title='Quiet mode' + describe={describe} + updateSection={handleUpdateSection} + /> + ); + } + render() { + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; } + var desktopSection = this.createDesktopSection(serverError); + + var quietSection = this.createQuietSection(serverError); + return ( <div className='modal fade' diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx index 5efe98dc6..63bd57c2a 100644 --- a/web/react/components/command_list.jsx +++ b/web/react/components/command_list.jsx @@ -3,25 +3,40 @@ var client = require('../utils/client.jsx'); -module.exports = React.createClass({ - getInitialState: function() { - return { suggestions: [ ], cmd: "" }; - }, - handleClick: function(i) { - this.props.addCommand(this.state.suggestions[i].suggestion) - this.setState({ suggestions: [ ], cmd: "" }); - }, - addFirstCommand: function() { - if (this.state.suggestions.length == 0) return; +export default class CommandList extends React.Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + this.addFirstCommand = this.addFirstCommand.bind(this); + this.isEmpty = this.isEmpty.bind(this); + this.getSuggestedCommands = this.getSuggestedCommands.bind(this); + + this.state = { + suggestions: [ ], + cmd: '' + }; + } + + handleClick(i) { + this.props.addCommand(this.state.suggestions[i].suggestion); + this.setState({suggestions: [ ], cmd: ''}); + } + + addFirstCommand() { + if (this.state.suggestions.length === 0) { + return; + } this.handleClick(0); - }, - isEmpty: function() { - return this.state.suggestions.length == 0; - }, - getSuggestedCommands: function(cmd) { + } - if (!cmd || cmd.charAt(0) != '/') { - this.setState({ suggestions: [ ], cmd: "" }); + isEmpty() { + return this.state.suggestions.length === 0; + } + + getSuggestedCommands(cmd) { + if (!cmd || cmd.charAt(0) !== '/') { + this.setState({suggestions: [ ], cmd: ''}); return; } @@ -29,36 +44,56 @@ module.exports = React.createClass({ this.props.channelId, cmd, true, - function(data) { + function success(data) { if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) { data.suggestions = []; } - this.setState({ suggestions: data.suggestions, cmd: cmd }); + this.setState({suggestions: data.suggestions, cmd: cmd}); }.bind(this), - function(err){ - } + function fail() { + } ); - }, - render: function() { - if (this.state.suggestions.length == 0) return (<div/>); + } + + render() { + if (this.state.suggestions.length === 0) { + return (<div/>); + } var suggestions = []; for (var i = 0; i < this.state.suggestions.length; i++) { - if (this.state.suggestions[i].suggestion != this.state.cmd) { + if (this.state.suggestions[i].suggestion !== this.state.cmd) { suggestions.push( - <div key={i} className="command-name" onClick={this.handleClick.bind(this, i)}> - <div className="pull-left"><strong>{ this.state.suggestions[i].suggestion }</strong></div> - <div className="command-desc pull-right">{ this.state.suggestions[i].description }</div> + <div + key={i} + className='command-name' + onClick={this.handleClick.bind(this, i)} + > + <div className='command__title'><strong>{this.state.suggestions[i].suggestion}</strong></div> + <div className='command__desc'>{this.state.suggestions[i].description}</div> </div> ); } } return ( - <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*37)+2}}> - { suggestions } + <div + ref='mentionlist' + className='command-box' + style={{height: (this.state.suggestions.length * 56) + 2}} + > + {suggestions} </div> ); } -}); +} + +CommandList.defaultProps = { + channelId: null +}; + +CommandList.propTypes = { + addCommand: React.PropTypes.func, + channelId: React.PropTypes.string +};
\ No newline at end of file diff --git a/web/react/components/confirm_modal.jsx b/web/react/components/confirm_modal.jsx index 3be13cf9b..cb3b9c5e3 100644 --- a/web/react/components/confirm_modal.jsx +++ b/web/react/components/confirm_modal.jsx @@ -1,31 +1,70 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - handleConfirm: function() { - $('#'+this.props.parent_id).attr('data-confirm', 'true'); - $('#'+this.props.parent_id).modal('hide'); - $('#'+this.props.id).modal('hide'); - }, - render: function() { +export default class ConfirmModal extends React.Component { + constructor(props) { + super(props); + + this.handleConfirm = this.handleConfirm.bind(this); + + this.state = {}; + } + handleConfirm() { + $('#' + this.props.parent_id).attr('data-confirm', 'true'); + $('#' + this.props.parent_id).modal('hide'); + $('#' + this.props.id).modal('hide'); + } + render() { return ( - <div className="modal fade" id={this.props.id} tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <h4 className="modal-title">{this.props.title}</h4> - </div> - <div className="modal-body"> - {this.props.message} - </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button onClick={this.handleConfirm} type="button" className="btn btn-primary">{this.props.confirm_button}</button> + <div + className='modal fade' + id={this.props.id} + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <h4 className='modal-title'>{this.props.title}</h4> + </div> + <div className='modal-body'> + {this.props.message} + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + onClick={this.handleConfirm} + type='button' + className='btn btn-primary' + > + {this.props.confirm_button} + </button> + </div> </div> - </div> - </div> + </div> </div> ); } -}); +} +ConfirmModal.defaultProps = { + parent_id: '', + id: '', + title: '', + message: '', + confirm_button: '' +}; +ConfirmModal.propTypes = { + parent_id: React.PropTypes.string, + id: React.PropTypes.string, + title: React.PropTypes.string, + message: React.PropTypes.string, + confirm_button: React.PropTypes.string +}; diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index c2b7e222f..c2fc0dcf3 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -1,24 +1,48 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var PostStore = require('../stores/post_store.jsx'); -var Textbox = require('./textbox.jsx'); -var MsgTyping = require('./msg_typing.jsx'); -var FileUpload = require('./file_upload.jsx'); -var FilePreview = require('./file_preview.jsx'); -var utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; - -module.exports = React.createClass({ - lastTime: 0, - handleSubmit: function(e) { +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const PostStore = require('../stores/post_store.jsx'); +const Textbox = require('./textbox.jsx'); +const MsgTyping = require('./msg_typing.jsx'); +const FileUpload = require('./file_upload.jsx'); +const FilePreview = require('./file_preview.jsx'); +const Utils = require('../utils/utils.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; + +export default class CreateComment extends React.Component { + constructor(props) { + super(props); + + this.lastTime = 0; + + this.handleSubmit = this.handleSubmit.bind(this); + this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + this.handleUploadStart = this.handleUploadStart.bind(this); + this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); + this.handleUploadError = this.handleUploadError.bind(this); + this.removePreview = this.removePreview.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.getFileCount = this.getFileCount.bind(this); + + PostStore.clearCommentDraftUploads(); + + const draft = PostStore.getCommentDraft(this.props.rootId); + this.state = { + messageText: draft.message, + uploadsInProgress: draft.uploadsInProgress, + previews: draft.previews, + submitting: false + }; + } + handleSubmit(e) { e.preventDefault(); if (this.state.uploadsInProgress.length > 0) { @@ -29,7 +53,7 @@ module.exports = React.createClass({ return; } - var post = {}; + let post = {}; post.filenames = []; post.message = this.state.messageText; @@ -38,30 +62,30 @@ module.exports = React.createClass({ } if (post.message.length > Constants.CHARACTER_LIMIT) { - this.setState({postError: 'Comment length must be less than ' + Constants.CHARACTER_LIMIT + ' characters.'}); + this.setState({postError: `Comment length must be less than ${Constants.CHARACTER_LIMIT} characters.`}); return; } - var user_id = UserStore.getCurrentId(); + const userId = UserStore.getCurrentId(); post.channel_id = this.props.channelId; post.root_id = this.props.rootId; post.parent_id = this.props.rootId; post.filenames = this.state.previews; - var time = utils.getTimestamp(); - post.pending_post_id = user_id + ':'+ time; - post.user_id = user_id; + const time = Utils.getTimestamp(); + post.pending_post_id = `${userId}:${time}`; + post.user_id = userId; post.create_at = time; PostStore.storePendingPost(post); PostStore.storeCommentDraft(this.props.rootId, null); - client.createPost(post, ChannelStore.getCurrent(), - function(data) { + Client.createPost(post, ChannelStore.getCurrent(), + function handlePostSuccess(data) { AsyncClient.getPosts(this.props.channelId); - var channel = ChannelStore.get(this.props.channelId); - var member = ChannelStore.getMember(this.props.channelId); + const channel = ChannelStore.get(this.props.channelId); + let member = ChannelStore.getMember(this.props.channelId); member.msg_count = channel.total_msg_count; member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); @@ -71,8 +95,8 @@ module.exports = React.createClass({ post: data }); }.bind(this), - function(err) { - var state = {}; + function handlePostError(err) { + let state = {}; if (err.message === 'Invalid RootId parameter') { if ($('#post_deleted').length > 0) { @@ -90,76 +114,76 @@ module.exports = React.createClass({ ); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); - }, - commentMsgKeyPress: function(e) { + } + commentMsgKeyPress(e) { if (e.which === 13 && !e.shiftKey && !e.altKey) { e.preventDefault(); - this.refs.textbox.getDOMNode().blur(); + React.findDOMNode(this.refs.textbox).blur(); this.handleSubmit(e); } - var t = Date.now(); + const t = Date.now(); if ((t - this.lastTime) > 5000) { - SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {'parent_id': this.props.rootId}}); + SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}}); this.lastTime = t; } - }, - handleUserInput: function(messageText) { - var draft = PostStore.getCommentDraft(this.props.rootId); + } + handleUserInput(messageText) { + let draft = PostStore.getCommentDraft(this.props.rootId); draft.message = messageText; PostStore.storeCommentDraft(this.props.rootId, draft); $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); $('.post-right__scroll').perfectScrollbar('update'); this.setState({messageText: messageText}); - }, - handleUploadStart: function(clientIds, channelId) { - var draft = PostStore.getCommentDraft(this.props.rootId); + } + handleUploadStart(clientIds) { + let draft = PostStore.getCommentDraft(this.props.rootId); - draft['uploadsInProgress'] = draft['uploadsInProgress'].concat(clientIds); + draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds); PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress']}); - }, - handleFileUploadComplete: function(filenames, clientIds, channelId) { - var draft = PostStore.getCommentDraft(this.props.rootId); + this.setState({uploadsInProgress: draft.uploadsInProgress}); + } + handleFileUploadComplete(filenames, clientIds) { + let draft = PostStore.getCommentDraft(this.props.rootId); // remove each finished file from uploads - for (var i = 0; i < clientIds.length; i++) { - var index = draft['uploadsInProgress'].indexOf(clientIds[i]); + for (let i = 0; i < clientIds.length; i++) { + const index = draft.uploadsInProgress.indexOf(clientIds[i]); if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); + draft.uploadsInProgress.splice(index, 1); } } - draft['previews'] = draft['previews'].concat(filenames); + draft.previews = draft.previews.concat(filenames); PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); - }, - handleUploadError: function(err, clientId) { + this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + } + handleUploadError(err, clientId) { if (clientId !== -1) { - var draft = PostStore.getCommentDraft(this.props.rootId); + let draft = PostStore.getCommentDraft(this.props.rootId); - var index = draft['uploadsInProgress'].indexOf(clientId); + const index = draft.uploadsInProgress.indexOf(clientId); if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); + draft.uploadsInProgress.splice(index, 1); } PostStore.storeCommentDraft(this.props.rootId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err}); } else { this.setState({serverError: err}); } - }, - removePreview: function(id) { - var previews = this.state.previews; - var uploadsInProgress = this.state.uploadsInProgress; + } + removePreview(id) { + let previews = this.state.previews; + let uploadsInProgress = this.state.uploadsInProgress; // id can either be the path of an uploaded file or the client id of an in progress upload - var index = previews.indexOf(id); + let index = previews.indexOf(id); if (index !== -1) { previews.splice(index, 1); } else { @@ -171,30 +195,24 @@ module.exports = React.createClass({ } } - var draft = PostStore.getCommentDraft(this.props.rootId); + let draft = PostStore.getCommentDraft(this.props.rootId); draft.previews = previews; draft.uploadsInProgress = uploadsInProgress; PostStore.storeCommentDraft(this.props.rootId, draft); this.setState({previews: previews, uploadsInProgress: uploadsInProgress}); - }, - getInitialState: function() { - PostStore.clearCommentDraftUploads(); - - var draft = PostStore.getCommentDraft(this.props.rootId); - return {messageText: draft['message'], uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews'], submitting: false}; - }, - componentWillReceiveProps: function(newProps) { + } + componentWillReceiveProps(newProps) { if (newProps.rootId !== this.props.rootId) { - var draft = PostStore.getCommentDraft(newProps.rootId); - this.setState({messageText: draft['message'], uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); + const draft = PostStore.getCommentDraft(newProps.rootId); + this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); } - }, - getFileCount: function(channelId) { + } + getFileCount() { return this.state.previews.length + this.state.uploadsInProgress.length; - }, - render: function() { - var serverError = null; + } + render() { + let serverError = null; if (this.state.serverError) { serverError = ( <div className='form-group has-error'> @@ -203,22 +221,23 @@ module.exports = React.createClass({ ); } - var postError = null; + let postError = null; if (this.state.postError) { postError = <label className='control-label'>{this.state.postError}</label>; } - var preview = null; + let preview = null; if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) { preview = ( <FilePreview files={this.state.previews} onRemove={this.removePreview} - uploadsInProgress={this.state.uploadsInProgress} /> + uploadsInProgress={this.state.uploadsInProgress} + /> ); } - var postFooterClassName = 'post-create-footer'; + let postFooterClassName = 'post-create-footer'; if (postError) { postFooterClassName += ' has-error'; } @@ -226,7 +245,10 @@ module.exports = React.createClass({ return ( <form onSubmit={this.handleSubmit}> <div className='post-create'> - <div id={this.props.rootId} className='post-create-body comment-create-body'> + <div + id={this.props.rootId} + className='post-create-body comment-create-body' + > <Textbox onUserInput={this.handleUserInput} onKeyPress={this.commentMsgKeyPress} @@ -234,7 +256,8 @@ module.exports = React.createClass({ createMessage='Add a comment...' initialText='' id='reply_textbox' - ref='textbox' /> + ref='textbox' + /> <FileUpload ref='fileUpload' getFileCount={this.getFileCount} @@ -242,11 +265,20 @@ module.exports = React.createClass({ onFileUpload={this.handleFileUploadComplete} onUploadError={this.handleUploadError} postType='comment' - channelId={this.props.channelId} /> + channelId={this.props.channelId} + /> </div> - <MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} /> + <MsgTyping + channelId={this.props.channelId} + parentId={this.props.rootId} + /> <div className={postFooterClassName}> - <input type='button' className='btn btn-primary comment-btn pull-right' value='Add Comment' onClick={this.handleSubmit} /> + <input + type='button' + className='btn btn-primary comment-btn pull-right' + value='Add Comment' + onClick={this.handleSubmit} + /> {postError} {serverError} </div> @@ -255,4 +287,9 @@ module.exports = React.createClass({ </form> ); } -}); +} + +CreateComment.propTypes = { + channelId: React.PropTypes.string.isRequired, + rootId: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index b9142223f..d9e67836d 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -1,33 +1,73 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var PostStore = require('../stores/post_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); -var MsgTyping = require('./msg_typing.jsx'); -var Textbox = require('./textbox.jsx'); -var FileUpload = require('./file_upload.jsx'); -var FilePreview = require('./file_preview.jsx'); -var utils = require('../utils/utils.jsx'); - -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; - -module.exports = React.createClass({ - displayName: 'CreatePost', - lastTime: 0, - handleSubmit: function(e) { +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const PostStore = require('../stores/post_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); +const MsgTyping = require('./msg_typing.jsx'); +const Textbox = require('./textbox.jsx'); +const FileUpload = require('./file_upload.jsx'); +const FilePreview = require('./file_preview.jsx'); +const Utils = require('../utils/utils.jsx'); + +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; + +export default class CreatePost extends React.Component { + constructor(props) { + super(props); + + this.lastTime = 0; + + this.handleSubmit = this.handleSubmit.bind(this); + this.postMsgKeyPress = this.postMsgKeyPress.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + this.resizePostHolder = this.resizePostHolder.bind(this); + this.handleUploadStart = this.handleUploadStart.bind(this); + this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this); + this.handleUploadError = this.handleUploadError.bind(this); + this.removePreview = this.removePreview.bind(this); + this.onChange = this.onChange.bind(this); + this.getFileCount = this.getFileCount.bind(this); + + PostStore.clearDraftUploads(); + + const draft = PostStore.getCurrentDraft(); + let previews = []; + let messageText = ''; + let uploadsInProgress = []; + if (draft && draft.previews && draft.message) { + previews = draft.previews; + messageText = draft.message; + uploadsInProgress = draft.uploadsInProgress; + } + + this.state = { + channelId: ChannelStore.getCurrentId(), + messageText: messageText, + uploadsInProgress: uploadsInProgress, + previews: previews, + submitting: false, + initialText: messageText + }; + } + componentDidUpdate(prevProps, prevState) { + if (prevState.previews.length !== this.state.previews.length) { + this.resizePostHolder(); + } + } + handleSubmit(e) { e.preventDefault(); if (this.state.uploadsInProgress.length > 0 || this.state.submitting) { return; } - var post = {}; + let post = {}; post.filenames = []; post.message = this.state.messageText; @@ -36,18 +76,18 @@ module.exports = React.createClass({ } if (post.message.length > Constants.CHARACTER_LIMIT) { - this.setState({postError: 'Post length must be less than ' + Constants.CHARACTER_LIMIT + ' characters.'}); + this.setState({postError: `Post length must be less than ${Constants.CHARACTER_LIMIT} characters.`}); return; } this.setState({submitting: true, serverError: null}); if (post.message.indexOf('/') === 0) { - client.executeCommand( + Client.executeCommand( this.state.channelId, post.message, false, - function(data) { + function handleCommandSuccess(data) { PostStore.storeDraft(data.channel_id, null); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); @@ -55,8 +95,8 @@ module.exports = React.createClass({ window.location.href = data.goto_location; } }.bind(this), - function(err) { - var state = {}; + function handleCommandError(err) { + let state = {}; state.serverError = err.message; state.submitting = false; this.setState(state); @@ -66,26 +106,25 @@ module.exports = React.createClass({ post.channel_id = this.state.channelId; post.filenames = this.state.previews; - var time = utils.getTimestamp(); - var userId = UserStore.getCurrentId(); - post.pending_post_id = userId + ':' + time; + const time = Utils.getTimestamp(); + const userId = UserStore.getCurrentId(); + post.pending_post_id = `${userId}:${time}`; post.user_id = userId; post.create_at = time; post.root_id = this.state.rootId; post.parent_id = this.state.parentId; - var channel = ChannelStore.get(this.state.channelId); + const channel = ChannelStore.get(this.state.channelId); PostStore.storePendingPost(post); PostStore.storeDraft(channel.id, null); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); - client.createPost(post, channel, - function(data) { - this.resizePostHolder(); + Client.createPost(post, channel, + function handlePostSuccess(data) { AsyncClient.getPosts(); - var member = ChannelStore.getMember(channel.id); + let member = ChannelStore.getMember(channel.id); member.msg_count = channel.total_msg_count; member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); @@ -94,9 +133,9 @@ module.exports = React.createClass({ type: ActionTypes.RECIEVED_POST, post: data }); - }.bind(this), - function(err) { - var state = {}; + }, + function handlePostError(err) { + let state = {}; if (err.message === 'Invalid RootId parameter') { if ($('#post_deleted').length > 0) { @@ -113,83 +152,79 @@ module.exports = React.createClass({ }.bind(this) ); } - }, - componentDidUpdate: function() { - this.resizePostHolder(); - }, - postMsgKeyPress: function(e) { + } + postMsgKeyPress(e) { if (e.which === 13 && !e.shiftKey && !e.altKey) { e.preventDefault(); - this.refs.textbox.getDOMNode().blur(); + React.findDOMNode(this.refs.textbox).blur(); this.handleSubmit(e); } - var t = Date.now(); + const t = Date.now(); if ((t - this.lastTime) > 5000) { - SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {'parent_id': ''}, state: {}}); + SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}}); this.lastTime = t; } - }, - handleUserInput: function(messageText) { - this.resizePostHolder(); + } + handleUserInput(messageText) { this.setState({messageText: messageText}); - var draft = PostStore.getCurrentDraft(); - draft['message'] = messageText; + let draft = PostStore.getCurrentDraft(); + draft.message = messageText; PostStore.storeCurrentDraft(draft); - }, - resizePostHolder: function() { - var height = $(window).height() - $(this.refs.topDiv.getDOMNode()).height() - $('#error_bar').outerHeight() - 50; - $('.post-list-holder-by-time').css('height', height + 'px'); + } + resizePostHolder() { + const height = $(window).height() - $(React.findDOMNode(this.refs.topDiv)).height() - $('#error_bar').outerHeight() - 50; + $('.post-list-holder-by-time').css('height', `${height}px`); $(window).trigger('resize'); - }, - handleUploadStart: function(clientIds, channelId) { - var draft = PostStore.getDraft(channelId); + } + handleUploadStart(clientIds, channelId) { + let draft = PostStore.getDraft(channelId); - draft['uploadsInProgress'] = draft['uploadsInProgress'].concat(clientIds); + draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds); PostStore.storeDraft(channelId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress']}); - }, - handleFileUploadComplete: function(filenames, clientIds, channelId) { - var draft = PostStore.getDraft(channelId); + this.setState({uploadsInProgress: draft.uploadsInProgress}); + } + handleFileUploadComplete(filenames, clientIds, channelId) { + let draft = PostStore.getDraft(channelId); // remove each finished file from uploads - for (var i = 0; i < clientIds.length; i++) { - var index = draft['uploadsInProgress'].indexOf(clientIds[i]); + for (let i = 0; i < clientIds.length; i++) { + const index = draft.uploadsInProgress.indexOf(clientIds[i]); if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); + draft.uploadsInProgress.splice(index, 1); } } - draft['previews'] = draft['previews'].concat(filenames); + draft.previews = draft.previews.concat(filenames); PostStore.storeDraft(channelId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']}); - }, - handleUploadError: function(err, clientId) { + this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews}); + } + handleUploadError(err, clientId) { if (clientId !== -1) { - var draft = PostStore.getDraft(this.state.channelId); + let draft = PostStore.getDraft(this.state.channelId); - var index = draft['uploadsInProgress'].indexOf(clientId); + const index = draft.uploadsInProgress.indexOf(clientId); if (index !== -1) { - draft['uploadsInProgress'].splice(index, 1); + draft.uploadsInProgress.splice(index, 1); } PostStore.storeDraft(this.state.channelId, draft); - this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err}); + this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err}); } else { this.setState({serverError: err}); } - }, - removePreview: function(id) { - var previews = this.state.previews; - var uploadsInProgress = this.state.uploadsInProgress; + } + removePreview(id) { + let previews = this.state.previews; + let uploadsInProgress = this.state.uploadsInProgress; // id can either be the path of an uploaded file or the client id of an in progress upload - var index = previews.indexOf(id); + let index = previews.indexOf(id); if (index !== -1) { previews.splice(index, 1); } else { @@ -201,28 +236,28 @@ module.exports = React.createClass({ } } - var draft = PostStore.getCurrentDraft(); - draft['previews'] = previews; - draft['uploadsInProgress'] = uploadsInProgress; + let draft = PostStore.getCurrentDraft(); + draft.previews = previews; + draft.uploadsInProgress = uploadsInProgress; PostStore.storeCurrentDraft(draft); this.setState({previews: previews, uploadsInProgress: uploadsInProgress}); - }, - componentDidMount: function() { - ChannelStore.addChangeListener(this._onChange); + } + componentDidMount() { + ChannelStore.addChangeListener(this.onChange); this.resizePostHolder(); - }, - componentWillUnmount: function() { - ChannelStore.removeChangeListener(this._onChange); - }, - _onChange: function() { - var channelId = ChannelStore.getCurrentId(); + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.onChange); + } + onChange() { + const channelId = ChannelStore.getCurrentId(); if (this.state.channelId !== channelId) { - var draft = PostStore.getCurrentDraft(); + let draft = PostStore.getCurrentDraft(); - var previews = []; - var messageText = ''; - var uploadsInProgress = []; + let previews = []; + let messageText = ''; + let uploadsInProgress = []; if (draft && draft.previews && draft.message) { previews = draft.previews; messageText = draft.message; @@ -231,33 +266,17 @@ module.exports = React.createClass({ this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress}); } - }, - getInitialState: function() { - PostStore.clearDraftUploads(); - - var draft = PostStore.getCurrentDraft(); - var previews = []; - var messageText = ''; - var uploadsInProgress = []; - if (draft && draft.previews && draft.message) { - previews = draft.previews; - messageText = draft.message; - uploadsInProgress = draft.uploadsInProgress; - } - - return {channelId: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: uploadsInProgress, previews: previews, submitting: false, initialText: messageText}; - }, - getFileCount: function(channelId) { + } + getFileCount(channelId) { if (channelId === this.state.channelId) { return this.state.previews.length + this.state.uploadsInProgress.length; - } else { - var draft = PostStore.getDraft(channelId); - - return draft['previews'].length + draft['uploadsInProgress'].length; } - }, - render: function() { - var serverError = null; + + const draft = PostStore.getDraft(channelId); + return draft.previews.length + draft.uploadsInProgress.length; + } + render() { + let serverError = null; if (this.state.serverError) { serverError = ( <div className='has-error'> @@ -266,55 +285,75 @@ module.exports = React.createClass({ ); } - var postError = null; + let postError = null; if (this.state.postError) { postError = <label className='control-label'>{this.state.postError}</label>; } - var preview = null; + let preview = null; if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) { preview = ( <FilePreview files={this.state.previews} onRemove={this.removePreview} - uploadsInProgress={this.state.uploadsInProgress} /> + uploadsInProgress={this.state.uploadsInProgress} + /> ); } - var postFooterClassName = 'post-create-footer'; + let postFooterClassName = 'post-create-footer'; if (postError) { postFooterClassName += ' has-error'; } return ( - <form id='create_post' ref='topDiv' role='form' onSubmit={this.handleSubmit}> + <form + id='create_post' + ref='topDiv' + role='form' + onSubmit={this.handleSubmit} + > <div className='post-create'> <div className='post-create-body'> - <Textbox - onUserInput={this.handleUserInput} - onKeyPress={this.postMsgKeyPress} - messageText={this.state.messageText} - createMessage='Write a message...' - channelId={this.state.channelId} - id='post_textbox' - ref='textbox' /> - <FileUpload - ref='fileUpload' - getFileCount={this.getFileCount} - onUploadStart={this.handleUploadStart} - onFileUpload={this.handleFileUploadComplete} - onUploadError={this.handleUploadError} - postType='post' - channelId='' /> + <div className='post-body__cell'> + <Textbox + onUserInput={this.handleUserInput} + onKeyPress={this.postMsgKeyPress} + onHeightChange={this.resizePostHolder} + messageText={this.state.messageText} + createMessage='Write a message...' + channelId={this.state.channelId} + id='post_textbox' + ref='textbox' + /> + <FileUpload + ref='fileUpload' + getFileCount={this.getFileCount} + onUploadStart={this.handleUploadStart} + onFileUpload={this.handleFileUploadComplete} + onUploadError={this.handleUploadError} + postType='post' + channelId='' + /> + </div> + <a + className='send-button theme' + onClick={this.handleSubmit} + > + <i className='fa fa-paper-plane' /> + </a> </div> <div className={postFooterClassName}> {postError} {serverError} {preview} - <MsgTyping channelId={this.state.channelId} parentId=''/> + <MsgTyping + channelId={this.state.channelId} + parentId='' + /> </div> </div> </form> ); } -}); +} diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index 589737271..4efb9cb23 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -1,58 +1,99 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var Client =require('../utils/client.jsx'); -var AsyncClient =require('../utils/async_client.jsx'); -var ChannelStore =require('../stores/channel_store.jsx') +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); -module.exports = React.createClass({ - handleDelete: function(e) { - if (this.state.channel_id.length != 26) return; +export default class DeleteChannelModal extends React.Component { + constructor(props) { + super(props); - Client.deleteChannel(this.state.channel_id, - function(data) { + this.handleDelete = this.handleDelete.bind(this); + + this.state = { + title: '', + channelId: '' + }; + } + handleDelete() { + if (this.state.channelId.length !== 26) { + return; + } + + Client.deleteChannel(this.state.channelId, + function handleDeleteSuccess() { AsyncClient.getChannels(true); window.location.href = '/'; - }.bind(this), - function(err) { - AsyncClient.dispatchError(err, "handleDelete"); - }.bind(this) + }, + function handleDeleteError(err) { + AsyncClient.dispatchError(err, 'handleDelete'); + } ); - }, - componentDidMount: function() { - var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + } + componentDidMount() { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow(e) { var button = $(e.relatedTarget); - self.setState({ title: button.attr('data-title'), channel_id: button.attr('data-channelid') }); - }); - }, - getInitialState: function() { - return { title: "", channel_id: "" }; - }, - render: function() { - - var channelType = ChannelStore.getCurrent() && ChannelStore.getCurrent().type === 'P' ? "private group" : "channel" + this.setState({ + title: button.attr('data-title'), + channelId: button.attr('data-channelid') + }); + }.bind(this)); + } + render() { + const channel = ChannelStore.getCurrent(); + let channelType = 'channel'; + if (channel && channel.type === 'P') { + channelType = 'private group'; + } return ( - <div className="modal fade" ref="modal" id="delete_channel" role="dialog" tabIndex="-1" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title">Confirm DELETE Channel</h4> - </div> - <div className="modal-body"> - <p> - Are you sure you wish to delete the {this.state.title} {channelType}? - </p> - </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" className="btn btn-danger" data-dismiss="modal" onClick={this.handleDelete}>Delete</button> - </div> + <div + className='modal fade' + ref='modal' + id='delete_channel' + role='dialog' + tabIndex='-1' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 className='modal-title'>Confirm DELETE Channel</h4> + </div> + <div className='modal-body'> + <p> + Are you sure you wish to delete the {this.state.title} {channelType}? + </p> + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + type='button' + className='btn btn-danger' + data-dismiss='modal' + onClick={this.handleDelete} + > + Delete + </button> + </div> + </div> </div> - </div> </div> ); } -}); +} diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 55d6f509c..075f9c742 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -4,20 +4,28 @@ var Client = require('../utils/client.jsx'); var PostStore = require('../stores/post_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -module.exports = React.createClass({ - handleDelete: function(e) { - Client.deletePost(this.state.channel_id, this.state.post_id, - function(data) { - var selected_list = this.state.selectedList; - if (selected_list && selected_list.order && selected_list.order.length > 0) { - var selected_post = selected_list.posts[selected_list.order[0]]; - if ((selected_post.id === this.state.post_id && this.state.title === "Post") || selected_post.root_id === this.state.post_id) { +export default class DeletePostModal extends React.Component { + constructor(props) { + super(props); + + this.handleDelete = this.handleDelete.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + + this.state = {title: '', postId: '', channelId: '', selectedList: PostStore.getSelectedPost(), comments: 0}; + } + handleDelete() { + Client.deletePost(this.state.channelId, this.state.postId, + function deleteSuccess() { + var selectedList = this.state.selectedList; + if (selectedList && selectedList.order && selectedList.order.length > 0) { + var selectedPost = selectedList.posts[selectedList.order[0]]; + if ((selectedPost.id === this.state.postId && this.state.title === 'Post') || selectedPost.root_id === this.state.postId) { AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_SEARCH, results: null @@ -27,14 +35,14 @@ module.exports = React.createClass({ type: ActionTypes.RECIEVED_POST_SELECTED, results: null }); - } else if (selected_post.id === this.state.post_id && this.state.title === "Comment") { - if (selected_post.root_id && selected_post.root_id.length > 0 && selected_list.posts[selected_post.root_id]) { - selected_list.order = [selected_post.root_id]; - delete selected_list.posts[selected_post.id]; + } else if (selectedPost.id === this.state.postId && this.state.title === 'Comment') { + if (selectedPost.root_id && selectedPost.root_id.length > 0 && selectedList.posts[selectedPost.root_id]) { + selectedList.order = [selectedPost.root_id]; + delete selectedList.posts[selectedPost.id]; AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_POST_SELECTED, - post_list: selected_list + post_list: selectedList }); AppDispatcher.handleServerAction({ @@ -44,67 +52,97 @@ module.exports = React.createClass({ } } } - PostStore.removePost(this.state.post_id, this.state.channel_id); - AsyncClient.getPosts(this.state.channel_id); + PostStore.removePost(this.state.postId, this.state.channelId); + AsyncClient.getPosts(this.state.channelId); }.bind(this), - function(err) { - AsyncClient.dispatchError(err, "deletePost"); - }.bind(this) + function deleteFailed(err) { + AsyncClient.dispatchError(err, 'deletePost'); + } ); - }, - componentDidMount: function() { - var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + } + componentDidMount() { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function freshOpen(e) { var newState = {}; - if(BrowserStore.getItem('edit_state_transfer')) { + if (BrowserStore.getItem('edit_state_transfer')) { newState = BrowserStore.getItem('edit_state_transfer'); BrowserStore.removeItem('edit_state_transfer'); } else { var button = e.relatedTarget; - newState = { title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments') }; + newState = {title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), postId: $(button).attr('data-postid'), comments: $(button).attr('data-comments')}; } - self.setState(newState); - }); - PostStore.addSelectedPostChangeListener(this._onChange); - }, - componentWillUnmount: function() { - PostStore.removeSelectedPostChangeListener(this._onChange); - }, - _onChange: function() { + this.setState(newState); + }.bind(this)); + PostStore.addSelectedPostChangeListener(this.onListenerChange); + } + componentWillUnmount() { + PostStore.removeSelectedPostChangeListener(this.onListenerChange); + } + onListenerChange() { var newList = PostStore.getSelectedPost(); - if (!utils.areStatesEqual(this.state.selectedList, newList)) { - this.setState({ selectedList: newList }); + if (!Utils.areStatesEqual(this.state.selectedList, newList)) { + this.setState({selectedList: newList}); + } + } + render() { + var error = null; + if (this.state.error) { + error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; + } + + var commentWarning = ''; + if (this.state.comments > 0) { + commentWarning = 'This post has ' + this.state.comments + ' comment(s) on it.'; } - }, - getInitialState: function() { - return { title: "", post_id: "", channel_id: "", selectedList: PostStore.getSelectedPost(), comments: 0 }; - }, - render: function() { - var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null; return ( - <div className="modal fade" id="delete_post" ref="modal" role="dialog" tabIndex="-1" aria-hidden="true"> - <div className="modal-dialog modal-push-down"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title">Confirm {this.state.title} Delete</h4> + <div + className='modal fade' + id='delete_post' + ref='modal' + role='dialog' + tabIndex='-1' + aria-hidden='true' + > + <div className='modal-dialog modal-push-down'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 className='modal-title'>Confirm {this.state.title} Delete</h4> </div> - <div className="modal-body"> + <div className='modal-body'> Are you sure you want to delete the {this.state.title.toLowerCase()}? <br/> <br/> - { this.state.comments > 0 ? - "This post has " + this.state.comments + " comment(s) on it." - : "" } + {commentWarning} </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" className="btn btn-danger" data-dismiss="modal" onClick={this.handleDelete}>Delete</button> + {error} + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + type='button' + className='btn btn-danger' + data-dismiss='modal' + onClick={this.handleDelete} + > + Delete + </button> </div> </div> </div> </div> ); } -}); +} diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 76f0c2c4d..e93bab431 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -1,79 +1,142 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); -module.exports = React.createClass({ - handleEdit: function(e) { +export default class EditChannelModal extends React.Component { + constructor(props) { + super(props); + + this.handleEdit = this.handleEdit.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = { + description: '', + title: '', + channelId: '', + serverError: '' + }; + } + handleEdit() { var data = {}; - data["channel_id"] = this.state.channel_id; - if (data["channel_id"].length !== 26) return; - data["channel_description"] = this.state.description.trim(); + data.channel_id = this.state.channelId; + + if (data.channel_id.length !== 26) { + return; + } + + data.channel_description = this.state.description.trim(); Client.updateChannelDesc(data, - function(data) { - this.setState({ server_error: "" }); - AsyncClient.getChannel(this.state.channel_id); - $(this.refs.modal.getDOMNode()).modal('hide'); + function handleUpdateSuccess() { + this.setState({serverError: ''}); + AsyncClient.getChannel(this.state.channelId); + $(React.findDOMNode(this.refs.modal)).modal('hide'); }.bind(this), - function(err) { - if (err.message === "Invalid channel_description parameter") { - this.setState({ server_error: "This description is too long, please enter a shorter one" }); - } - else { - this.setState({ server_error: err.message }); + function handleUpdateError(err) { + if (err.message === 'Invalid channel_description parameter') { + this.setState({serverError: 'This description is too long, please enter a shorter one'}); + } else { + this.setState({serverError: err.message}); } }.bind(this) ); - }, - handleUserInput: function(e) { - this.setState({ description: e.target.value }); - }, - handleClose: function() { - this.setState({description: "", server_error: ""}); - }, - componentDidMount: function() { - var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { - var button = e.relatedTarget; - self.setState({ description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), server_error: "" }); - }); - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose) - }, - componentWillUnmount: function() { - $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose) - }, - getInitialState: function() { - return { description: "", title: "", channel_id: "" }; - }, - render: function() { - var server_error = this.state.server_error ? <div className='form-group has-error'><br/><label className='control-label'>{ this.state.server_error }</label></div> : null; + } + handleUserInput(e) { + this.setState({description: e.target.value}); + } + handleClose() { + this.setState({description: '', serverError: ''}); + } + componentDidMount() { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow(e) { + const button = e.relatedTarget; + this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); + }.bind(this)); + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); + } + render() { + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>; + } - var editTitle = <h4 className='modal-title' ref='title'>Edit Description</h4>; + var editTitle = ( + <h4 + className='modal-title' + ref='title' + > + Edit Description + </h4> + ); if (this.state.title) { - editTitle = <h4 className='modal-title' ref='title'>Edit Description for <span className='name'>{this.state.title}</span></h4>; + editTitle = ( + <h4 + className='modal-title' + ref='title' + > + Edit Description for <span className='name'>{this.state.title}</span> + </h4> + ); } return ( - <div className="modal fade" ref="modal" id="edit_channel" role="dialog" tabIndex="-1" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - {editTitle} - </div> - <div className="modal-body"> - <textarea className="form-control no-resize" rows="6" ref="channelDesc" maxLength="1024" value={this.state.description} onChange={this.handleUserInput}></textarea> - { server_error } - </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" className="btn btn-primary" onClick={this.handleEdit}>Save</button> - </div> + <div + className='modal fade' + ref='modal' + id='edit_channel' + role='dialog' + tabIndex='-1' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + {editTitle} + </div> + <div className='modal-body'> + <textarea + className='form-control no-resize' + rows='6' + ref='channelDesc' + maxLength='1024' + value={this.state.description} + onChange={this.handleUserInput} + /> + {serverError} + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + type='button' + className='btn btn-primary' + onClick={this.handleEdit} + > + Save + </button> + </div> + </div> </div> - </div> </div> ); } -}); +} diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 1c5a1ed5e..ad7bd30b5 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -3,13 +3,21 @@ var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -var Constants = require('../utils/constants.jsx'); -var utils = require('../utils/utils.jsx'); var Textbox = require('./textbox.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); -module.exports = React.createClass({ - handleEdit: function(e) { +export default class EditPostModal extends React.Component { + constructor() { + super(); + + this.handleEdit = this.handleEdit.bind(this); + this.handleEditInput = this.handleEditInput.bind(this); + this.handleEditKeyPress = this.handleEditKeyPress.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + + this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''}; + } + handleEdit() { var updatedPost = {}; updatedPost.message = this.state.editText.trim(); @@ -17,8 +25,8 @@ module.exports = React.createClass({ var tempState = this.state; delete tempState.editText; BrowserStore.setItem('edit_state_transfer', tempState); - $("#edit_post").modal('hide'); - $("#delete_post").modal('show'); + $('#edit_post').modal('hide'); + $('#delete_post').modal('show'); return; } @@ -26,79 +34,106 @@ module.exports = React.createClass({ updatedPost.channel_id = this.state.channel_id; Client.updatePost(updatedPost, - function(data) { + function success() { AsyncClient.getPosts(this.state.channel_id); window.scrollTo(0, 0); }.bind(this), - function(err) { - AsyncClient.dispatchError(err, "updatePost"); - }.bind(this) + function error(err) { + AsyncClient.dispatchError(err, 'updatePost'); + } ); - $("#edit_post").modal('hide'); + $('#edit_post').modal('hide'); $(this.state.refocusId).focus(); - }, - handleEditInput: function(editMessage) { + } + handleEditInput(editMessage) { this.setState({editText: editMessage}); - }, - handleEditKeyPress: function(e) { - if (e.which == 13 && !e.shiftKey && !e.altKey) { + } + handleEditKeyPress(e) { + if (e.which === 13 && !e.shiftKey && !e.altKey) { e.preventDefault(); - this.refs.editbox.getDOMNode().blur(); + React.findDOMNode(this.refs.editbox).blur(); this.handleEdit(e); } - }, - handleUserInput: function(e) { - this.setState({ editText: e.target.value }); - }, - componentDidMount: function() { + } + handleUserInput(e) { + this.setState({editText: e.target.value}); + } + componentDidMount() { var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({editText: "", title: "", channel_id: "", post_id: "", comments: 0, refocusId: "", error: ''}); + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function onHidden() { + self.setState({editText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: ''}); }); - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) { var button = e.relatedTarget; - self.setState({ editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid') }); + self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refoucsid')}); }); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function(e) { + $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() { self.refs.editbox.resize(); }); - }, - getInitialState: function() { - return { editText: "", title: "", post_id: "", channel_id: "", comments: 0, refocusId: "" }; - }, - render: function() { - var error = this.state.error ? <div className='form-group has-error'><br /><label className='control-label'>{ this.state.error }</label></div> : <div className='form-group'><br /></div>; + } + render() { + var error = (<div className='form-group'><br /></div>); + if (this.state.error) { + error = (<div className='form-group has-error'><br /><label className='control-label'>{this.state.error}</label></div>); + } return ( - <div className="modal fade edit-modal" ref="modal" id="edit_post" role="dialog" tabIndex="-1" aria-hidden="true"> - <div className="modal-dialog modal-push-down"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close" onClick={this.handleEditClose}><span aria-hidden="true">×</span></button> - <h4 className="modal-title">Edit {this.state.title}</h4> - </div> - <div className="edit-modal-body modal-body"> - <Textbox - onUserInput={this.handleEditInput} - onKeyPress={this.handleEditKeyPress} - messageText={this.state.editText} - createMessage="Edit the post..." - id="edit_textbox" - ref="editbox" - /> - { error } - </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button type="button" className="btn btn-primary" onClick={this.handleEdit}>Save</button> - </div> + <div + className='modal fade edit-modal' + ref='modal' + id='edit_post' + role='dialog' + tabIndex='-1' + aria-hidden='true' + > + <div className='modal-dialog modal-push-down'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + onClick={this.handleEditClose} + > + <span aria-hidden='true'>×</span> + </button> + <h4 className='modal-title'>Edit {this.state.title}</h4> + </div> + <div className='edit-modal-body modal-body'> + <Textbox + onUserInput={this.handleEditInput} + onKeyPress={this.handleEditKeyPress} + messageText={this.state.editText} + createMessage='Edit the post...' + id='edit_textbox' + ref='editbox' + /> + {error} + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + type='button' + className='btn btn-primary' + onClick={this.handleEdit} + > + Save + </button> + </div> + </div> </div> - </div> </div> ); } -}); +} diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 678eb9928..95948c8dd 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -1,35 +1,62 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - handleResend: function() { - window.location.href = window.location.href + "&resend=true" - }, - render: function() { - var title = ""; - var body = ""; - var resend = ""; - if (this.props.isVerified === "true") { - title = config.SiteName + " Email Verified"; - body = <p>Your email has been verified! Click <a href={this.props.teamURL + "?email=" + this.props.userEmail}>here</a> to log in.</p>; +import {config} from '../utils/config.js'; + +export default class EmailVerify extends React.Component { + constructor(props) { + super(props); + + this.handleResend = this.handleResend.bind(this); + + this.state = {}; + } + handleResend() { + window.location.href = window.location.href + '&resend=true'; + } + render() { + var title = ''; + var body = ''; + var resend = ''; + if (this.props.isVerified === 'true') { + title = config.SiteName + ' Email Verified'; + body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>; } else { - title = config.SiteName + " Email Not Verified"; + title = config.SiteName + ' Email Not Verified'; body = <p>Please verify your email address. Check your inbox for an email.</p>; - resend = <button onClick={this.handleResend} className="btn btn-primary">Resend Email</button> + resend = ( + <button + onClick={this.handleResend} + className='btn btn-primary' + > + Resend Email + </button> + ); } return ( - <div className="col-sm-offset-4 col-sm-4"> - <div className="panel panel-default"> - <div className="panel-heading"> - <h3 className="panel-title">{ title }</h3> + <div className='col-sm-offset-4 col-sm-4'> + <div className='panel panel-default'> + <div className='panel-heading'> + <h3 className='panel-title'>{title}</h3> </div> - <div className="panel-body"> - { body } - { resend } + <div className='panel-body'> + {body} + {resend} </div> </div> </div> ); } -}); +} + +EmailVerify.defaultProps = { + isVerified: 'false', + teamURL: '', + userEmail: '' +}; +EmailVerify.propTypes = { + isVerified: React.PropTypes.string, + teamURL: React.PropTypes.string, + userEmail: React.PropTypes.string +}; diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx index f7514a009..87d94a41d 100644 --- a/web/react/components/error_bar.jsx +++ b/web/react/components/error_bar.jsx @@ -7,32 +7,40 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -function getStateFromStores() { - var error = ErrorStore.getLastError(); - if (error && error.message !== "There appears to be a problem with your internet connection") { - return { message: error.message }; - } else { - return { message: null }; - } -} +export default class ErrorBar extends React.Component { + constructor() { + super(); -module.exports = React.createClass({ - displayName: 'ErrorBar', + this.onErrorChange = this.onErrorChange.bind(this); + this.handleClose = this.handleClose.bind(this); - componentDidMount: function() { - ErrorStore.addChangeListener(this._onChange); + this.state = this.getStateFromStores(); + if (this.state.message) { + setTimeout(this.handleClose, 10000); + } + } + getStateFromStores() { + var error = ErrorStore.getLastError(); + if (!error || error.message === 'There appears to be a problem with your internet connection') { + return {message: null}; + } + + return {message: error.message}; + } + componentDidMount() { + ErrorStore.addChangeListener(this.onErrorChange); $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight()); - $(window).resize(function() { + $(window).resize(function onResize() { if (this.state.message) { $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight()); } }.bind(this)); - }, - componentWillUnmount: function() { - ErrorStore.removeChangeListener(this._onChange); - }, - _onChange: function() { - var newState = getStateFromStores(); + } + componentWillUnmount() { + ErrorStore.removeChangeListener(this.onErrorChange); + } + onErrorChange() { + var newState = this.getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { if (newState.message) { setTimeout(this.handleClose, 10000); @@ -40,9 +48,11 @@ module.exports = React.createClass({ this.setState(newState); } - }, - handleClose: function(e) { - if (e) e.preventDefault(); + } + handleClose(e) { + if (e) { + e.preventDefault(); + } AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_ERROR, @@ -50,24 +60,23 @@ module.exports = React.createClass({ }); $('body').css('padding-top', '0'); - }, - getInitialState: function() { - var state = getStateFromStores(); - if (state.message) { - setTimeout(this.handleClose, 10000); - } - return state; - }, - render: function() { + } + render() { if (this.state.message) { return ( - <div className="error-bar"> + <div className='error-bar'> <span>{this.state.message}</span> - <a href="#" className="error-bar__close" onClick={this.handleClose}>×</a> + <a + href='#' + className='error-bar__close' + onClick={this.handleClose} + > + × + </a> </div> ); - } else { - return <div/>; } + + return <div/>; } -});
\ No newline at end of file +} diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 45e6c5e28..c9aa06a97 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -5,31 +5,24 @@ var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); -module.exports = React.createClass({ - displayName: "FileAttachment", - canSetState: false, - propTypes: { - // a list of file pathes displayed by the parent FileAttachmentList - filename: React.PropTypes.string.isRequired, - // the index of this attachment preview in the parent FileAttachmentList - index: React.PropTypes.number.isRequired, - // the identifier of the modal dialog used to preview files - modalId: React.PropTypes.string.isRequired, - // handler for when the thumbnail is clicked - handleImageClick: React.PropTypes.func - }, - getInitialState: function() { - return {fileSize: -1}; - }, - componentDidMount: function() { +export default class FileAttachment extends React.Component { + constructor(props) { + super(props); + + this.loadFiles = this.loadFiles.bind(this); + + this.canSetState = false; + this.state = {fileSize: -1}; + } + componentDidMount() { this.loadFiles(); - }, - componentDidUpdate: function(prevProps) { + } + componentDidUpdate(prevProps) { if (this.props.filename !== prevProps.filename) { this.loadFiles(); } - }, - loadFiles: function() { + } + loadFiles() { this.canSetState = true; var filename = this.props.filename; @@ -39,91 +32,97 @@ module.exports = React.createClass({ var type = utils.getFileType(fileInfo.ext); // This is a temporary patch to fix issue with old files using absolute paths - if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { - fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) { + fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1]; } - fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; - - if (type === "image") { - var self = this; - $('<img/>').attr('src', fileInfo.path+'_thumb.jpg').load(function(path, name){ return function() { - $(this).remove(); - if (name in self.refs) { - var imgDiv = self.refs[name].getDOMNode(); - - $(imgDiv).removeClass('post__load'); - $(imgDiv).addClass('post__image'); - - var width = this.width || $(this).width(); - var height = this.height || $(this).height(); - - if (width < Constants.THUMBNAIL_WIDTH - && height < Constants.THUMBNAIL_HEIGHT) { - $(imgDiv).addClass('small'); - } else { - $(imgDiv).addClass('normal'); + fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; + + if (type === 'image') { + var self = this; // Need this reference since we use the given "this" + $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { + return function loader() { + $(this).remove(); + if (name in self.refs) { + var imgDiv = React.findDOMNode(self.refs[name]); + + $(imgDiv).removeClass('post__load'); + $(imgDiv).addClass('post__image'); + + var width = this.width || $(this).width(); + var height = this.height || $(this).height(); + + if (width < Constants.THUMBNAIL_WIDTH && + height < Constants.THUMBNAIL_HEIGHT) { + $(imgDiv).addClass('small'); + } else { + $(imgDiv).addClass('normal'); + } + + var re1 = new RegExp(' ', 'g'); + var re2 = new RegExp('\\(', 'g'); + var re3 = new RegExp('\\)', 'g'); + var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); } - - var re1 = new RegExp(' ', 'g'); - var re2 = new RegExp('\\(', 'g'); - var re3 = new RegExp('\\)', 'g'); - var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url('+url+'_thumb.jpg)'); - } - }}(fileInfo.path, filename)); + }; + }(fileInfo.path, filename)); } } - }, - componentWillUnmount: function() { + } + componentWillUnmount() { // keep track of when this component is mounted so that we can asynchronously change state without worrying about whether or not we're mounted this.canSetState = false; - }, - shouldComponentUpdate: function(nextProps, nextState) { + } + shouldComponentUpdate(nextProps, nextState) { if (!utils.areStatesEqual(nextProps, this.props)) { return true; } // the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering - if (nextState.fileSize != this.state.fileSize) { + if (nextState.fileSize !== this.state.fileSize) { if (this.refs.fileSize) { // update the UI element to display the file size without re-rendering the whole component - this.refs.fileSize.getDOMNode().innerHTML = utils.fileSizeToString(nextState.fileSize); + React.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize); return false; - } else { - // we can't find the element that should hold the file size so we must not have rendered yet - return true; } - } else { + + // we can't find the element that should hold the file size so we must not have rendered yet return true; } - }, - render: function() { + + return true; + } + render() { var filename = this.props.filename; var fileInfo = utils.splitFileLocation(filename); + var fileUrl = utils.getFileUrl(filename); var type = utils.getFileType(fileInfo.ext); var thumbnail; - if (type === "image") { - thumbnail = <div ref={filename} className="post__load" style={{backgroundImage: 'url(/static/images/load.gif)'}}/>; + if (type === 'image') { + thumbnail = ( + <div + ref={filename} + className='post__load' + style={{backgroundImage: 'url(/static/images/load.gif)'}} + /> + ); } else { - thumbnail = <div className={"file-icon "+utils.getIconClassName(type)}/>; + thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>; } - var fileSizeString = ""; + var fileSizeString = ''; if (this.state.fileSize < 0) { - var self = this; - Client.getFileInfo( filename, - function(data) { - if (self.canSetState) { - self.setState({fileSize: parseInt(data["size"], 10)}); + function success(data) { + if (this.canSetState) { + this.setState({fileSize: parseInt(data.size, 10)}); } - }, - function(err) { - } + }.bind(this), + function error() {} ); } else { fileSizeString = utils.fileSizeToString(this.state.fileSize); @@ -132,25 +131,65 @@ module.exports = React.createClass({ var filenameString = decodeURIComponent(utils.getFileName(filename)); var trimmedFilename; if (filenameString.length > 35) { - trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + "..."; + trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + '...'; } else { trimmedFilename = filenameString; } return ( - <div className="post-image__column" key={filename}> - <a className="post-image__thumbnail" href="#" onClick={this.props.handleImageClick} - data-img-id={this.props.index} data-toggle="modal" data-target={"#" + this.props.modalId }> + <div + className='post-image__column' + key={filename} + > + <a className='post-image__thumbnail' + href='#' + onClick={this.props.handleImageClick} + data-img-id={this.props.index} + data-toggle='modal' + data-target={'#' + this.props.modalId} + > {thumbnail} </a> - <div className="post-image__details"> - <div data-toggle="tooltip" title={filenameString} className="post-image__name">{trimmedFilename}</div> + <div className='post-image__details'> + <a + href={fileUrl} + download={filenameString} + data-toggle='tooltip' + title={filenameString} + className='post-image__name' + > + {trimmedFilename} + </a> <div> - <span className="post-image__type">{fileInfo.ext.toUpperCase()}</span> - <span className="post-image__size">{fileSizeString}</span> + <a + href={fileUrl} + download={filenameString} + className='post-image__download' + > + <span + className='fa fa-download' + /> + </a> + <span className='post-image__type'>{fileInfo.ext.toUpperCase()}</span> + <span className='post-image__size'>{fileSizeString}</span> </div> </div> </div> ); } -}); +} + +FileAttachment.propTypes = { + + // a list of file pathes displayed by the parent FileAttachmentList + filename: React.PropTypes.string.isRequired, + + // the index of this attachment preview in the parent FileAttachmentList + index: React.PropTypes.number.isRequired, + + // the identifier of the modal dialog used to preview files + modalId: React.PropTypes.string.isRequired, + + // handler for when the thumbnail is clicked + handleImageClick: React.PropTypes.func +}; diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx index df4424d03..abe72089a 100644 --- a/web/react/components/file_attachment_list.jsx +++ b/web/react/components/file_attachment_list.jsx @@ -5,33 +5,37 @@ var ViewImageModal = require('./view_image.jsx'); var FileAttachment = require('./file_attachment.jsx'); var Constants = require('../utils/constants.jsx'); -module.exports = React.createClass({ - displayName: "FileAttachmentList", - propTypes: { - // a list of file pathes displayed by this - filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, - // the identifier of the modal dialog used to preview files - modalId: React.PropTypes.string.isRequired, - // the channel that this is part of - channelId: React.PropTypes.string, - // the user that owns the post that this is attached to - userId: React.PropTypes.string - }, - getInitialState: function() { - return {startImgId: 0}; - }, - render: function() { +export default class FileAttachmentList extends React.Component { + constructor(props) { + super(props); + + this.handleImageClick = this.handleImageClick.bind(this); + + this.state = {startImgId: 0}; + } + handleImageClick(e) { + this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'), 10)}); + } + render() { var filenames = this.props.filenames; var modalId = this.props.modalId; var postFiles = []; for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - postFiles.push(<FileAttachment key={i} filename={filenames[i]} index={i} modalId={modalId} handleImageClick={this.handleImageClick} />); + postFiles.push( + <FileAttachment + key={i} + filename={filenames[i]} + index={i} + modalId={modalId} + handleImageClick={this.handleImageClick} + /> + ); } return ( <div> - <div className="post-image__columns"> + <div className='post-image__columns'> {postFiles} </div> <ViewImageModal @@ -39,11 +43,24 @@ module.exports = React.createClass({ userId={this.props.userId} modalId={modalId} startId={this.state.startImgId} - filenames={filenames} /> + filenames={filenames} + /> </div> ); - }, - handleImageClick: function(e) { - this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))}); } -}); +} + +FileAttachmentList.propTypes = { + + // a list of file pathes displayed by this + filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + + // the identifier of the modal dialog used to preview files + modalId: React.PropTypes.string.isRequired, + + // the channel that this is part of + channelId: React.PropTypes.string, + + // the user that owns the post that this is attached to + userId: React.PropTypes.string +}; diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx index d1b2f734a..33382a439 100644 --- a/web/react/components/file_preview.jsx +++ b/web/react/components/file_preview.jsx @@ -1,14 +1,17 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - handleRemove: function(e) { +export default class FilePreview extends React.Component { + constructor(props) { + super(props); + + this.handleRemove = this.handleRemove.bind(this); + + this.state = {}; + } + handleRemove(e) { var previewDiv = e.target.parentNode.parentNode; if (previewDiv.hasAttribute('data-filename')) { @@ -16,51 +19,96 @@ module.exports = React.createClass({ } else if (previewDiv.hasAttribute('data-client-id')) { this.props.onRemove(previewDiv.getAttribute('data-client-id')); } - }, - render: function() { + } + render() { var previews = []; - this.props.files.forEach(function(filename) { - + this.props.files.forEach(function setupPreview(fullFilename) { + var filename = fullFilename; var originalFilename = filename; var filenameSplit = filename.split('.'); - var ext = filenameSplit[filenameSplit.length-1]; - var type = utils.getFileType(ext); + var ext = filenameSplit[filenameSplit.length - 1]; + var type = Utils.getFileType(ext); + // This is a temporary patch to fix issue with old files using absolute paths - if (filename.indexOf("/api/v1/files/get") != -1) { - filename = filename.split("/api/v1/files/get")[1]; + + if (filename.indexOf('/api/v1/files/get') !== -1) { + filename = filename.split('/api/v1/files/get')[1]; } - filename = utils.getWindowLocationOrigin() + "/api/v1/files/get" + filename; + filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename; - if (type === "image") { + if (type === 'image') { previews.push( - <div key={filename} className="preview-div" data-filename={originalFilename}> - <img className="preview-img" src={filename}/> - <a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a> + <div + key={filename} + className='preview-div' + data-filename={originalFilename} + > + <img + className='preview-img' + src={filename} + /> + <a + className='remove-preview' + onClick={this.handleRemove} + > + <i className='glyphicon glyphicon-remove'/> + </a> </div> ); } else { previews.push( - <div key={filename} className="preview-div custom-file" data-filename={originalFilename}> - <div className={"file-icon "+utils.getIconClassName(type)}/> - <a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a> + <div + key={filename} + className='preview-div custom-file' + data-filename={originalFilename} + > + <div className={'file-icon ' + Utils.getIconClassName(type)}/> + <a + className='remove-preview' + onClick={this.handleRemove} + > + <i className='glyphicon glyphicon-remove'/> + </a> </div> ); } }.bind(this)); - this.props.uploadsInProgress.forEach(function(clientId) { + this.props.uploadsInProgress.forEach(function addUploadsInProgress(clientId) { previews.push( - <div className="preview-div" data-client-id={clientId}> - <img className="spinner" src="/static/images/load.gif"/> - <a className="remove-preview" onClick={this.handleRemove}><i className="glyphicon glyphicon-remove"/></a> + <div + key={clientId} + className='preview-div' + data-client-id={clientId} + > + <img + className='spinner' + src='/static/images/load.gif' + /> + <a + className='remove-preview' + onClick={this.handleRemove} + > + <i className='glyphicon glyphicon-remove'/> + </a> </div> ); }.bind(this)); return ( - <div className="preview-container"> + <div className='preview-container'> {previews} </div> ); } -}); +} + +FilePreview.defaultProps = { + files: null, + uploadsInProgress: null +}; +FilePreview.propTypes = { + onRemove: React.PropTypes.func.isRequired, + files: React.PropTypes.array, + uploadsInProgress: React.PropTypes.array +}; diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index e77982559..534f0136e 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -6,71 +6,64 @@ var Constants = require('../utils/constants.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - displayName: 'FileUpload', - propTypes: { - onUploadError: React.PropTypes.func, - getFileCount: React.PropTypes.func, - onFileUpload: React.PropTypes.func, - onUploadStart: React.PropTypes.func, - channelId: React.PropTypes.string, - postType: React.PropTypes.string - }, - getInitialState: function() { - return {requests: {}}; - }, - handleChange: function() { - var element = $(this.refs.fileInput.getDOMNode()); - var files = element.prop('files'); +export default class FileUpload extends React.Component { + constructor(props) { + super(props); - var channelId = this.props.channelId || ChannelStore.getCurrentId(); + this.uploadFiles = this.uploadFiles.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleDrop = this.handleDrop.bind(this); - this.props.onUploadError(null); + this.state = { + requests: {} + }; + } - // This looks redundant, but must be done this way due to - // setState being an asynchronous call - var numFiles = 0; - for (var i = 0; i < files.length; i++) { - if (files[i].size <= Constants.MAX_FILE_SIZE) { - numFiles++; - } + fileUploadSuccess(channelId, data) { + var parsedData = $.parseJSON(data); + this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); + + var requests = this.state.requests; + for (var j = 0; j < parsedData.client_ids.length; j++) { + delete requests[parsedData.client_ids[j]]; } + this.setState({requests: requests}); + } - var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId), numFiles); + fileUploadFail(clientId, err) { + this.props.onUploadError(err, clientId); + } - if (numFiles > numToUpload) { - this.props.onUploadError('Uploads limited to ' + Constants.MAX_UPLOAD_FILES + ' files maximum. Please use additional posts for more files.'); - } + uploadFiles(files) { + // clear any existing errors + this.props.onUploadError(null); + + var channelId = this.props.channelId || ChannelStore.getCurrentId(); + + var uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId); + var numUploads = 0; + + // keep track of how many files have been too large + var tooLargeFiles = []; - for (var i = 0; i < files.length && i < numToUpload; i++) { + for (let i = 0; i < files.length && numUploads < uploadsRemaining; i++) { if (files[i].size > Constants.MAX_FILE_SIZE) { - this.props.onUploadError('Files must be no more than ' + Constants.MAX_FILE_SIZE / 1000000 + ' MB'); + tooLargeFiles.push(files[i]); continue; } - // generate a unique id that can be used by other components to refer back to this file upload + // generate a unique id that can be used by other components to refer back to this upload var clientId = utils.generateId(); - // Prepare data to be uploaded. + // prepare data to be uploaded var formData = new FormData(); formData.append('channel_id', channelId); formData.append('files', files[i], files[i].name); formData.append('client_ids', clientId); var request = client.uploadFile(formData, - function(data) { - var parsedData = $.parseJSON(data); - this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); - - var requests = this.state.requests; - for (var j = 0; j < parsedData.client_ids.length; j++) { - delete requests[parsedData.client_ids[j]]; - } - this.setState({requests: requests}); - }.bind(this), - function(err) { - this.props.onUploadError(err, clientId); - }.bind(this) + this.fileUploadSuccess.bind(this, channelId), + this.fileUploadFail.bind(this, clientId) ); var requests = this.state.requests; @@ -78,8 +71,26 @@ module.exports = React.createClass({ this.setState({requests: requests}); this.props.onUploadStart([clientId], channelId); + + numUploads += 1; } + if (files.length > uploadsRemaining) { + this.props.onUploadError(`Uploads limited to ${Constants.MAX_UPLOAD_FILES} files maximum. Please use additional posts for more files.`); + } else if (tooLargeFiles.length > 1) { + var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', '); + + this.props.onUploadError(`Files above ${Constants.MAX_FILE_SIZE / 1000000}MB could not be uploaded: ${tooLargeFilenames}`); + } else if (tooLargeFiles.length > 0) { + this.props.onUploadError(`File above ${Constants.MAX_FILE_SIZE / 1000000}MB could not be uploaded: ${tooLargeFiles[0].name}`); + } + } + + handleChange() { + var element = $(React.findDOMNode(this.refs.fileInput)); + + this.uploadFiles(element.prop('files')); + // clear file input for all modern browsers try { element[0].value = ''; @@ -87,97 +98,56 @@ module.exports = React.createClass({ element[0].type = 'text'; element[0].type = 'file'; } - } catch(e) {} - }, - handleDrop: function(e) { + } catch(e) { + // Do nothing + } + } + + handleDrop(e) { this.props.onUploadError(null); var files = e.originalEvent.dataTransfer.files; - var channelId = this.props.channelId || ChannelStore.getCurrentId(); if (typeof files !== 'string' && files.length) { - var numFiles = files.length; - - var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId), numFiles); - - if (numFiles > numToUpload) { - this.props.onUploadError('Uploads limited to ' + Constants.MAX_UPLOAD_FILES + ' files maximum. Please use additional posts for more files.'); - } - - for (var i = 0; i < files.length && i < numToUpload; i++) { - if (files[i].size > Constants.MAX_FILE_SIZE) { - this.props.onUploadError('Files must be no more than ' + Constants.MAX_FILE_SIZE / 1000000 + ' MB'); - continue; - } - - // generate a unique id that can be used by other components to refer back to this file upload - var clientId = utils.generateId(); - - // Prepare data to be uploaded. - var formData = new FormData(); - formData.append('channel_id', channelId); - formData.append('files', files[i], files[i].name); - formData.append('client_ids', clientId); - - var request = client.uploadFile(formData, - function(data) { - var parsedData = $.parseJSON(data); - this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); - - var requests = this.state.requests; - for (var j = 0; j < parsedData.client_ids.length; j++) { - delete requests[parsedData.client_ids[j]]; - } - this.setState({requests: requests}); - }.bind(this), - function(err) { - this.props.onUploadError(err, clientId); - }.bind(this) - ); - - var requests = this.state.requests; - requests[clientId] = request; - this.setState({requests: requests}); - - this.props.onUploadStart([clientId], channelId); - } + this.uploadFiles(files); } else { this.props.onUploadError('Invalid file upload', -1); } - }, - componentDidMount: function() { - var inputDiv = this.refs.input.getDOMNode(); + } + + componentDidMount() { + var inputDiv = React.findDOMNode(this.refs.input); var self = this; if (this.props.postType === 'post') { $('.row.main').dragster({ - enter: function() { + enter() { $('.center-file-overlay').removeClass('hidden'); }, - leave: function() { + leave() { $('.center-file-overlay').addClass('hidden'); }, - drop: function(dragsterEvent, e) { + drop(dragsterEvent, e) { $('.center-file-overlay').addClass('hidden'); self.handleDrop(e); } }); } else if (this.props.postType === 'comment') { $('.post-right__container').dragster({ - enter: function() { + enter() { $('.right-file-overlay').removeClass('hidden'); }, - leave: function() { + leave() { $('.right-file-overlay').addClass('hidden'); }, - drop: function(dragsterEvent, e) { + drop(dragsterEvent, e) { $('.right-file-overlay').addClass('hidden'); self.handleDrop(e); } }); } - document.addEventListener('paste', function(e) { + document.addEventListener('paste', function handlePaste(e) { var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0]; if (textarea !== e.target && !$.contains(textarea, e.target)) { @@ -191,7 +161,7 @@ module.exports = React.createClass({ var items = e.clipboardData.items; var numItems = 0; if (items) { - for (var i = 0; i < items.length; i++) { + for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { var testExt = items[i].type.split('/')[1].toLowerCase(); @@ -245,19 +215,8 @@ module.exports = React.createClass({ formData.append('client_ids', clientId); var request = client.uploadFile(formData, - function(data) { - var parsedData = $.parseJSON(data); - self.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId); - - var requests = self.state.requests; - for (var j = 0; j < parsedData.client_ids.length; j++) { - delete requests[parsedData.client_ids[j]]; - } - self.setState({requests: requests}); - }, - function(err) { - self.props.onUploadError(err, clientId); - } + self.fileUploadSuccess.bind(self, channelId), + self.fileUploadFail.bind(self, clientId) ); var requests = self.state.requests; @@ -269,8 +228,9 @@ module.exports = React.createClass({ } } }); - }, - cancelUpload: function(clientId) { + } + + cancelUpload(clientId) { var requests = this.state.requests; var request = requests[clientId]; @@ -280,15 +240,33 @@ module.exports = React.createClass({ delete requests[clientId]; this.setState({requests: requests}); } - }, - render: function() { + } + + render() { return ( - <span ref='input' className='btn btn-file'> + <span + ref='input' + className='btn btn-file' + > <span> <i className='glyphicon glyphicon-paperclip' /> </span> - <input ref='fileInput' type='file' onChange={this.handleChange} multiple/> + <input + ref='fileInput' + type='file' + onChange={this.handleChange} + multiple='true' + /> </span> ); } -}); +} + +FileUpload.propTypes = { + onUploadError: React.PropTypes.func, + getFileCount: React.PropTypes.func, + onFileUpload: React.PropTypes.func, + onUploadStart: React.PropTypes.func, + channelId: React.PropTypes.string, + postType: React.PropTypes.string +}; diff --git a/web/react/components/file_upload_overlay.jsx b/web/react/components/file_upload_overlay.jsx index f35556371..265924206 100644 --- a/web/react/components/file_upload_overlay.jsx +++ b/web/react/components/file_upload_overlay.jsx @@ -1,12 +1,8 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - displayName: 'FileUploadOverlay', - propTypes: { - overlayType: React.PropTypes.string - }, - render: function() { +export default class FileUploadOverlay extends React.Component { + render() { var overlayClass = 'file-overlay hidden'; if (this.props.overlayType === 'right') { overlayClass += ' right-file-overlay'; @@ -23,4 +19,8 @@ module.exports = React.createClass({ </div> ); } -}); +} + +FileUploadOverlay.propTypes = { + overlayType: React.PropTypes.string +}; diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx index d896a1f12..52988886c 100644 --- a/web/react/components/find_team.jsx +++ b/web/react/components/find_team.jsx @@ -1,53 +1,58 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); +import {strings} from '../utils/config.js'; + +export default class FindTeam extends React.Component { + constructor(props) { + super(props); + this.state = {}; + + this.handleSubmit = this.handleSubmit.bind(this); + } -module.exports = React.createClass({ - handleSubmit: function(e) { + handleSubmit(e) { e.preventDefault(); var state = { }; - var email = this.refs.email.getDOMNode().value.trim().toLowerCase(); + var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase(); if (!email || !utils.isEmail(email)) { - state.email_error = "Please enter a valid email address"; + state.email_error = 'Please enter a valid email address'; this.setState(state); return; } - else { - state.email_error = ""; - } + + state.email_error = ''; client.findTeamsSendEmail(email, - function(data) { + function success() { state.sent = true; this.setState(state); }.bind(this), - function(err) { + function fail(err) { state.email_error = err.message; this.setState(state); }.bind(this) ); - }, - getInitialState: function() { - return { }; - }, - render: function() { + } - var email_error = this.state.email_error ? <label className='control-label'>{ this.state.email_error }</label> : null; + render() { + var emailError = null; + var emailErrorClass = 'form-group'; - var divStyle = { - "marginTop": "50px", + if (this.state.email_error) { + emailError = <label className='control-label'>{this.state.email_error}</label>; + emailErrorClass = 'form-group has-error'; } if (this.state.sent) { return ( <div> - <h4>{"Find Your " + utils.toTitleCase(strings.Team)}</h4> - <p>{"An email was sent with links to any " + strings.TeamPlural + " to which you are a member."}</p> + <h4>{'Find Your ' + utils.toTitleCase(strings.Team)}</h4> + <p>{'An email was sent with links to any ' + strings.TeamPlural + ' to which you are a member.'}</p> </div> ); } @@ -56,17 +61,28 @@ module.exports = React.createClass({ <div> <h4>Find Your Team</h4> <form onSubmit={this.handleSubmit}> - <p>{"Get an email with links to any " + strings.TeamPlural + " to which you are a member."}</p> - <div className="form-group"> + <p>{'Get an email with links to any ' + strings.TeamPlural + ' to which you are a member.'}</p> + <div className='form-group'> <label className='control-label'>Email</label> - <div className={ email_error ? "form-group has-error" : "form-group" }> - <input type="text" ref="email" className="form-control" placeholder="you@domain.com" maxLength="128" /> - { email_error } + <div className={emailErrorClass}> + <input + type='text' + ref='email' + className='form-control' + placeholder='you@domain.com' + maxLength='128' + /> + {emailError} </div> </div> - <button className="btn btn-md btn-primary" type="submit">Send</button> + <button + className='btn btn-md btn-primary' + type='submit' + > + Send + </button> </form> </div> ); } -}); +} diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 3b10926f5..1f25ea0b7 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -2,69 +2,115 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); -var ZeroClipboardMixin = require('react-zeroclipboard-mixin'); +import {strings} from '../utils/config.js'; -ZeroClipboardMixin.ZeroClipboard.config({ - swfPath: '../../static/flash/ZeroClipboard.swf' -}); +export default class GetLinkModal extends React.Component { + constructor(props) { + super(props); -module.exports = React.createClass({ - displayName: 'GetLinkModal', - zeroclipboardElementsSelector: '[data-copy-btn]', - mixins: [ZeroClipboardMixin], - componentDidMount: function() { - var self = this; + this.handleClick = this.handleClick.bind(this); + + this.state = {copiedLink: false}; + } + componentDidMount() { if (this.refs.modal) { - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { - var button = e.relatedTarget; - self.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')}); - }); - $(this.refs.modal.getDOMNode()).on('hide.bs.modal', function() { - self.setState({copiedLink: false}); - }); + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) { + var button = e.relatedTarget; + this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')}); + }.bind(this)); + $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function hide() { + this.setState({copiedLink: false}); + }.bind(this)); + } + } + handleClick() { + var copyTextarea = $(React.findDOMNode(this.refs.textarea)); + copyTextarea.select(); + + try { + var successful = document.execCommand('copy'); + if (successful) { + this.setState({copiedLink: true}); + } else { + this.setState({copiedLink: false}); + } + } catch (err) { + this.setState({copiedLink: false}); } - }, - getInitialState: function() { - return {copiedLink: false}; - }, - handleClick: function() { - this.setState({copiedLink: true}); - }, - render: function() { + } + render() { var currentUser = UserStore.getCurrentUser(); var copyLinkConfirm = null; if (this.state.copiedLink) { - copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className="fa fa-check"></i> Link copied to clipboard.</p>; + copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> Link copied to clipboard.</p>; } if (currentUser != null) { return ( - <div className='modal fade' ref='modal' id='get_link' tabIndex='-1' role='dialog' aria-hidden='true'> - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' id='myModalLabel'>{this.state.title} Link</h4> - </div> - <div className='modal-body'> - <p> - Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site. - <br /><br /> - Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}. - </p> - <textarea className='form-control no-resize' readOnly='true' value={this.state.value}></textarea> - </div> - <div className='modal-footer'> - <button type='button' className='btn btn-default' data-dismiss='modal'>Close</button> - <button data-copy-btn='true' type='button' className='btn btn-primary pull-left' onClick={this.handleClick} data-clipboard-text={this.state.value}>Copy Link</button> - {copyLinkConfirm} + <div + className='modal fade' + ref='modal' + id='get_link' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + > + {this.state.title} Link + </h4> + </div> + <div className='modal-body'> + <p> + Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site. + <br /><br /> + Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}. + </p> + <textarea + className='form-control no-resize' + readOnly='true' + ref='textarea' + value={this.state.value} + /> + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Close + </button> + <button + data-copy-btn='true' + type='button' + className='btn btn-primary pull-left' + onClick={this.handleClick} + data-clipboard-text={this.state.value} + > + Copy Link + </button> + {copyLinkConfirm} + </div> </div> - </div> - </div> + </div> </div> ); } return <div/>; } -}); +} diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 5b6924891..c1cfa7800 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -6,11 +6,30 @@ var ConfigStore = require('../stores/config_store.jsx'); var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); +import {config} from '../utils/config.js'; -module.exports = React.createClass({ - componentDidMount: function() { +export default class InviteMemberModal extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.addInviteFields = this.addInviteFields.bind(this); + this.clearFields = this.clearFields.bind(this); + this.removeInviteFields = this.removeInviteFields.bind(this); + + this.state = { + inviteIds: [0], + idCount: 0, + emailErrors: {}, + firstNameErrors: {}, + lastNameErrors: {}, + emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) + }; + } + + componentDidMount() { var self = this; - $('#invite_member').on('hide.bs.modal', function(e) { + $('#invite_member').on('hide.bs.modal', function hide(e) { if ($('#invite_member').attr('data-confirm') === 'true') { $('#invite_member').attr('data-confirm', 'false'); return; @@ -19,7 +38,7 @@ module.exports = React.createClass({ var notEmpty = false; for (var i = 0; i < self.state.inviteIds.length; i++) { var index = self.state.inviteIds[i]; - if (self.refs['email' + index].getDOMNode().value.trim() !== '') { + if (React.findDOMNode(self.refs['email' + index]).value.trim() !== '') { notEmpty = true; break; } @@ -31,11 +50,12 @@ module.exports = React.createClass({ } }); - $('#invite_member').on('hidden.bs.modal', function() { + $('#invite_member').on('hidden.bs.modal', function show() { self.clearFields(); }); - }, - handleSubmit: function(e) { + } + + handleSubmit() { if (!this.state.emailEnabled) { return; } @@ -51,7 +71,7 @@ module.exports = React.createClass({ for (var i = 0; i < count; i++) { var index = inviteIds[i]; var invite = {}; - invite.email = this.refs['email' + index].getDOMNode().value.trim(); + invite.email = React.findDOMNode(this.refs['email' + index]).value.trim(); if (!invite.email || !utils.isEmail(invite.email)) { emailErrors[index] = 'Please enter a valid email address'; valid = false; @@ -60,7 +80,7 @@ module.exports = React.createClass({ } if (config.AllowInviteNames) { - invite.firstName = this.refs['first_name' + index].getDOMNode().value.trim(); + invite.firstName = React.findDOMNode(this.refs['first_name' + index]).value.trim(); if (!invite.firstName && config.RequireInviteNames) { firstNameErrors[index] = 'This is a required field'; valid = false; @@ -68,7 +88,7 @@ module.exports = React.createClass({ firstNameErrors[index] = ''; } - invite.lastName = this.refs['last_name' + index].getDOMNode().value.trim(); + invite.lastName = React.findDOMNode(this.refs['last_name' + index]).value.trim(); if (!invite.lastName && config.RequireInviteNames) { lastNameErrors[index] = 'This is a required field'; valid = false; @@ -90,11 +110,11 @@ module.exports = React.createClass({ data.invites = invites; Client.inviteMembers(data, - function() { - $(this.refs.modal.getDOMNode()).attr('data-confirm', 'true'); - $(this.refs.modal.getDOMNode()).modal('hide'); + function success() { + $(React.findDOMNode(this.refs.modal)).attr('data-confirm', 'true'); + $(React.findDOMNode(this.refs.modal)).modal('hide'); }.bind(this), - function(err) { + function fail(err) { if (err.message === 'This person is already on your team') { emailErrors[err.detailed_error] = err.message; this.setState({emailErrors: emailErrors}); @@ -103,26 +123,29 @@ module.exports = React.createClass({ } }.bind(this) ); - }, - componentDidUpdate: function() { - $(this.refs.modalBody.getDOMNode()).css('max-height', $(window).height() - 200); - $(this.refs.modalBody.getDOMNode()).css('overflow-y', 'scroll'); - }, - addInviteFields: function() { + } + + componentDidUpdate() { + $(React.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); + $(React.findDOMNode(this.refs.modalBody)).css('overflow-y', 'scroll'); + } + + addInviteFields() { var count = this.state.idCount + 1; var inviteIds = this.state.inviteIds; inviteIds.push(count); this.setState({inviteIds: inviteIds, idCount: count}); - }, - clearFields: function() { + } + + clearFields() { var inviteIds = this.state.inviteIds; for (var i = 0; i < inviteIds.length; i++) { var index = inviteIds[i]; - this.refs['email' + index].getDOMNode().value = ''; + React.findDOMNode(this.refs['email' + index]).value = ''; if (config.AllowInviteNames) { - this.refs['first_name' + index].getDOMNode().value = ''; - this.refs['last_name' + index].getDOMNode().value = ''; + React.findDOMNode(this.refs['first_name' + index]).value = ''; + React.findDOMNode(this.refs['last_name' + index]).value = ''; } } @@ -133,8 +156,9 @@ module.exports = React.createClass({ firstNameErrors: {}, lastNameErrors: {} }); - }, - removeInviteFields: function(index) { + } + + removeInviteFields(index) { var count = this.state.idCount; var inviteIds = this.state.inviteIds; var i = inviteIds.indexOf(index); @@ -145,24 +169,10 @@ module.exports = React.createClass({ inviteIds.push(++count); } this.setState({inviteIds: inviteIds, idCount: count}); - }, - getInitialState: function() { - return { - inviteIds: [0], - idCount: 0, - emailErrors: {}, - firstNameErrors: {}, - lastNameErrors: {}, - emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) - }; - }, - render: function() { - var currentUser = UserStore.getCurrentUser(); + } - var inputDisabled = ''; - if (!this.state.emailEnabled) { - inputDisabled = 'disabled'; - } + render() { + var currentUser = UserStore.getCurrentUser(); if (currentUser != null) { var inviteSections = []; @@ -185,7 +195,13 @@ module.exports = React.createClass({ var removeButton = null; if (index) { removeButton = (<div> - <button type='button' className='btn btn-link remove__member' onClick={this.removeInviteFields.bind(this, index)}><span className='fa fa-trash'></span></button> + <button + type='button' + className='btn btn-link remove__member' + onClick={this.removeInviteFields.bind(this, index)} + > + <span className='fa fa-trash'></span> + </button> </div>); } var emailClass = 'form-group invite'; @@ -206,13 +222,27 @@ module.exports = React.createClass({ nameFields = (<div className='row--invite'> <div className='col-sm-6'> <div className={firstNameClass}> - <input type='text' className='form-control' ref={'first_name' + index} placeholder='First name' maxLength='64' disabled={!this.state.emailEnabled}/> + <input + type='text' + className='form-control' + ref={'first_name' + index} + placeholder='First name' + maxLength='64' + disabled={!this.state.emailEnabled} + /> {firstNameError} </div> </div> <div className='col-sm-6'> <div className={lastNameClass}> - <input type='text' className='form-control' ref={'last_name' + index} placeholder='Last name' maxLength='64' disabled={!this.state.emailEnabled}/> + <input + type='text' + className='form-control' + ref={'last_name' + index} + placeholder='Last name' + maxLength='64' + disabled={!this.state.emailEnabled} + /> {lastNameError} </div> </div> @@ -223,7 +253,15 @@ module.exports = React.createClass({ <div key={'key' + index}> {removeButton} <div className={emailClass}> - <input onKeyUp={this.displayNameKeyUp} type='text' ref={'email' + index} className='form-control' placeholder='email@domain.com' maxLength='64' disabled={!this.state.emailEnabled}/> + <input + onKeyUp={this.displayNameKeyUp} + type='text' + ref={'email' + index} + className='form-control' + placeholder='email@domain.com' + maxLength='64' + disabled={!this.state.emailEnabled} + /> {emailError} </div> {nameFields} @@ -242,23 +280,44 @@ module.exports = React.createClass({ content = ( <div> {serverError} - <button type='button' className='btn btn-default' onClick={this.addInviteFields}>Add another</button> + <button + type='button' + className='btn btn-default' + onClick={this.addInviteFields} + >Add another</button> <br/> <br/> <span>People invited automatically join Town Square channel.</span> </div> ); - sendButton = <button onClick={this.handleSubmit} type='button' className='btn btn-primary'>Send Invitations</button> + sendButton = + ( + <button + onClick={this.handleSubmit} + type='button' + className='btn btn-primary' + >Send Invitations</button> + ); } else { var teamInviteLink = null; if (currentUser && this.props.teamType === 'O') { var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id; - var link = <a href='#' data-toggle='modal' data-target='#get_link' data-title='Team Invite' data-value={linkUrl} onClick={ - function() { - $('#invite_member').modal('hide'); - } - }>Team Invite Link</a>; + var link = + ( + <a + href='#' + data-toggle='modal' + data-target='#get_link' + data-title='Team Invite' + data-value={linkUrl} + onClick={ + function click() { + $('#invite_member').modal('hide'); + } + } + >Team Invite Link</a> + ); teamInviteLink = ( <p> @@ -277,22 +336,46 @@ module.exports = React.createClass({ return ( <div> - <div className='modal fade' ref='modal' id='invite_member' tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade' + ref='modal' + id='invite_member' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close' data-reactid='.5.0.0.0.0'><span aria-hidden='true' data-reactid='.5.0.0.0.0.0'>×</span></button> - <h4 className='modal-title' id='myModalLabel'>Invite New Member</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + >Invite New Member</h4> </div> - <div ref='modalBody' className='modal-body'> + <div + ref='modalBody' + className='modal-body' + > <form role='form'> {inviteSections} </form> {content} </div> <div className='modal-footer'> - <button type='button' className='btn btn-default' data-dismiss='modal'>Cancel</button> - {sendButton} + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + >Cancel</button> + {sendButton} </div> </div> </div> @@ -309,4 +392,8 @@ module.exports = React.createClass({ } return <div/>; } -}); +} + +InviteMemberModal.propTypes = { + teamType: React.PropTypes.string +}; diff --git a/web/react/components/loading_screen.jsx b/web/react/components/loading_screen.jsx index 5905e519b..b0f42ce86 100644 --- a/web/react/components/loading_screen.jsx +++ b/web/react/components/loading_screen.jsx @@ -1,24 +1,31 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - displayName: "LoadingScreen", - propTypes: { - position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) - }, - getDefaultProps: function() { - return { position: 'relative' }; - }, - render: function() { +export default class LoadingScreen extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { return ( - <div className="loading-screen" style={{position: this.props.position}}> - <div className="loading__content"> + <div + className='loading-screen' + style={{position: this.props.position}} + > + <div className='loading__content'> <h3>Loading</h3> - <div className="round round-1"></div> - <div className="round round-2"></div> - <div className="round round-3"></div> + <div className='round round-1'></div> + <div className='round round-2'></div> + <div className='round round-3'></div> </div> </div> ); } -}); +} + +LoadingScreen.defaultProps = { + position: 'relative' +}; +LoadingScreen.propTypes = { + position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 28dd64c39..b20c62833 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx'); -var Constants = require('../utils/constants.jsx'); +const Utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const BrowserStore = require('../stores/browser_store.jsx'); +const Constants = require('../utils/constants.jsx'); +import {config, strings} from '../utils/config.js'; export default class Login extends React.Component { constructor(props) { @@ -17,23 +18,23 @@ export default class Login extends React.Component { } handleSubmit(e) { e.preventDefault(); - var state = {}; + let state = {}; - var name = this.props.teamName; + const name = this.props.teamName; if (!name) { state.serverError = 'Bad team name'; this.setState(state); return; } - var email = this.refs.email.getDOMNode().value.trim(); + const email = React.findDOMNode(this.refs.email).value.trim(); if (!email) { state.serverError = 'An email is required'; this.setState(state); return; } - var password = this.refs.password.getDOMNode().value.trim(); + const password = React.findDOMNode(this.refs.password).value.trim(); if (!password) { state.serverError = 'A password is required'; this.setState(state); @@ -49,12 +50,12 @@ export default class Login extends React.Component { state.serverError = ''; this.setState(state); - client.loginByEmail(name, email, password, + Client.loginByEmail(name, email, password, function loggedIn(data) { UserStore.setCurrentUser(data); UserStore.setLastEmail(email); - var redirect = utils.getUrlParameter('redirect'); + const redirect = Utils.getUrlParameter('redirect'); if (redirect) { window.location.href = decodeURIComponent(redirect); } else { @@ -73,31 +74,31 @@ export default class Login extends React.Component { ); } render() { - var serverError; + let serverError; if (this.state.serverError) { serverError = <label className='control-label'>{this.state.serverError}</label>; } - var priorEmail = UserStore.getLastEmail(); + let priorEmail = UserStore.getLastEmail(); - var emailParam = utils.getUrlParameter('email'); + const emailParam = Utils.getUrlParameter('email'); if (emailParam) { priorEmail = decodeURIComponent(emailParam); } - var teamDisplayName = this.props.teamDisplayName; - var teamName = this.props.teamName; + const teamDisplayName = this.props.teamDisplayName; + const teamName = this.props.teamName; - var focusEmail = false; - var focusPassword = false; + let focusEmail = false; + let focusPassword = false; if (priorEmail !== '') { focusPassword = true; } else { focusEmail = true; } - var authServices = JSON.parse(this.props.authServices); + const authServices = JSON.parse(this.props.authServices); - var loginMessage = []; + let loginMessage = []; if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) { loginMessage.push( <a @@ -110,12 +111,12 @@ export default class Login extends React.Component { ); } - var errorClass = ''; + let errorClass = ''; if (serverError) { errorClass = ' has-error'; } - var emailSignup; + let emailSignup; if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) { emailSignup = ( <div> @@ -163,7 +164,7 @@ export default class Login extends React.Component { ); } - var forgotPassword; + let forgotPassword; if (emailSignup) { forgotPassword = ( <div className='form-group'> diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx index 69da5cfc3..65495b569 100644 --- a/web/react/components/member_list.jsx +++ b/web/react/components/member_list.jsx @@ -3,32 +3,47 @@ var MemberListItem = require('./member_list_item.jsx'); -module.exports = React.createClass({ - render: function() { +export default class MemberList extends React.Component { + constructor(props) { + super(props); + } + + render() { var members = []; - if (this.props.memberList != null) { + if (this.props.memberList !== null) { members = this.props.memberList; } - var message = ""; - if (members.length === 0) + var message = ''; + if (members.length === 0) { message = <span>No users to add.</span>; + } return ( - <div className="member-list-holder"> - {members.map(function(member) { - return <MemberListItem - key={member.id} - member={member} - isAdmin={this.props.isAdmin} - handleInvite={this.props.handleInvite} - handleRemove={this.props.handleRemove} - handleMakeAdmin={this.props.handleMakeAdmin} - />; + <div className='member-list-holder'> + {members.map(function mymembers(member) { + return ( + <MemberListItem + key={member.id} + member={member} + isAdmin={this.props.isAdmin} + handleInvite={this.props.handleInvite} + handleRemove={this.props.handleRemove} + handleMakeAdmin={this.props.handleMakeAdmin} + /> + ); }, this)} {message} </div> ); } -}); +} + +MemberList.propTypes = { + memberList: React.PropTypes.array, + isAdmin: React.PropTypes.bool, + handleInvite: React.PropTypes.func, + handleRemove: React.PropTypes.func, + handleMakeAdmin: React.PropTypes.func +}; diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index a5279909b..9a31a2e30 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -1,64 +1,125 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -module.exports = React.createClass({ - displayName: 'MemberListItem', - handleInvite: function(e) { +export default class MemberListItem extends React.Component { + constructor(props) { + super(props); + + this.handleInvite = this.handleInvite.bind(this); + this.handleRemove = this.handleRemove.bind(this); + this.handleMakeAdmin = this.handleMakeAdmin.bind(this); + } + handleInvite(e) { e.preventDefault(); this.props.handleInvite(this.props.member.id); - }, - handleRemove: function(e) { + } + handleRemove(e) { e.preventDefault(); this.props.handleRemove(this.props.member.id); - }, - handleMakeAdmin: function(e) { + } + handleMakeAdmin(e) { e.preventDefault(); this.props.handleMakeAdmin(this.props.member.id); - }, - render: function() { - + } + render() { var member = this.props.member; var isAdmin = this.props.isAdmin; - var isMemberAdmin = member.roles.indexOf("admin") > -1; + var isMemberAdmin = member.roles.indexOf('admin') > -1; var timestamp = UserStore.getCurrentUser().update_at; var invite; if (member.invited && this.props.handleInvite) { - invite = <span className="member-role">Added</span>; + invite = <span className='member-role'>Added</span>; } else if (this.props.handleInvite) { - invite = <a onClick={this.handleInvite} className="btn btn-sm btn-primary member-invite"><i className="glyphicon glyphicon-envelope"/> Add</a>; - } else if (isAdmin && !isMemberAdmin && (member.id != UserStore.getCurrentId())) { + invite = ( + <a + onClick={this.handleInvite} + className='btn btn-sm btn-primary member-invite' + > + <i className='glyphicon glyphicon-envelope'/> + {' Add'} + </a> + ); + } else if (isAdmin && !isMemberAdmin && (member.id !== UserStore.getCurrentId())) { var self = this; + + let makeAdminOption = null; + if (this.props.handleMakeAdmin) { + makeAdminOption = ( + <li role='presentation'> + <a + href='' + role='menuitem' + onClick={self.handleMakeAdmin} + > + Make Admin + </a> + </li>); + } + + let handleRemoveOption = null; + if (this.props.handleRemove) { + handleRemoveOption = ( + <li role='presentation'> + <a + href='' + role='menuitem' + onClick={self.handleRemove} + > + Remove Member + </a> + </li>); + } + invite = ( - <div className="dropdown member-drop"> - <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> - <span className="text-capitalize">{member.roles || 'Member'} </span> - <span className="caret"></span> + <div className='dropdown member-drop'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <span className='text-capitalize'>{member.roles || 'Member'} </span> + <span className='caret'></span> </a> - <ul className="dropdown-menu member-menu" role="menu" aria-labelledby="channel_header_dropdown"> - { this.props.handleMakeAdmin ? - <li role="presentation"><a href="" role="menuitem" onClick={self.handleMakeAdmin}>Make Admin</a></li> - : null } - { this.props.handleRemove ? - <li role="presentation"><a href="" role="menuitem" onClick={self.handleRemove}>Remove Member</a></li> - : null } + <ul + className='dropdown-menu member-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {makeAdminOption} + {handleRemoveOption} </ul> </div> ); } else { - invite = <div className="member-role text-capitalize">{member.roles || 'Member'}<span className="caret hidden"></span></div>; + invite = <div className='member-role text-capitalize'>{member.roles || 'Member'}<span className='caret hidden'></span></div>; } return ( - <div className="row member-div"> - <img className="post-profile-img pull-left" src={"/api/v1/users/" + member.id + "/image?time=" + timestamp} height="36" width="36" /> - <span className="member-name">{member.username}</span> - <span className="member-email">{member.email}</span> - { invite } + <div className='row member-div'> + <img + className='post-profile-img pull-left' + src={'/api/v1/users/' + member.id + '/image?time=' + timestamp} + height='36' + width='36' + /> + <span className='member-name'>{member.username}</span> + <span className='member-email'>{member.email}</span> + {invite} </div> ); } -}); +} + +MemberListItem.propTypes = { + handleInvite: React.PropTypes.func, + handleRemove: React.PropTypes.func, + handleMakeAdmin: React.PropTypes.func, + member: React.PropTypes.object, + isAdmin: React.PropTypes.bool +}; diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx index cb48e5cc5..064330c8d 100644 --- a/web/react/components/member_list_team.jsx +++ b/web/react/components/member_list_team.jsx @@ -1,122 +1,27 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var ChannelStore = require('../stores/channel_store.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var utils = require('../utils/utils.jsx'); - -var MemberListTeamItem = React.createClass({ - handleMakeMember: function() { - var data = {}; - data["user_id"] = this.props.user.id; - data["new_roles"] = ""; - - Client.updateRoles(data, - function(data) { - AsyncClient.getProfiles(); - }.bind(this), - function(err) { - this.setState({ server_error: err.message }); - }.bind(this) - ); - }, - handleMakeActive: function() { - Client.updateActive(this.props.user.id, true, - function(data) { - AsyncClient.getProfiles(); - }.bind(this), - function(err) { - this.setState({ server_error: err.message }); - }.bind(this) - ); - }, - handleMakeNotActive: function() { - Client.updateActive(this.props.user.id, false, - function(data) { - AsyncClient.getProfiles(); - }.bind(this), - function(err) { - this.setState({ server_error: err.message }); - }.bind(this) - ); - }, - handleMakeAdmin: function() { - var data = {}; - data["user_id"] = this.props.user.id; - data["new_roles"] = "admin"; - - Client.updateRoles(data, - function(data) { - AsyncClient.getProfiles(); - }.bind(this), - function(err) { - this.setState({ server_error: err.message }); - }.bind(this) - ); - }, - getInitialState: function() { - return {}; - }, - render: function() { - var server_error = this.state.server_error ? <div className="has-error"><label className='has-error control-label'>{this.state.server_error}</label></div> : null; - var user = this.props.user; - var currentRoles = "Member"; - var timestamp = UserStore.getCurrentUser().update_at; - - if (user.roles.length > 0) { - currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); - } - - var email = user.email.length > 0 ? user.email : ""; - var showMakeMember = user.roles == "admin"; - var showMakeAdmin = user.roles == ""; - var showMakeActive = false; - var showMakeNotActive = true; - - if (user.delete_at > 0) { - currentRoles = "Inactive"; - showMakeMember = false; - showMakeAdmin = false; - showMakeActive = true; - showMakeNotActive = false; - } +const MemberListTeamItem = require('./member_list_team_item.jsx'); + +export default class MemberListTeam extends React.Component { + render() { + const memberList = this.props.users.map(function makeListItem(user) { + return ( + <MemberListTeamItem + key={user.id} + user={user} + /> + ); + }, this); return ( - <div className="row member-div"> - <img className="post-profile-img pull-left" src={"/api/v1/users/" + user.id + "/image?time=" + timestamp} height="36" width="36" /> - <span className="member-name">{utils.getDisplayName(user)}</span> - <span className="member-email">{email}</span> - <div className="dropdown member-drop"> - <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> - <span>{currentRoles} </span> - <span className="caret"></span> - </a> - <ul className="dropdown-menu member-menu" role="menu" aria-labelledby="channel_header_dropdown"> - { showMakeAdmin ? <li role="presentation"><a role="menuitem" href="#" onClick={this.handleMakeAdmin}>Make Admin</a></li> : "" } - { showMakeMember ? <li role="presentation"><a role="menuitem" href="#" onClick={this.handleMakeMember}>Make Member</a></li> : "" } - { showMakeActive ? <li role="presentation"><a role="menuitem" href="#" onClick={this.handleMakeActive}>Make Active</a></li> : "" } - { showMakeNotActive ? <li role="presentation"><a role="menuitem" href="#" onClick={this.handleMakeNotActive}>Make Inactive</a></li> : "" } - </ul> - </div> - { server_error } + <div className='member-list-holder'> + {memberList} </div> ); } -}); +} - -module.exports = React.createClass({ - render: function() { - return ( - <div className="member-list-holder"> - { - this.props.users.map(function(user) { - return <MemberListTeamItem key={user.id} user={user} />; - }, this) - } - </div> - ); - } -}); +MemberListTeam.propTypes = { + users: React.PropTypes.arrayOf(React.PropTypes.object).isRequired +}; diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx new file mode 100644 index 000000000..b7e81f843 --- /dev/null +++ b/web/react/components/member_list_team_item.jsx @@ -0,0 +1,203 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const UserStore = require('../stores/user_store.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const Utils = require('../utils/utils.jsx'); + +export default class MemberListTeamItem extends React.Component { + constructor(props) { + super(props); + + this.handleMakeMember = this.handleMakeMember.bind(this); + this.handleMakeActive = this.handleMakeActive.bind(this); + this.handleMakeNotActive = this.handleMakeNotActive.bind(this); + this.handleMakeAdmin = this.handleMakeAdmin.bind(this); + + this.state = {}; + } + handleMakeMember() { + const data = { + user_id: this.props.user.id, + new_roles: '' + }; + + Client.updateRoles(data, + function handleMakeMemberSuccess() { + AsyncClient.getProfiles(); + }, + function handleMakeMemberError(err) { + this.setState({serverError: err.message}); + }.bind(this) + ); + } + handleMakeActive() { + Client.updateActive(this.props.user.id, true, + function handleMakeActiveSuccess() { + AsyncClient.getProfiles(); + }, + function handleMakeActiveError(err) { + this.setState({serverError: err.message}); + }.bind(this) + ); + } + handleMakeNotActive() { + Client.updateActive(this.props.user.id, false, + function handleMakeNotActiveSuccess() { + AsyncClient.getProfiles(); + }, + function handleMakeNotActiveError(err) { + this.setState({serverError: err.message}); + }.bind(this) + ); + } + handleMakeAdmin() { + const data = { + user_id: this.props.user.id, + new_roles: 'admin' + }; + + Client.updateRoles(data, + function handleMakeAdminSuccess() { + AsyncClient.getProfiles(); + }, + function handleMakeAdmitError(err) { + this.setState({serverError: err.message}); + }.bind(this) + ); + } + render() { + let serverError = null; + if (this.state.serverError) { + serverError = ( + <div className='has-error'> + <label className='has-error control-label'>{this.state.serverError}</label> + </div> + ); + } + + const user = this.props.user; + let currentRoles = 'Member'; + const timestamp = UserStore.getCurrentUser().update_at; + + if (user.roles.length > 0) { + currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1); + } + + const email = user.email; + let showMakeMember = user.roles === 'admin'; + let showMakeAdmin = user.roles === ''; + let showMakeActive = false; + let showMakeNotActive = true; + + if (user.delete_at > 0) { + currentRoles = 'Inactive'; + showMakeMember = false; + showMakeAdmin = false; + showMakeActive = true; + showMakeNotActive = false; + } + + let makeAdmin = null; + if (showMakeAdmin) { + makeAdmin = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeAdmin} + > + Make Admin + </a> + </li> + ); + } + + let makeMember = null; + if (showMakeMember) { + makeMember = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeMember} + > + Make Member + </a> + </li> + ); + } + + let makeActive = null; + if (showMakeActive) { + makeActive = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeActive} + > + Make Active + </a> + </li> + ); + } + + let makeNotActive = null; + if (showMakeNotActive) { + makeNotActive = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleMakeNotActive} + > + Make Inactive + </a> + </li> + ); + } + + return ( + <div className='row member-div'> + <img + className='post-profile-img pull-left' + src={`/api/v1/users/${user.id}/image?time=${timestamp}`} + height='36' + width='36' + /> + <span className='member-name'>{Utils.getDisplayName(user)}</span> + <span className='member-email'>{email}</span> + <div className='dropdown member-drop'> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <span>{currentRoles} </span> + <span className='caret'></span> + </a> + <ul + className='dropdown-menu member-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {makeAdmin} + {makeMember} + {makeActive} + {makeNotActive} + </ul> + </div> + {serverError} + </div> + ); + } +} + +MemberListTeamItem.propTypes = { + user: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx index 114dc183f..3a09e843d 100644 --- a/web/react/components/mention.jsx +++ b/web/react/components/mention.jsx @@ -1,30 +1,60 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require("../stores/user_store.jsx"); +var UserStore = require('../stores/user_store.jsx'); -module.exports = React.createClass({ - handleClick: function() { +export default class Mention extends React.Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + + this.state = null; + } + handleClick() { this.props.handleClick(this.props.username); - }, - getInitialState: function() { - return null; - }, - render: function() { - var self = this; + } + render() { var icon; var timestamp = UserStore.getCurrentUser().update_at; - if (this.props.id === "allmention" || this.props.id === "channelmention") { - icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>; + if (this.props.id === 'allmention' || this.props.id === 'channelmention') { + icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>; } else if (this.props.id != null) { - icon = <span><img className="mention-img" src={"/api/v1/users/" + this.props.id + "/image?time=" + timestamp}/></span>; + icon = ( + <span> + <img + className='mention-img' + src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp} + /> + </span> + ); } else { - icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>; + icon = <span><i className='mention-img fa fa-users fa-2x'></i></span>; } return ( - <div className={"mentions-name " + this.props.isFocused} id={this.props.id + "_mentions"} onClick={this.handleClick} onMouseEnter={this.props.handleMouseEnter}> - <div className="pull-left">{icon}</div> - <div className="pull-left mention-align"><span>@{this.props.username}</span><span className="mention-fullname">{this.props.secondary_text}</span></div> + <div + className={'mentions-name ' + this.props.isFocused} + id={this.props.id + '_mentions'} + onClick={this.handleClick} + onMouseEnter={this.props.handleMouseEnter} + > + <div className='pull-left'>{icon}</div> + <div className='pull-left mention-align'><span>@{this.props.username}</span><span className='mention-fullname'>{this.props.secondary_text}</span></div> </div> ); } -}); +} + +Mention.defaultProps = { + username: '', + id: '', + isFocused: '', + secondary_text: '' +}; +Mention.propTypes = { + handleClick: React.PropTypes.func.isRequired, + handleMouseEnter: React.PropTypes.func.isRequired, + username: React.PropTypes.string, + id: React.PropTypes.string, + isFocused: React.PropTypes.string, + secondary_text: React.PropTypes.string +}; diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx index f562cfb29..46a9d76ae 100644 --- a/web/react/components/mention_list.jsx +++ b/web/react/components/mention_list.jsx @@ -14,54 +14,66 @@ var MAX_HEIGHT_LIST = 292; var MAX_ITEMS_IN_LIST = 25; var ITEM_HEIGHT = 36; -module.exports = React.createClass({ - displayName: 'MentionList', - componentDidMount: function() { +export default class MentionList extends React.Component { + constructor(props) { + super(props); + + this.onListenerChange = this.onListenerChange.bind(this); + this.handleClick = this.handleClick.bind(this); + this.handleMouseEnter = this.handleMouseEnter.bind(this); + this.getSelection = this.getSelection.bind(this); + this.addCurrentMention = this.addCurrentMention.bind(this); + this.addFirstMention = this.addFirstMention.bind(this); + this.isEmpty = this.isEmpty.bind(this); + this.scrollToMention = this.scrollToMention.bind(this); + + this.state = {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''}; + } + componentDidMount() { PostStore.addMentionDataChangeListener(this.onListenerChange); - var self = this; - $('.post-right__scroll').scroll(function(){ - if($('.mentions--top').length){ - $('#reply_mention_tab .mentions--top').css({ bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top }); + $('.post-right__scroll').scroll(function onScroll() { + if ($('.mentions--top').length) { + $('#reply_mention_tab .mentions--top').css({bottom: $(window).height() - $('.post-right__scroll #reply_textbox').offset().top}); } }); $('body').on('keydown.mentionlist', '#' + this.props.id, - function(e) { - if (!self.isEmpty() && self.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) { + function onMentionListKey(e) { + if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 13 || e.which === 9)) { e.stopPropagation(); e.preventDefault(); - self.addCurrentMention(); - } else if (!self.isEmpty() && self.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) { + this.addCurrentMention(); + } else if (!this.isEmpty() && this.state.mentionText !== '-1' && (e.which === 38 || e.which === 40)) { e.stopPropagation(); e.preventDefault(); if (e.which === 38) { - if (self.getSelection(self.state.selectedMention - 1)) { - self.setState({selectedMention: self.state.selectedMention - 1, selectedUsername: self.refs['mention' + (self.state.selectedMention - 1)].props.username}); + if (this.getSelection(this.state.selectedMention - 1)) { + this.setState({selectedMention: this.state.selectedMention - 1, selectedUsername: this.refs['mention' + (this.state.selectedMention - 1)].props.username}); } } else if (e.which === 40) { - if (self.getSelection(self.state.selectedMention + 1)) { - self.setState({selectedMention: self.state.selectedMention + 1, selectedUsername: self.refs['mention' + (self.state.selectedMention + 1)].props.username}); + if (this.getSelection(this.state.selectedMention + 1)) { + this.setState({selectedMention: this.state.selectedMention + 1, selectedUsername: this.refs['mention' + (this.state.selectedMention + 1)].props.username}); } } - self.scrollToMention(e.which); + this.scrollToMention(e.which); } - } + }.bind(this) ); - $(document).click(function(e) { - if (!($('#' + self.props.id).is(e.target) || $('#' + self.props.id).has(e.target).length || - ('mentionlist' in self.refs && $(self.refs.mentionlist.getDOMNode()).has(e.target).length))) { - self.setState({mentionText: '-1'}); + $(document).click(function onClick(e) { + if (!($('#' + this.props.id).is(e.target) || $('#' + this.props.id).has(e.target).length || + ('mentionlist' in this.refs && $(React.findDOMNode(this.refs.mentionlist)).has(e.target).length))) { + this.setState({mentionText: '-1'}); } - }); - }, - componentWillUnmount: function() { + }.bind(this)); + } + componentWillUnmount() { PostStore.removeMentionDataChangeListener(this.onListenerChange); $('body').off('keydown.mentionlist', '#' + this.props.id); - }, - componentDidUpdate: function() { + } + componentDidUpdate() { if (this.state.mentionText !== '-1') { if (this.state.selectedUsername !== '' && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) { var tempSelectedMention = -1; @@ -80,8 +92,8 @@ module.exports = React.createClass({ } else if (this.state.selectedMention !== 0) { this.setState({selectedMention: 0, selectedUsername: ''}); } - }, - onListenerChange: function(id, mentionText) { + } + onListenerChange(id, mentionText) { if (id !== this.props.id) { return; } @@ -92,8 +104,8 @@ module.exports = React.createClass({ } this.setState(newState); - }, - handleClick: function(name) { + } + handleClick(name) { AppDispatcher.handleViewAction({ type: ActionTypes.RECIEVED_ADD_MENTION, id: this.props.id, @@ -101,33 +113,33 @@ module.exports = React.createClass({ }); this.setState({mentionText: '-1'}); - }, - handleMouseEnter: function(listId) { + } + handleMouseEnter(listId) { this.setState({selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username}); - }, - getSelection: function(listId) { + } + getSelection(listId) { if (!this.refs['mention' + listId]) { return false; } return true; - }, - addCurrentMention: function() { + } + addCurrentMention() { if (!this.getSelection(this.state.selectedMention)) { this.addFirstMention(); } else { this.refs['mention' + this.state.selectedMention].handleClick(); } - }, - addFirstMention: function() { + } + addFirstMention() { if (!this.refs.mention0) { return; } this.refs.mention0.handleClick(); - }, - isEmpty: function() { + } + isEmpty() { return (!this.refs.mention0); - }, - scrollToMention: function(keyPressed) { + } + scrollToMention(keyPressed) { var direction; if (keyPressed === 38) { direction = 'up'; @@ -145,12 +157,8 @@ module.exports = React.createClass({ $('#mentionsbox').animate({ scrollTop: scrollAmount }, 75); - }, - getInitialState: function() { - return {excludeUsers: [], mentionText: '-1', selectedMention: 0, selectedUsername: ''}; - }, - render: function() { - var self = this; + } + render() { var mentionText = this.state.mentionText; if (mentionText === '-1') { return null; @@ -158,8 +166,10 @@ module.exports = React.createClass({ var profiles = UserStore.getActiveOnlyProfiles(); var users = []; - for (var id in profiles) { - users.push(profiles[id]); + for (let id in profiles) { + if (profiles[id]) { + users.push(profiles[id]); + } } var all = {}; @@ -176,7 +186,7 @@ module.exports = React.createClass({ channel.id = 'channelmention'; users.push(channel); - users.sort(function(a, b) { + users.sort(function sortByUsername(a, b) { if (a.username < b.username) { return -1; } @@ -185,29 +195,35 @@ module.exports = React.createClass({ } return 0; }); - var mentions = {}; + var mentions = []; var index = 0; for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) { if ((users[i].first_name && users[i].first_name.lastIndexOf(mentionText, 0) === 0) || (users[i].last_name && users[i].last_name.lastIndexOf(mentionText, 0) === 0) || users[i].username.lastIndexOf(mentionText, 0) === 0) { + let isFocused = ''; + if (this.state.selectedMention === index) { + isFocused = 'mentions-focus'; + } mentions[index] = ( <Mention + key={'mention_key_' + index} ref={'mention' + index} username={users[i].username} secondary_text={Utils.getFullName(users[i])} id={users[i].id} listId={index} - isFocused={this.state.selectedMention === index ? 'mentions-focus' : ''} - handleMouseEnter={function(value) { return function() { self.handleMouseEnter(value); } }(index)} - handleClick={this.handleClick} /> + isFocused={isFocused} + handleMouseEnter={this.handleMouseEnter.bind(this, index)} + handleClick={this.handleClick} + /> ); index++; } } - var numMentions = Object.keys(mentions).length; + var numMentions = mentions.length; if (numMentions < 1) { return null; @@ -223,11 +239,22 @@ module.exports = React.createClass({ }; return ( - <div className='mentions--top' style={style}> - <div ref='mentionlist' className='mentions-box' id='mentionsbox'> + <div + className='mentions--top' + style={style} + > + <div + ref='mentionlist' + className='mentions-box' + id='mentionsbox' + > {mentions} </div> </div> ); } -}); +} + +MentionList.propTypes = { + id: React.PropTypes.string +}; diff --git a/web/react/components/message_wrapper.jsx b/web/react/components/message_wrapper.jsx index 5fc88a61b..bce305853 100644 --- a/web/react/components/message_wrapper.jsx +++ b/web/react/components/message_wrapper.jsx @@ -1,17 +1,30 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - render: function() { +export default class MessageWrapper extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { if (this.props.message) { - var inner = utils.textToJsx(this.props.message, this.props.options); + var inner = Utils.textToJsx(this.props.message, this.props.options); return ( <div>{inner}</div> ); - } else { - return <div/> } + + return <div/>; } -}); +} + +MessageWrapper.defaultProps = { + message: null, + options: null +}; +MessageWrapper.propTypes = { + message: React.PropTypes.string, + options: React.PropTypes.object +}; diff --git a/web/react/components/more_channels.jsx b/web/react/components/more_channels.jsx index 2b2c8b68d..ba8be12b2 100644 --- a/web/react/components/more_channels.jsx +++ b/web/react/components/more_channels.jsx @@ -29,12 +29,12 @@ export default class MoreChannels extends React.Component { } componentDidMount() { ChannelStore.addMoreChangeListener(this.onListenerChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function shown() { + $(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function shown() { asyncClient.getMoreChannels(true); }); var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function show(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) { var button = e.relatedTarget; self.setState({channelType: $(button).attr('data-channeltype')}); }); @@ -52,7 +52,7 @@ export default class MoreChannels extends React.Component { this.setState({joiningChannel: channelIndex}); client.joinChannel(channel.id, function joinSuccess() { - $(this.refs.modal.getDOMNode()).modal('hide'); + $(React.findDOMNode(this.refs.modal)).modal('hide'); asyncClient.getChannel(channel.id); utils.switchChannel(channel); this.setState({joiningChannel: -1}); @@ -65,7 +65,7 @@ export default class MoreChannels extends React.Component { ); } handleNewChannel() { - $(this.refs.modal.getDOMNode()).modal('hide'); + $(React.findDOMNode(this.refs.modal)).modal('hide'); } render() { var serverError; @@ -86,15 +86,21 @@ export default class MoreChannels extends React.Component { {channels.map(function cMap(channel, index) { var joinButton; if (self.state.joiningChannel === index) { - joinButton = (<img - className='join-channel-loading-gif' - src='/static/images/load.gif' - />); + joinButton = ( + <img + className='join-channel-loading-gif' + src='/static/images/load.gif' + /> + ); } else { - joinButton = (<button - onClick={self.handleJoin.bind(self, channel, index)} - className='btn btn-primary'>Join - </button>); + joinButton = ( + <button + onClick={self.handleJoin.bind(self, channel, index)} + className='btn btn-primary' + > + Join + </button> + ); } return ( @@ -152,7 +158,9 @@ export default class MoreChannels extends React.Component { data-channeltype={this.state.channelType} type='button' className='btn btn-primary channel-create-btn' - onClick={this.handleNewChannel}>Create New Channel + onClick={this.handleNewChannel} + > + Create New Channel </button> </div> <div className='modal-body'> diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index 11ddbcbd1..f27b09ecc 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -7,19 +7,22 @@ var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - displayName: 'MoreDirectChannels', - componentDidMount: function() { +export default class MoreDirectChannels extends React.Component { + constructor(props) { + super(props); + + this.state = {channels: [], loadingDMChannel: -1}; + } + + componentDidMount() { var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function showModal(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) { var button = e.relatedTarget; self.setState({channels: $(button).data('channels')}); }); - }, - getInitialState: function() { - return {channels: [], loadingDMChannel: -1}; - }, - render: function() { + } + + render() { var self = this; var directMessageItems = this.state.channels.map(function mapActivityToChannel(channel, index) { @@ -41,14 +44,19 @@ module.exports = React.createClass({ handleClick = function clickHandler(e) { e.preventDefault(); utils.switchChannel(channel, channel.teammate_username); - $(self.refs.modal.getDOMNode()).modal('hide'); + $(React.findDOMNode(self.refs.modal)).modal('hide'); }; } else { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = utils.getUserIdFromChannelName(channel); if (self.state.loadingDMChannel === index) { - badge = <img className='channel-loading-gif pull-right' src='/static/images/load.gif'/>; + badge = ( + <img + className='channel-loading-gif pull-right' + src='/static/images/load.gif' + /> + ); } if (self.state.loadingDMChannel === -1) { @@ -58,7 +66,7 @@ module.exports = React.createClass({ Client.createDirectChannel(channel, otherUserId, function success(data) { - $(self.refs.modal.getDOMNode()).modal('hide'); + $(React.findDOMNode(self.refs.modal)).modal('hide'); self.setState({loadingDMChannel: -1}); AsyncClient.getChannel(data.id); utils.switchChannel(data); @@ -73,16 +81,36 @@ module.exports = React.createClass({ } return ( - <li key={channel.name} className={active}><a className={'sidebar-channel ' + titleClass} href='#' onClick={handleClick}>{badge}{channel.display_name}</a></li> + <li + key={channel.name} + className={active} + > + <a + className={'sidebar-channel ' + titleClass} + href='#' + onClick={handleClick} + >{badge}{channel.display_name}</a> + </li> ); }); return ( - <div className='modal fade' id='more_direct_channels' ref='modal' tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade' + id='more_direct_channels' + ref='modal' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal'> + <button + type='button' + className='close' + data-dismiss='modal' + > <span aria-hidden='true'>×</span> <span className='sr-only'>Close</span> </button> @@ -94,11 +122,15 @@ module.exports = React.createClass({ </ul> </div> <div className='modal-footer'> - <button type='button' className='btn btn-default' data-dismiss='modal'>Close</button> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + >Close</button> </div> </div> </div> </div> ); } -}); +} diff --git a/web/react/components/msg_typing.jsx b/web/react/components/msg_typing.jsx index aacb315dd..d9823c3cf 100644 --- a/web/react/components/msg_typing.jsx +++ b/web/react/components/msg_typing.jsx @@ -1,57 +1,70 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var SocketStore = require('../stores/socket_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -module.exports = React.createClass({ - timer: null, - lastTime: 0, - componentDidMount: function() { - SocketStore.addChangeListener(this._onChange); - }, - componentWillReceiveProps: function(newProps) { - if(this.props.channelId !== newProps.channelId) { - this.setState({text:""}); +export default class MsgTyping extends React.Component { + constructor(props) { + super(props); + + this.timer = null; + this.lastTime = 0; + + this.onChange = this.onChange.bind(this); + + this.state = { + text: '' + }; + } + + componentDidMount() { + SocketStore.addChangeListener(this.onChange); + } + + componentWillReceiveProps(newProps) { + if (this.props.channelId !== newProps.channelId) { + this.setState({text: ''}); } - }, - componentWillUnmount: function() { - SocketStore.removeChangeListener(this._onChange); - }, - _onChange: function(msg) { - if (msg.action == "typing" && - this.props.channelId == msg.channel_id && - this.props.parentId == msg.props.parent_id) { + } + + componentWillUnmount() { + SocketStore.removeChangeListener(this.onChange); + } + onChange(msg) { + if (msg.action === 'typing' && + this.props.channelId === msg.channel_id && + this.props.parentId === msg.props.parent_id) { this.lastTime = new Date().getTime(); - var username = "Someone"; + var username = 'Someone'; if (UserStore.hasProfile(msg.user_id)) { username = UserStore.getProfile(msg.user_id).username; } - this.setState({ text: username + " is typing..." }); + this.setState({text: username + ' is typing...'}); if (!this.timer) { - var outer = this; - outer.timer = setInterval(function() { - if ((new Date().getTime() - outer.lastTime) > 8000) { - outer.setState({ text: "" }); - } - }, 3000); + this.timer = setInterval(function myTimer() { + if ((new Date().getTime() - this.lastTime) > 8000) { + this.setState({text: ''}); + } + }.bind(this), 3000); } + } else if (msg.action === 'posted' && msg.channel_id === this.props.channelId) { + this.setState({text: ''}); } - else if (msg.action == "posted" && msg.channel_id === this.props.channelId) { - this.setState({text:""}) - } - }, - getInitialState: function() { - return { text: "" }; - }, - render: function() { + } + + render() { return ( - <span className="msg-typing">{ this.state.text }</span> + <span className='msg-typing'>{this.state.text}</span> ); } -}); +} + +MsgTyping.propTypes = { + channelId: React.PropTypes.string, + parentId: React.PropTypes.string +}; diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 06c373e5d..2258bf2b3 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -1,7 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); @@ -13,22 +13,27 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -function getStateFromStores() { - return { - channel: ChannelStore.getCurrent(), - member: ChannelStore.getCurrentMember(), - users: ChannelStore.getCurrentExtraInfo().members - }; -} +export default class Navbar extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + this.handleLeave = this.handleLeave.bind(this); + this.createCollapseButtons = this.createCollapseButtons.bind(this); + this.createDropdown = this.createDropdown.bind(this); -module.exports = React.createClass({ - displayName: 'Navbar', - propTypes: { - teamDisplayName: React.PropTypes.string - }, - componentDidMount: function() { - ChannelStore.addChangeListener(this.onListenerChange); - ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + this.state = this.getStateFromStores(); + } + getStateFromStores() { + return { + channel: ChannelStore.getCurrent(), + member: ChannelStore.getCurrentMember(), + users: ChannelStore.getCurrentExtraInfo().members + }; + } + componentDidMount() { + ChannelStore.addChangeListener(this.onChange); + ChannelStore.addExtraInfoChangeListener(this.onChange); $('.inner__wrap').click(this.hideSidebars); $('body').on('click.infopopover', function handlePopoverClick(e) { @@ -36,15 +41,15 @@ module.exports = React.createClass({ $('.info-popover').popover('hide'); } }); - }, - componentWillUnmount: function() { - ChannelStore.removeChangeListener(this.onListenerChange); - }, - handleSubmit: function(e) { + } + componentWillUnmount() { + ChannelStore.removeChangeListener(this.onChange); + } + handleSubmit(e) { e.preventDefault(); - }, - handleLeave: function() { - client.leaveChannel(this.state.channel.id, + } + handleLeave() { + Client.leaveChannel(this.state.channel.id, function success() { AsyncClient.getChannels(true); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; @@ -53,8 +58,8 @@ module.exports = React.createClass({ AsyncClient.dispatchError(err, 'handleLeave'); } ); - }, - hideSidebars: function(e) { + } + hideSidebars(e) { var windowWidth = $(window).outerWidth(); if (windowWidth <= 768) { AppDispatcher.handleServerAction({ @@ -74,32 +79,262 @@ module.exports = React.createClass({ $('.sidebar--menu').removeClass('move--left'); } } - }, - toggleLeftSidebar: function() { + } + toggleLeftSidebar() { $('.inner__wrap').toggleClass('move--right'); $('.sidebar--left').toggleClass('move--right'); - }, - toggleRightSidebar: function() { + } + toggleRightSidebar() { $('.inner__wrap').toggleClass('move--left-small'); $('.sidebar--menu').toggleClass('move--left'); - }, - onListenerChange: function() { - this.setState(getStateFromStores()); + } + onChange() { + this.setState(this.getStateFromStores()); $('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true}); - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { + } + createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) { + if (channel) { + var viewInfoOption = ( + <li role='presentation'> + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_info' + data-channelid={channel.id} + href='#' + > + View Info + </a> + </li> + ); + + var setChannelDescriptionOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#edit_channel' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + > + Set Channel Description... + </a> + </li> + ); + + var addMembersOption; + var leaveChannelOption; + if (!isDirect && !ChannelStore.isDefault(channel)) { + addMembersOption = ( + <li role='presentation'> + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_invite' + href='#' + > + Add Members + </a> + </li> + ); + + leaveChannelOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleLeave} + > + Leave Channel + </a> + </li> + ); + } + + var manageMembersOption; + var renameChannelOption; + var deleteChannelOption; + if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { + manageMembersOption = ( + <li role='presentation'> + <a + role='menuitem' + data-toggle='modal' + data-target='#channel_members' + href='#' + > + Manage Members + </a> + </li> + ); + + renameChannelOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#rename_channel' + data-display={channel.display_name} + data-name={channel.name} + data-channelid={channel.id} + > + Rename Channel... + </a> + </li> + ); + + deleteChannelOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#delete_channel' + data-title={channel.display_name} + data-channelid={channel.id} + > + Delete Channel... + </a> + </li> + ); + } + + var notificationPreferenceOption; + if (!isDirect) { + notificationPreferenceOption = ( + <li role='presentation'> + <a + role='menuitem' + href='#' + data-toggle='modal' + data-target='#channel_notifications' + data-title={channel.display_name} + data-channelid={channel.id} + > + Notification Preferences + </a> + </li> + ); + } + + return ( + <div className='navbar-brand'> + <div className='dropdown'> + <div + data-toggle='popover' + data-content={popoverContent} + className='description info-popover' + /> + <a + href='#' + className='dropdown-toggle theme' + type='button' + id='channel_header_dropdown' + data-toggle='dropdown' + aria-expanded='true' + > + <span className='heading'>{channelTitle} </span> + <span className='glyphicon glyphicon-chevron-down header-dropdown__icon'></span> + </a> + <ul + className='dropdown-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {viewInfoOption} + {addMembersOption} + {manageMembersOption} + {setChannelDescriptionOption} + {notificationPreferenceOption} + {renameChannelOption} + {deleteChannelOption} + {leaveChannelOption} + </ul> + </div> + </div> + ); + } + + return ( + <div className='navbar-brand'> + <a + href='/' + className='heading' + > + {channelTitle} + </a> + </div> + ); + } + createCollapseButtons(currentId) { + var buttons = []; + if (currentId == null) { + buttons.push( + <button + key='navbar-toggle-collapse' + type='button' + className='navbar-toggle' + data-toggle='collapse' + data-target='#navbar-collapse-1' + > + <span className='sr-only'>Toggle sidebar</span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + </button> + ); + } else { + buttons.push( + <button + key='navbar-toggle-sidebar' + type='button' + className='navbar-toggle' + data-toggle='collapse' + data-target='#sidebar-nav' + onClick={this.toggleLeftSidebar} + > + <span className='sr-only'>Toggle sidebar</span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <span className='icon-bar'></span> + <NotifyCounts /> + </button> + ); + + buttons.push( + <button + key='navbar-toggle-menu' + type='button' + className='navbar-toggle menu-toggle pull-right' + data-toggle='collapse' + data-target='#sidebar-nav' + onClick={this.toggleRightSidebar} + > + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> + </button> + ); + } + + return buttons; + } + render() { var currentId = UserStore.getCurrentId(); - var popoverContent = ''; + var channel = this.state.channel; var channelTitle = this.props.teamDisplayName; + var popoverContent; var isAdmin = false; var isDirect = false; - var channel = this.state.channel; if (channel) { - popoverContent = React.renderToString(<MessageWrapper message={channel.description} options={{singleline: true, noMentionHighlight: true}}/>); + popoverContent = React.renderToString( + <MessageWrapper + message={channel.description} + options={{singleline: true, noMentionHighlight: true}} + /> + ); isAdmin = this.state.member.roles.indexOf('admin') > -1; if (channel.type === 'O') { @@ -118,110 +353,46 @@ module.exports = React.createClass({ } if (channel.description.length === 0) { - popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); + popoverContent = React.renderToString( + <div> + No channel description yet. <br/> + <a + href='#' + data-toggle='modal' + data-desc={channel.description} + data-title={channel.display_name} + data-channelid={channel.id} + data-target='#edit_channel' + > + Click here + </a> to add one.</div> + ); } } - var navbarCollapseButton = null; - if (currentId == null) { - navbarCollapseButton = (<button type='button' className='navbar-toggle' data-toggle='collapse' data-target='#navbar-collapse-1'> - <span className='sr-only'>Toggle sidebar</span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - </button>); - } - - var sidebarCollapseButton = null; - if (currentId != null) { - sidebarCollapseButton = (<button type='button' className='navbar-toggle' data-toggle='collapse' data-target='#sidebar-nav' onClick={this.toggleLeftSidebar}> - <span className='sr-only'>Toggle sidebar</span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - <span className='icon-bar'></span> - <NotifyCounts /> - </button>); - } - - var rightSidebarCollapseButton = null; - if (currentId != null) { - rightSidebarCollapseButton = (<button type='button' className='navbar-toggle menu-toggle pull-right' data-toggle='collapse' data-target='#sidebar-nav' onClick={this.toggleRightSidebar}> - <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> - </button>); - } - - var channelMenuDropdown = null; - if (channel) { - var viewInfoOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_info' data-channelid={channel.id} href='#'>View Info</a></li> - - var addMembersOption = null; - if (!isDirect && !ChannelStore.isDefault(channel)) { - addMembersOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_invite' href='#'>Add Members</a></li>; - } - - var manageMembersOption = null; - if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { - manageMembersOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_members' href='#'>Manage Members</a></li>; - } - - var setChannelDescriptionOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li>; - - var notificationPreferenceOption = null; - if (!isDirect) { - notificationPreferenceOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#channel_notifications' data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li>; - } - - var renameChannelOption = null; - if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { - renameChannelOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#rename_channel' data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename Channel...</a></li>; - } - - var deleteChannelOption = null; - if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) { - deleteChannelOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#delete_channel' data-title={channel.display_name} data-channelid={channel.id}>Delete Channel...</a></li>; - } + var collapseButtons = this.createCollapseButtons(currentId); - var leaveChannelOption = null; - if (!isDirect && !ChannelStore.isDefault(channel)) { - leaveChannelOption = <li role='presentation'><a role='menuitem' href='#' onClick={this.handleLeave}>Leave Channel</a></li>; - } - - channelMenuDropdown = (<div className='navbar-brand'> - <div className='dropdown'> - <div data-toggle='popover' data-content={popoverContent} className='description info-popover'></div> - <a href='#' className='dropdown-toggle theme' type='button' id='channel_header_dropdown' data-toggle='dropdown' aria-expanded='true'> - <span className='heading'>{channelTitle} </span> - <span className='glyphicon glyphicon-chevron-down header-dropdown__icon'></span> - </a> - <ul className='dropdown-menu' role='menu' aria-labelledby='channel_header_dropdown'> - {viewInfoOption} - {addMembersOption} - {manageMembersOption} - {setChannelDescriptionOption} - {notificationPreferenceOption} - {renameChannelOption} - {deleteChannelOption} - {leaveChannelOption} - </ul> - </div> - </div>); - } else { - channelMenuDropdown = (<div className='navbar-brand'> - <a href='/' className='heading'>{channelTitle}</a> - </div>); - } + var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent); return ( - <nav className='navbar navbar-default navbar-fixed-top' role='navigation'> + <nav + className='navbar navbar-default navbar-fixed-top' + role='navigation' + > <div className='container-fluid theme'> <div className='navbar-header'> - {navbarCollapseButton} - {sidebarCollapseButton} - {rightSidebarCollapseButton} + {collapseButtons} {channelMenuDropdown} </div> </div> </nav> ); } -}); +} + +Navbar.defaultProps = { + teamDisplayName: '' +}; +Navbar.propTypes = { + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx new file mode 100644 index 000000000..99cdfa1ad --- /dev/null +++ b/web/react/components/navbar_dropdown.jsx @@ -0,0 +1,216 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); + +var Constants = require('../utils/constants.jsx'); +import {config} from '../utils/config.js'; + +function getStateFromStores() { + return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()}; +} + +export default class NavbarDropdown extends React.Component { + constructor(props) { + super(props); + this.blockToggle = false; + + this.handleLogoutClick = this.handleLogoutClick.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + + this.state = getStateFromStores(); + } + handleLogoutClick(e) { + e.preventDefault(); + client.logout(); + } + componentDidMount() { + UserStore.addTeamsChangeListener(this.onListenerChange); + TeamStore.addChangeListener(this.onListenerChange); + + $(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', function resetDropdown() { + this.blockToggle = true; + setTimeout(function blockTimeout() { + this.blockToggle = false; + }.bind(this), 100); + }.bind(this)); + } + componentWillUnmount() { + UserStore.removeTeamsChangeListener(this.onListenerChange); + TeamStore.removeChangeListener(this.onListenerChange); + + $(React.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); + } + onListenerChange() { + var newState = getStateFromStores(); + if (!Utils.areStatesEqual(newState, this.state)) { + this.setState(newState); + } + } + render() { + var teamLink = ''; + var inviteLink = ''; + var manageLink = ''; + var currentUser = UserStore.getCurrentUser(); + var isAdmin = false; + var teamSettings = null; + + if (currentUser != null) { + isAdmin = currentUser.roles.indexOf('admin') > -1; + + inviteLink = ( + <li> + <a + href='#' + data-toggle='modal' + data-target='#invite_member' + > + Invite New Member + </a> + </li> + ); + + if (this.props.teamType === 'O') { + teamLink = ( + <li> + <a + href='#' + data-toggle='modal' + data-target='#get_link' + data-title='Team Invite' + data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id} + > + Get Team Invite Link + </a> + </li> + ); + } + } + + if (isAdmin) { + manageLink = ( + <li> + <a + href='#' + data-toggle='modal' + data-target='#team_members' + > + Manage Team + </a> + </li> + ); + teamSettings = (<li> + <a + href='#' + data-toggle='modal' + data-target='#team_settings' + > + Team Settings + </a> + </li>); + } + + var teams = []; + + teams.push( + <li + className='divider' + key='div' + > + </li> + ); + if (this.state.teams.length > 1 && this.state.currentTeam) { + var curTeamName = this.state.currentTeam.name; + this.state.teams.forEach(function listTeams(teamName) { + if (teamName !== curTeamName) { + teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>Switch to {teamName}</a></li>); + } + }); + } + teams.push(<li key='newTeam_li'> + <a + key='newTeam_a' + target='_blank' + href={Utils.getWindowLocationOrigin() + '/signup_team'} + > + Create a New Team + </a> + </li>); + + return ( + <ul className='nav navbar-nav navbar-right'> + <li + ref='dropdown' + className='dropdown' + > + <a + href='#' + className='dropdown-toggle' + data-toggle='dropdown' + role='button' + aria-expanded='false' + > + <span + className='dropdown__icon' + dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} + /> + </a> + <ul + className='dropdown-menu' + role='menu' + > + <li> + <a + href='#' + data-toggle='modal' + data-target='#user_settings' + > + Account Settings + </a> + </li> + {teamSettings} + {inviteLink} + {teamLink} + {manageLink} + <li> + <a + href='#' + onClick={this.handleLogoutClick} + > + Logout + </a> + </li> + {teams} + <li className='divider'></li> + <li> + <a + target='_blank' + href={config.HelpLink} + > + Help + </a> + </li> + <li> + <a + target='_blank' + href={config.ReportProblemLink} + > + Report a Problem + </a> + </li> + </ul> + </li> + </ul> + ); + } +} + +NavbarDropdown.defaultProps = { + teamType: '' +}; +NavbarDropdown.propTypes = { + teamType: React.PropTypes.string +}; diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx index fc24a7cdc..1a11fc652 100644 --- a/web/react/components/new_channel.jsx +++ b/web/react/components/new_channel.jsx @@ -5,17 +5,24 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var asyncClient = require('../utils/async_client.jsx'); var UserStore = require('../stores/user_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); -module.exports = React.createClass({ - displayName: 'NewChannelModal', - handleSubmit: function(e) { +export default class NewChannelModal extends React.Component { + constructor() { + super(); + + this.handleSubmit = this.handleSubmit.bind(this); + this.displayNameKeyUp = this.displayNameKeyUp.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = {channelType: ''}; + } + handleSubmit(e) { e.preventDefault(); var channel = {}; var state = {serverError: ''}; - channel.display_name = this.refs.display_name.getDOMNode().value.trim(); + channel.display_name = React.findDOMNode(this.refs.display_name).value.trim(); if (!channel.display_name) { state.displayNameError = 'This field is required'; state.inValid = true; @@ -26,7 +33,7 @@ module.exports = React.createClass({ state.displayNameError = ''; } - channel.name = this.refs.channel_name.getDOMNode().value.trim(); + channel.name = React.findDOMNode(this.refs.channel_name).value.trim(); if (!channel.name) { state.nameError = 'This field is required'; state.inValid = true; @@ -52,54 +59,51 @@ module.exports = React.createClass({ var cu = UserStore.getCurrentUser(); channel.team_id = cu.team_id; - channel.description = this.refs.channel_desc.getDOMNode().value.trim(); + channel.description = React.findDOMNode(this.refs.channel_desc).value.trim(); channel.type = this.state.channelType; client.createChannel(channel, - function(data) { - $(this.refs.modal.getDOMNode()).modal('hide'); + function success(data) { + $(React.findDOMNode(this.refs.modal)).modal('hide'); asyncClient.getChannel(data.id); utils.switchChannel(data); - this.refs.display_name.getDOMNode().value = ''; - this.refs.channel_name.getDOMNode().value = ''; - this.refs.channel_desc.getDOMNode().value = ''; + React.findDOMNode(this.refs.display_name).value = ''; + React.findDOMNode(this.refs.channel_name).value = ''; + React.findDOMNode(this.refs.channel_desc).value = ''; }.bind(this), - function(err) { + function error(err) { state.serverError = err.message; state.inValid = true; this.setState(state); }.bind(this) ); - }, - displayNameKeyUp: function() { - var displayName = this.refs.display_name.getDOMNode().value.trim(); + } + displayNameKeyUp() { + var displayName = React.findDOMNode(this.refs.display_name).value.trim(); var channelName = utils.cleanUpUrlable(displayName); - this.refs.channel_name.getDOMNode().value = channelName; - }, - componentDidMount: function() { + React.findDOMNode(this.refs.channel_name).value = channelName; + } + componentDidMount() { var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onModalShow(e) { var button = e.relatedTarget; self.setState({channelType: $(button).attr('data-channeltype')}); }); - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { - $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose); - }, - handleClose: function() { - $(this.getDOMNode()).find('.form-control').each(function clearForms() { + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function clearForms() { this.value = ''; }); this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false}); - }, - getInitialState: function() { - return {channelType: ''}; - }, - render: function() { + } + render() { var displayNameError = null; var nameError = null; var serverError = null; @@ -124,11 +128,22 @@ module.exports = React.createClass({ } return ( - <div className='modal fade' id='new_channel' ref='modal' tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade' + id='new_channel' + ref='modal' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal'> + <button + type='button' + className='close' + data-dismiss='modal' + > <span aria-hidden='true'>×</span> <span className='sr-only'>Cancel</span> </button> @@ -138,23 +153,54 @@ module.exports = React.createClass({ <div className='modal-body'> <div className={displayNameClass}> <label className='control-label'>Display Name</label> - <input onKeyUp={this.displayNameKeyUp} type='text' ref='display_name' className='form-control' placeholder='Enter display name' maxLength='22' /> + <input + onKeyUp={this.displayNameKeyUp} + type='text' + ref='display_name' + className='form-control' + placeholder='Enter display name' + maxLength='22' + /> {displayNameError} </div> <div className={nameClass}> <label className='control-label'>Handle</label> - <input type='text' className='form-control' ref='channel_name' placeholder="lowercase alphanumeric's only" maxLength='22' /> + <input + type='text' + className='form-control' + ref='channel_name' + placeholder="lowercase alphanumeric's only" + maxLength='22' + /> {nameError} </div> <div className='form-group'> <label className='control-label'>Description</label> - <textarea className='form-control no-resize' ref='channel_desc' rows='3' placeholder='Description' maxLength='1024'></textarea> + <textarea + className='form-control no-resize' + ref='channel_desc' + rows='3' + placeholder='Description' + maxLength='1024' + /> </div> {serverError} </div> <div className='modal-footer'> - <button type='button' className='btn btn-default' data-dismiss='modal'>Cancel</button> - <button onClick={this.handleSubmit} type='submit' className='btn btn-primary'>Create New {channelTerm}</button> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + onClick={this.handleSubmit} + type='submit' + className='btn btn-primary' + > + Create New {channelTerm} + </button> </div> </form> </div> @@ -162,4 +208,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/notify_counts.jsx b/web/react/components/notify_counts.jsx index ebc49882b..0b7c41b62 100644 --- a/web/react/components/notify_counts.jsx +++ b/web/react/components/notify_counts.jsx @@ -23,27 +23,30 @@ function getCountsStateFromStores() { return {count: count}; } -module.exports = React.createClass({ - displayName: 'NotifyCounts', - componentDidMount: function() { +export default class NotifyCounts extends React.Component { + constructor(props) { + super(props); + + this.onListenerChange = this.onListenerChange.bind(this); + + this.state = getCountsStateFromStores(); + } + componentDidMount() { ChannelStore.addChangeListener(this.onListenerChange); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { ChannelStore.removeChangeListener(this.onListenerChange); - }, - onListenerChange: function() { + } + onListenerChange() { var newState = getCountsStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - }, - getInitialState: function() { - return getCountsStateFromStores(); - }, - render: function() { + } + render() { if (this.state.count) { return <span className='badge badge-notify'>{this.state.count}</span>; } return null; } -}); +} diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx index b2edea620..399d3b7b9 100644 --- a/web/react/components/password_reset.jsx +++ b/web/react/components/password_reset.jsx @@ -1,143 +1,47 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var UserStore = require('../stores/user_store.jsx'); +var PasswordResetSendLink = require('./password_reset_send_link.jsx'); +var PasswordResetForm = require('./password_reset_form.jsx'); -SendResetPasswordLink = React.createClass({ - handleSendLink: function(e) { - e.preventDefault(); - var state = {}; +export default class PasswordReset extends React.Component { + constructor(props) { + super(props); - var email = this.refs.email.getDOMNode().value.trim(); - if (!email) { - state.error = "Please enter a valid email address." - this.setState(state); - return; - } - - state.error = null; - this.setState(state); - - data = {}; - data['email'] = email; - data['name'] = this.props.teamName; - - client.sendPasswordReset(data, - function(data) { - this.setState({ error: null, update_text: <p>A password reset link has been sent to <b>{email}</b> for your <b>{this.props.teamDisplayName}</b> team on {window.location.hostname}.</p>, more_update_text: "Please check your inbox." }); - $(this.refs.reset_form.getDOMNode()).hide(); - }.bind(this), - function(err) { - this.setState({ error: err.message, update_text: null, more_update_text: null }); - }.bind(this) - ); - }, - getInitialState: function() { - return {}; - }, - render: function() { - var update_text = this.state.update_text ? <div className="reset-form alert alert-success">{this.state.update_text}{this.state.more_update_text}</div> : null; - var error = this.state.error ? <div className="form-group has-error"><label className="control-label">{this.state.error}</label></div> : null; - - return ( - <div className="col-sm-12"> - <div className="signup-team__container"> - <h3>Password Reset</h3> - { update_text } - <form onSubmit={this.handleSendLink} ref="reset_form"> - <p>{"To reset your password, enter the email address you used to sign up for " + this.props.teamDisplayName + "."}</p> - <div className={error ? 'form-group has-error' : 'form-group'}> - <input type="text" className="form-control" name="email" ref="email" placeholder="Email" /> - </div> - { error } - <button type="submit" className="btn btn-primary">Reset my password</button> - </form> - </div> - </div> - ); + this.state = {}; } -}); - -ResetPassword = React.createClass({ - handlePasswordReset: function(e) { - e.preventDefault(); - var state = {}; - - var password = this.refs.password.getDOMNode().value.trim(); - if (!password || password.length < 5) { - state.error = "Please enter at least 5 characters." - this.setState(state); - return; - } - - state.error = null; - this.setState(state); - - data = {}; - data['new_password'] = password; - data['hash'] = this.props.hash; - data['data'] = this.props.data; - data['name'] = this.props.teamName; - - client.resetPassword(data, - function(data) { - this.setState({ error: null, update_text: "Your password has been updated successfully." }); - }.bind(this), - function(err) { - this.setState({ error: err.message, update_text: null }); - }.bind(this) - ); - }, - getInitialState: function() { - return {}; - }, - render: function() { - var update_text = this.state.update_text ? <div className="form-group"><br/><label className="control-label reset-form">{this.state.update_text} Click <a href={"/" + this.props.teamName + "/login"}>here</a> to log in.</label></div> : null; - var error = this.state.error ? <div className="form-group has-error"><label className="control-label">{this.state.error}</label></div> : null; - - return ( - <div className="col-sm-12"> - <div className="signup-team__container"> - <h3>Password Reset</h3> - <form onSubmit={this.handlePasswordReset}> - <p>{"Enter a new password for your " + this.props.teamDisplayName + " " + config.SiteName + " account."}</p> - <div className={error ? 'form-group has-error' : 'form-group'}> - <input type="password" className="form-control" name="password" ref="password" placeholder="Password" /> - </div> - { error } - <button type="submit" className="btn btn-primary">Change my password</button> - { update_text } - </form> - </div> - </div> - ); - } -}); - -module.exports = React.createClass({ - getInitialState: function() { - return {}; - }, - render: function() { - - if (this.props.isReset === "false") { - return ( - <SendResetPasswordLink - teamDisplayName={this.props.teamDisplayName} - teamName={this.props.teamName} - /> - ); - } else { + render() { + if (this.props.isReset === 'false') { return ( - <ResetPassword + <PasswordResetSendLink teamDisplayName={this.props.teamDisplayName} teamName={this.props.teamName} - hash={this.props.hash} - data={this.props.data} /> ); } + + return ( + <PasswordResetForm + teamDisplayName={this.props.teamDisplayName} + teamName={this.props.teamName} + hash={this.props.hash} + data={this.props.data} + /> + ); } -}); +} + +PasswordReset.defaultProps = { + isReset: '', + teamName: '', + teamDisplayName: '', + hash: '', + data: '' +}; +PasswordReset.propTypes = { + isReset: React.PropTypes.string, + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string, + hash: React.PropTypes.string, + data: React.PropTypes.string +}; diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx new file mode 100644 index 000000000..1b579efbc --- /dev/null +++ b/web/react/components/password_reset_form.jsx @@ -0,0 +1,101 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var client = require('../utils/client.jsx'); +import {config} from '../utils/config.js'; + +export default class PasswordResetForm extends React.Component { + constructor(props) { + super(props); + + this.handlePasswordReset = this.handlePasswordReset.bind(this); + + this.state = {}; + } + handlePasswordReset(e) { + e.preventDefault(); + var state = {}; + + var password = React.findDOMNode(this.refs.password).value.trim(); + if (!password || password.length < 5) { + state.error = 'Please enter at least 5 characters.'; + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var data = {}; + data.new_password = password; + data.hash = this.props.hash; + data.data = this.props.data; + data.name = this.props.teamName; + + client.resetPassword(data, + function resetSuccess() { + this.setState({error: null, updateText: 'Your password has been updated successfully.'}); + }.bind(this), + function resetFailure(err) { + this.setState({error: err.message, updateText: null}); + }.bind(this) + ); + } + render() { + var updateText = null; + if (this.state.updateText) { + updateText = <div className='form-group'><br/><label className='control-label reset-form'>{this.state.updateText} Click <a href={'/' + this.props.teamName + '/login'}>here</a> to log in.</label></div>; + } + + var error = null; + if (this.state.error) { + error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + return ( + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3>Password Reset</h3> + <form onSubmit={this.handlePasswordReset}> + <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + config.SiteName + ' account.'}</p> + <div className={formClass}> + <input + type='password' + className='form-control' + name='password' + ref='password' + placeholder='Password' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + Change my password + </button> + {updateText} + </form> + </div> + </div> + ); + } +} + +PasswordResetForm.defaultProps = { + teamName: '', + teamDisplayName: '', + hash: '', + data: '' +}; +PasswordResetForm.propTypes = { + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string, + hash: React.PropTypes.string, + data: React.PropTypes.string +}; diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx new file mode 100644 index 000000000..1e6cc3607 --- /dev/null +++ b/web/react/components/password_reset_send_link.jsx @@ -0,0 +1,98 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var client = require('../utils/client.jsx'); + +export default class PasswordResetSendLink extends React.Component { + constructor(props) { + super(props); + + this.handleSendLink = this.handleSendLink.bind(this); + + this.state = {}; + } + handleSendLink(e) { + e.preventDefault(); + var state = {}; + + var email = React.findDOMNode(this.refs.email).value.trim(); + if (!email) { + state.error = 'Please enter a valid email address.'; + this.setState(state); + return; + } + + state.error = null; + this.setState(state); + + var data = {}; + data.email = email; + data.name = this.props.teamName; + + client.sendPasswordReset(data, + function passwordResetSent() { + this.setState({error: null, updateText: <p>A password reset link has been sent to <b>{email}</b> for your <b>{this.props.teamDisplayName}</b> team on {window.location.hostname}.</p>, moreUpdateText: 'Please check your inbox.'}); + $(React.findDOMNode(this.refs.reset_form)).hide(); + }.bind(this), + function passwordResetFailedToSend(err) { + this.setState({error: err.message, update_text: null, moreUpdateText: null}); + }.bind(this) + ); + } + render() { + var updateText = null; + if (this.state.updateText) { + updateText = <div className='reset-form alert alert-success'>{this.state.updateText}{this.state.moreUpdateText}</div>; + } + + var error = null; + if (this.state.error) { + error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>; + } + + var formClass = 'form-group'; + if (error) { + formClass += ' has-error'; + } + + return ( + <div className='col-sm-12'> + <div className='signup-team__container'> + <h3>Password Reset</h3> + {updateText} + <form + onSubmit={this.handleSendLink} + ref='reset_form' + > + <p>{'To reset your password, enter the email address you used to sign up for ' + this.props.teamDisplayName + '.'}</p> + <div className={formClass}> + <input + type='text' + className='form-control' + name='email' + ref='email' + placeholder='Email' + /> + </div> + {error} + <button + type='submit' + className='btn btn-primary' + > + Reset my password + </button> + </form> + </div> + </div> + ); + } +} + +PasswordResetSendLink.defaultProps = { + teamName: '', + teamDisplayName: '' +}; +PasswordResetSendLink.propTypes = { + teamName: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx new file mode 100644 index 000000000..fb9522afb --- /dev/null +++ b/web/react/components/popover_list_members.jsx @@ -0,0 +1,80 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +export default class PopoverListMembers extends React.Component { + componentDidMount() { + const originalLeave = $.fn.popover.Constructor.prototype.leave; + $.fn.popover.Constructor.prototype.leave = function onLeave(obj) { + let selfObj; + if (obj instanceof this.constructor) { + selfObj = obj; + } else { + selfObj = $(obj.currentTarget)[this.type](this.getDelegateOptions()).data(`bs.${this.type}`); + } + originalLeave.call(this, obj); + + if (obj.currentTarget && selfObj.$tip) { + selfObj.$tip.one('mouseenter', function onMouseEnter() { + clearTimeout(selfObj.timeout); + selfObj.$tip.one('mouseleave', function onMouseLeave() { + $.fn.popover.Constructor.prototype.leave.call(selfObj, selfObj); + }); + }); + } + }; + + $('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true}); + $('body').on('click', function onClick(e) { + if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) { + $('#member_popover').popover('hide'); + } + }); + } + render() { + let popoverHtml = ''; + const members = this.props.members; + let count; + if (members.length > 20) { + count = '20+'; + } else { + count = members.length || '-'; + } + + if (members) { + members.sort(function compareByLocal(a, b) { + return a.username.localeCompare(b.username); + }); + + members.forEach(function addMemberElement(m) { + popoverHtml += `<div class='text--nowrap'>${m.username}</div>`; + }); + } + + return ( + <div + id='member_popover' + data-toggle='popover' + data-content={popoverHtml} + data-original-title='Members' + > + <div + id='member_tooltip' + data-placement='left' + data-toggle='tooltip' + title='View Channel Members' + > + {count} + <span + className='glyphicon glyphicon-user' + aria-hidden='true' + /> + </div> + </div> + ); + } +} + +PopoverListMembers.propTypes = { + members: React.PropTypes.array.isRequired, + channelId: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index acc2b51d2..37de4ecc0 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -15,9 +15,17 @@ var utils = require('../utils/utils.jsx'); var PostInfo = require('./post_info.jsx'); -module.exports = React.createClass({ - displayName: 'Post', - handleCommentClick: function(e) { +export default class Post extends React.Component { + constructor(props) { + super(props); + + this.handleCommentClick = this.handleCommentClick.bind(this); + this.forceUpdateInfo = this.forceUpdateInfo.bind(this); + this.retryPost = this.retryPost.bind(this); + + this.state = {}; + } + handleCommentClick(e) { e.preventDefault(); var data = {}; @@ -33,31 +41,31 @@ module.exports = React.createClass({ type: ActionTypes.RECIEVED_SEARCH, results: null }); - }, - forceUpdateInfo: function() { + } + forceUpdateInfo() { this.refs.info.forceUpdate(); this.refs.header.forceUpdate(); - }, - retryPost: function(e) { + } + retryPost(e) { e.preventDefault(); var post = this.props.post; client.createPost(post, post.channel_id, - function(data) { + function success(data) { AsyncClient.getPosts(); var channel = ChannelStore.get(post.channel_id); var member = ChannelStore.getMember(post.channel_id); member.msg_count = channel.total_msg_count; - member.last_viewed_at = (new Date).getTime(); + member.last_viewed_at = utils.getTimestamp(); ChannelStore.setChannelMember(member); AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_POST, post: data }); - }.bind(this), - function(err) { + }, + function error() { post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); this.forceUpdate(); @@ -67,18 +75,15 @@ module.exports = React.createClass({ post.state = Constants.POST_LOADING; PostStore.updatePendingPost(post); this.forceUpdate(); - }, - shouldComponentUpdate: function(nextProps) { + } + shouldComponentUpdate(nextProps) { if (!utils.areStatesEqual(nextProps.post, this.props.post)) { return true; } return false; - }, - getInitialState: function() { - return { }; - }, - render: function() { + } + render() { var post = this.props.post; var parentPost = this.props.parentPost; var posts = this.props.posts; @@ -89,19 +94,27 @@ module.exports = React.createClass({ } var commentCount = 0; - var commentRootId = parentPost ? post.root_id : post.id; + var commentRootId; + if (parentPost) { + commentRootId = post.root_id; + } else { + commentRootId = post.id; + } for (var postId in posts) { - if (posts[postId].root_id == commentRootId) { + if (posts[postId].root_id === commentRootId) { commentCount += 1; } } - var error = this.state.error ? <div className='form-group has-error'><label className='control-label'>{ this.state.error }</label></div> : null; - - var rootUser = this.props.sameRoot ? 'same--root' : 'other--root'; + var rootUser; + if (this.props.sameRoot) { + rootUser = 'same--root'; + } else { + rootUser = 'other--root'; + } var postType = ''; - if (type != 'Post'){ + if (type !== 'Post') { postType = 'post--comment'; } @@ -122,21 +135,65 @@ module.exports = React.createClass({ sameUserClass = 'same--user'; } + var profilePic = null; + if (!this.props.hideProfilePic) { + profilePic = ( + <div className='post-profile-img__container'> + <img + className='post-profile-img' + src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} + height='36' + width='36' + /> + </div> + ); + } + return ( <div> - <div id={post.id} className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss}> - { !this.props.hideProfilePic ? - <div className='post-profile-img__container'> - <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> - </div> - : null } + <div + id={post.id} + className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss} + > + {profilePic} <div className='post__content'> - <PostHeader ref='header' post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} isLastComment={this.props.isLastComment} /> - <PostBody post={post} sameRoot={this.props.sameRoot} parentPost={parentPost} posts={posts} handleCommentClick={this.handleCommentClick} retryPost={this.retryPost} /> - <PostInfo ref='info' post={post} sameRoot={this.props.sameRoot} commentCount={commentCount} handleCommentClick={this.handleCommentClick} allowReply='true' /> + <PostHeader + ref='header' + post={post} + sameRoot={this.props.sameRoot} + commentCount={commentCount} + handleCommentClick={this.handleCommentClick} + isLastComment={this.props.isLastComment} + /> + <PostBody + post={post} + sameRoot={this.props.sameRoot} + parentPost={parentPost} + posts={posts} + handleCommentClick={this.handleCommentClick} + retryPost={this.retryPost} + /> + <PostInfo + ref='info' + post={post} + sameRoot={this.props.sameRoot} + commentCount={commentCount} + handleCommentClick={this.handleCommentClick} + allowReply='true' + /> </div> </div> </div> ); } -}); +} + +Post.propTypes = { + post: React.PropTypes.object, + posts: React.PropTypes.object, + parentPost: React.PropTypes.object, + sameUser: React.PropTypes.bool, + sameRoot: React.PropTypes.bool, + hideProfilePic: React.PropTypes.bool, + isLastComment: React.PropTypes.bool +}; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index e5ab5b624..d9b8f20ce 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -1,95 +1,152 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var FileAttachmentList = require('./file_attachment_list.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); - -module.exports = React.createClass({ - componentWillReceiveProps: function(nextProps) { - var linkData = utils.extractLinks(nextProps.post.message); +const FileAttachmentList = require('./file_attachment_list.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); +const Constants = require('../utils/constants.jsx'); +const twemoji = require('twemoji'); + +export default class PostBody extends React.Component { + constructor(props) { + super(props); + + this.parseEmojis = this.parseEmojis.bind(this); + + const linkData = Utils.extractLinks(this.props.post.message); + this.state = {links: linkData.links, message: linkData.text}; + } + parseEmojis() { + twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); + } + componentDidMount() { + this.parseEmojis(); + } + componentDidUpdate() { + this.parseEmojis(); + } + componentWillReceiveProps(nextProps) { + const linkData = Utils.extractLinks(nextProps.post.message); this.setState({links: linkData.links, message: linkData.text}); - }, - getInitialState: function() { - var linkData = utils.extractLinks(this.props.post.message); - return {links: linkData.links, message: linkData.text}; - }, - render: function() { - var post = this.props.post; - var filenames = this.props.post.filenames; - var parentPost = this.props.parentPost; - var inner = utils.textToJsx(this.state.message); - - var comment = ''; - var reply = ''; - var postClass = ''; + } + render() { + const post = this.props.post; + const filenames = this.props.post.filenames; + const parentPost = this.props.parentPost; + const inner = Utils.textToJsx(this.state.message); + + let comment = ''; + let postClass = ''; if (parentPost) { - var profile = UserStore.getProfile(parentPost.user_id); - var apostrophe = ''; - var name = '...'; + const profile = UserStore.getProfile(parentPost.user_id); + + let apostrophe = ''; + let name = '...'; if (profile != null) { if (profile.username.slice(-1) === 's') { apostrophe = '\''; } else { apostrophe = '\'s'; } - name = <a className='theme' onClick={function searchName() { utils.searchForTerm(profile.username); }}>{profile.username}</a>; + name = ( + <a + className='theme' + onClick={Utils.searchForTerm.bind(null, profile.username)} + > + {profile.username} + </a> + ); } - var message = ''; + let message = ''; if (parentPost.message) { - message = utils.replaceHtmlEntities(parentPost.message); + message = Utils.replaceHtmlEntities(parentPost.message); } else if (parentPost.filenames.length) { message = parentPost.filenames[0].split('/').pop(); if (parentPost.filenames.length === 2) { message += ' plus 1 other file'; } else if (parentPost.filenames.length > 2) { - message += ' plus ' + (parentPost.filenames.length - 1) + ' other files'; + message += ` plus ${parentPost.filenames.length - 1} other files`; } } comment = ( <p className='post-link'> - <span>Commented on {name}{apostrophe} message: <a className='theme' onClick={this.props.handleCommentClick}>{message}</a></span> + <span> + Commented on {name}{apostrophe} message: + <a + className='theme' + onClick={this.props.handleCommentClick} + > + {message} + </a> + </span> </p> ); postClass += ' post-comment'; } - var loading; + let loading; if (post.state === Constants.POST_FAILED) { postClass += ' post-fail'; - loading = <a className='theme post-retry pull-right' href='#' onClick={this.props.retryPost}>Retry</a>; + loading = ( + <a + className='theme post-retry pull-right' + href='#' + onClick={this.props.retryPost} + > + Retry + </a> + ); } else if (post.state === Constants.POST_LOADING) { postClass += ' post-waiting'; - loading = <img className='post-loading-gif pull-right' src='/static/images/load.gif'/>; + loading = ( + <img + className='post-loading-gif pull-right' + src='/static/images/load.gif' + /> + ); } - var embed; + let embed; if (filenames.length === 0 && this.state.links) { - embed = utils.getEmbed(this.state.links[0]); + embed = Utils.getEmbed(this.state.links[0]); } - var fileAttachmentHolder = ''; + let fileAttachmentHolder = ''; if (filenames && filenames.length > 0) { - fileAttachmentHolder = (<FileAttachmentList - filenames={filenames} - modalId={'view_image_modal_' + post.id} - channelId={post.channel_id} - userId={post.user_id} />); + fileAttachmentHolder = ( + <FileAttachmentList + filenames={filenames} + modalId={`view_image_modal_${post.id}`} + channelId={post.channel_id} + userId={post.user_id} + /> + ); } return ( <div className='post-body'> {comment} - <p key={post.id + '_message'} className={postClass}>{loading}<span>{inner}</span></p> + <p + key={`${post.id}_message`} + className={postClass} + > + {loading}<span>{inner}</span> + </p> {fileAttachmentHolder} {embed} </div> ); } -}); +} + +PostBody.propTypes = { + post: React.PropTypes.object.isRequired, + parentPost: React.PropTypes.object, + retryPost: React.PropTypes.func.isRequired, + handleCommentClick: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/post_deleted_modal.jsx b/web/react/components/post_deleted_modal.jsx index 83b007bad..d284a9d1b 100644 --- a/web/react/components/post_deleted_modal.jsx +++ b/web/react/components/post_deleted_modal.jsx @@ -3,34 +3,61 @@ var UserStore = require('../stores/user_store.jsx'); -module.exports = React.createClass({ - getInitialState: function() { - return { }; - }, - render: function() { - var currentUser = UserStore.getCurrentUser() +export default class PostDeletedModal extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + render() { + var currentUser = UserStore.getCurrentUser(); if (currentUser != null) { return ( - <div className="modal fade" ref="modal" id="post_deleted" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" id="myModalLabel">Comment could not be posted</h4> - </div> - <div className="modal-body"> - <p>Someone deleted the message on which you tried to post a comment.</p> + <div + className='modal fade' + ref='modal' + id='post_deleted' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + > + Comment could not be posted + </h4> + </div> + <div className='modal-body'> + <p>Someone deleted the message on which you tried to post a comment.</p> + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-primary' + data-dismiss='modal' + > + Okay + </button> + </div> </div> - <div className="modal-footer"> - <button type="button" className="btn btn-primary" data-dismiss="modal">Okay</button> - </div> - </div> - </div> + </div> </div> ); - } else { - return <div/>; } + + return <div/>; } -}); +} diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 129db6d14..9dc525e03 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -1,23 +1,42 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var UserProfile = require( './user_profile.jsx' ); +var UserProfile = require('./user_profile.jsx'); var PostInfo = require('./post_info.jsx'); -module.exports = React.createClass({ - getInitialState: function() { - return { }; - }, - render: function() { +export default class PostHeader extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { var post = this.props.post; return ( - <ul className="post-header post-header-post"> - <li className="post-header-col post-header__name"><strong><UserProfile userId={post.user_id} /></strong></li> - <li className="post-info--hidden"> - <PostInfo post={post} commentCount={this.props.commentCount} handleCommentClick={this.props.handleCommentClick} allowReply="true" isLastComment={this.props.isLastComment} /> + <ul className='post-header post-header-post'> + <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li> + <li className='post-info--hidden'> + <PostInfo + post={post} + commentCount={this.props.commentCount} + handleCommentClick={this.props.handleCommentClick} + allowReply='true' + isLastComment={this.props.isLastComment} + /> </li> </ul> ); } -}); +} + +PostHeader.defaultProps = { + post: null, + commentCount: 0, + isLastComment: false +}; +PostHeader.propTypes = { + post: React.PropTypes.object, + commentCount: React.PropTypes.number, + isLastComment: React.PropTypes.bool, + handleCommentClick: React.PropTypes.func +}; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 73e897f62..c80b287a3 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -43,13 +43,16 @@ export default class PostInfo extends React.Component { if (isOwner) { dropdownContents.push( - <li role='presentation'> + <li + key='editPost' + role='presentation' + > <a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' - data-refoucsid="#post_textbox" + data-refoucsid='#post_textbox' data-title={type} data-message={post.message} data-postid={post.id} @@ -64,7 +67,10 @@ export default class PostInfo extends React.Component { if (isOwner || isAdmin) { dropdownContents.push( - <li role='presentation'> + <li + key='deletePost' + role='presentation' + > <a href='#' role='menuitem' @@ -83,7 +89,10 @@ export default class PostInfo extends React.Component { if (this.props.allowReply === 'true') { dropdownContents.push( - <li role='presentation'> + <li + key='replyLink' + role='presentation' + > <a className='reply-link theme' href='#' diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 5b0b1f79a..9d95887d9 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -15,9 +15,11 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; +import {strings} from '../utils/config.js'; + export default class PostList extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.gotMorePosts = false; this.scrolled = false; @@ -25,6 +27,7 @@ export default class PostList extends React.Component { this.seenNewMessages = false; this.isUserScroll = true; this.userHasSeenNew = false; + this.loadInProgress = false; this.onChange = this.onChange.bind(this); this.onTimeChange = this.onTimeChange.bind(this); @@ -32,27 +35,26 @@ export default class PostList extends React.Component { this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this); this.loadMorePosts = this.loadMorePosts.bind(this); this.loadFirstPosts = this.loadFirstPosts.bind(this); + this.activate = this.activate.bind(this); + this.deactivate = this.deactivate.bind(this); + this.resize = this.resize.bind(this); - this.state = this.getStateFromStores(); + this.state = this.getStateFromStores(props.channelId); this.state.numToDisplay = Constants.POST_CHUNK_SIZE; this.state.isFirstLoadComplete = false; } - getStateFromStores() { - var channel = ChannelStore.getCurrent(); - - if (channel == null) { - channel = {}; - } - - var postList = PostStore.getCurrentPosts(); + getStateFromStores(id) { + var postList = PostStore.getPosts(id); if (postList != null) { - var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id); + var deletedPosts = PostStore.getUnseenDeletedPosts(id); if (deletedPosts && Object.keys(deletedPosts).length > 0) { for (var pid in deletedPosts) { - postList.posts[pid] = deletedPosts[pid]; - postList.order.unshift(pid); + if (deletedPosts.hasOwnProperty(pid)) { + postList.posts[pid] = deletedPosts[pid]; + postList.order.unshift(pid); + } } postList.order.sort(function postSort(a, b) { @@ -66,53 +68,54 @@ export default class PostList extends React.Component { }); } - var pendingPostList = PostStore.getPendingPosts(channel.id); + var pendingPostList = PostStore.getPendingPosts(id); if (pendingPostList) { postList.order = pendingPostList.order.concat(postList.order); for (var ppid in pendingPostList.posts) { - postList.posts[ppid] = pendingPostList.posts[ppid]; + if (pendingPostList.posts.hasOwnProperty(ppid)) { + postList.posts[ppid] = pendingPostList.posts[ppid]; + } } } } - var lastViewed = Number.MAX_VALUE; - - if (ChannelStore.getCurrentMember() != null) { - lastViewed = ChannelStore.getCurrentMember().last_viewed_at; - } - return { - postList: postList, - channel: channel, - lastViewed: lastViewed + postList: postList }; } componentDidMount() { + if (this.props.isActive) { + this.activate(); + this.loadFirstPosts(this.props.channelId); + } + } + componentWillUnmount() { + this.deactivate(); + } + activate() { + this.gotMorePosts = false; + this.scrolled = false; + this.prevScrollTop = 0; + this.seenNewMessages = false; + this.isUserScroll = true; + this.userHasSeenNew = false; + + PostStore.clearUnseenDeletedPosts(this.props.channelId); PostStore.addChangeListener(this.onChange); - ChannelStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onTimeChange); SocketStore.addChangeListener(this.onSocketChange); - var postHolder = $('.post-list-holder-by-time'); - - $('.modal').on('show.bs.modal', function onShow() { - $('.modal-body').css('overflow-y', 'auto'); - $('.modal-body').css('max-height', $(window).height() * 0.7); - }); - - $(window).resize(function resize() { - if ($('#create_post').length > 0) { - var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; - postHolder.css('height', height + 'px'); - } + var postHolder = $(React.findDOMNode(this.refs.postlist)); + $(window).on('resize.' + this.props.channelId, function resize() { + this.resize(); if (!this.scrolled) { this.scrollToBottom(); } }.bind(this)); - postHolder.scroll(function scroll() { + postHolder.on('scroll', function scroll() { var position = postHolder.scrollTop() + postHolder.height() + 14; var bottom = postHolder[0].scrollHeight; @@ -128,43 +131,31 @@ export default class PostList extends React.Component { this.isUserScroll = true; }.bind(this)); - $('body').on('click.userpopover', function popOver(e) { - if ($(e.target).attr('data-toggle') !== 'popover' && - $(e.target).parents('.popover.in').length === 0) { - $('.user-popover').popover('hide'); - } - }); - $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); - $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { - if (ev.type === 'mouseenter') { - $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); - $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); - } else { - $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); - $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); - } - }); - - $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { - if (ev.type === 'mouseenter') { - $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); - $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); - } else { - $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); - $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); - } - }); + if (!this.state.isFirstLoadComplete) { + this.loadFirstPosts(this.props.channelId); + } + this.resize(); + this.onChange(); this.scrollToBottom(); - - if (this.state.channel.id != null) { - this.loadFirstPosts(this.state.channel.id); - } + } + deactivate() { + PostStore.removeChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onTimeChange); + SocketStore.removeChangeListener(this.onSocketChange); + $('body').off('click.userpopover'); + $(window).off('resize.' + this.props.channelId); + var postHolder = $(React.findDOMNode(this.refs.postlist)); + postHolder.off('scroll'); } componentDidUpdate(prevProps, prevState) { + if (!this.props.isActive) { + return; + } + $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); @@ -181,7 +172,7 @@ export default class PostList extends React.Component { var firstPost = posts[order[0]] || {}; var isNewPost = oldOrder.indexOf(order[0]) === -1; - if (this.state.channel.id !== prevState.channel.id) { + if (this.props.isActive && !prevProps.isActive) { this.scrollToBottom(); } else if (oldOrder.length === 0) { this.scrollToBottom(); @@ -195,39 +186,45 @@ export default class PostList extends React.Component { } else if (isNewPost && userId === firstPost.user_id && !utils.isComment(firstPost)) { - this.state.lastViewed = utils.getTimestamp(); this.scrollToBottom(true); // the user clicked 'load more messages' } else if (this.gotMorePosts) { var lastPost = oldPosts[oldOrder[prevState.numToDisplay]]; $('#' + lastPost.id)[0].scrollIntoView(); + this.gotMorePosts = false; } else { this.scrollTo(this.prevScrollTop); } } componentWillUpdate() { - var postHolder = $('.post-list-holder-by-time'); + var postHolder = $(React.findDOMNode(this.refs.postlist)); this.prevScrollTop = postHolder.scrollTop(); } - componentWillUnmount() { - PostStore.removeChangeListener(this.onChange); - ChannelStore.removeChangeListener(this.onChange); - UserStore.removeStatusesChangeListener(this.onTimeChange); - SocketStore.removeChangeListener(this.onSocketChange); - $('body').off('click.userpopover'); - $('.modal').off('show.bs.modal'); + componentWillReceiveProps(nextProps) { + if (nextProps.isActive === true && this.props.isActive === false) { + this.activate(); + } else if (nextProps.isActive === false && this.props.isActive === true) { + this.deactivate(); + } + } + resize() { + const postHolder = $(React.findDOMNode(this.refs.postlist)); + if ($('#create_post').length > 0) { + const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; + postHolder.css('height', height + 'px'); + } } scrollTo(val) { this.isUserScroll = false; - var postHolder = $('.post-list-holder-by-time'); + var postHolder = $(React.findDOMNode(this.refs.postlist)); postHolder[0].scrollTop = val; } scrollToBottom(force) { this.isUserScroll = false; - var postHolder = $('.post-list-holder-by-time'); - if ($('#new_message')[0] && !this.userHasSeenNew && !force) { - $('#new_message')[0].scrollIntoView(); + var postHolder = $(React.findDOMNode(this.refs.postlist)); + if ($('#new_message_' + this.props.channelId)[0] && !this.userHasSeenNew && !force) { + $('#new_message_' + this.props.channelId)[0].scrollIntoView(); } else { postHolder.addClass('hide-scroll'); postHolder[0].scrollTop = postHolder[0].scrollHeight; @@ -235,39 +232,36 @@ export default class PostList extends React.Component { } } loadFirstPosts(id) { + if (this.loadInProgress) { + return; + } + + if (this.props.channelId == null) { + return; + } + + this.loadInProgress = true; Client.getPosts( id, PostStore.getLatestUpdate(id), function success() { + this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); }.bind(this), function fail() { + this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); }.bind(this) ); } onChange() { - var newState = this.getStateFromStores(); - - // Special case where the channel wasn't yet set in componentDidMount - if (!this.state.isFirstLoadComplete && this.state.channel.id == null && newState.channel.id != null) { - this.loadFirstPosts(newState.channel.id); - } - - if (!utils.areStatesEqual(newState, this.state)) { - if (this.state.channel.id !== newState.channel.id) { - PostStore.clearUnseenDeletedPosts(this.state.channel.id); - this.userHasSeenNew = false; - newState.numToDisplay = Constants.POST_CHUNK_SIZE; - } else { - newState.lastViewed = this.state.lastViewed; - } + var newState = this.getStateFromStores(this.props.channelId); + if (!utils.areStatesEqual(newState.postList, this.state.postList)) { this.setState(newState); } } onSocketChange(msg) { - var postList; var post; if (msg.action === 'posted' || msg.action === 'post_edited') { post = JSON.parse(msg.props.post); @@ -280,7 +274,6 @@ export default class PostList extends React.Component { } post = JSON.parse(msg.props.post); - postList = this.state.postList; PostStore.storeUnseenDeletedPost(post); PostStore.removePost(post, true); @@ -401,6 +394,14 @@ export default class PostList extends React.Component { > <i className='fa fa-pencil'></i>Set a description </a> + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#channel_invite' + > + <i className='fa fa-user-plus'></i>Invite others to this channel + </a> </div> ); } @@ -412,7 +413,7 @@ export default class PostList extends React.Component { } } - var members = ChannelStore.getCurrentExtraInfo().members; + var members = ChannelStore.getExtraInfo(channel.id).members; for (var i = 0; i < members.length; i++) { if (members[i].roles.indexOf('admin') > -1) { return members[i].username; @@ -476,6 +477,11 @@ export default class PostList extends React.Component { var userId = UserStore.getCurrentId(); var renderedLastViewed = false; + var lastViewed = Number.MAX_VALUE; + + if (ChannelStore.getMember(this.props.channelId) != null) { + lastViewed = ChannelStore.getMember(this.props.channelId).last_viewed_at; + } var numToDisplay = this.state.numToDisplay; if (order.length - 1 < numToDisplay) { @@ -506,7 +512,7 @@ export default class PostList extends React.Component { var postCtl = ( <Post - key={post.id} + key={post.id + 'postKey'} ref={post.id} sameUser={sameUser} sameRoot={sameRoot} @@ -531,13 +537,13 @@ export default class PostList extends React.Component { ); } - if (post.user_id !== userId && post.create_at > this.state.lastViewed && !renderedLastViewed) { + if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) { renderedLastViewed = true; // Temporary fix to solve ie10/11 rendering issue let newSeparatorId = ''; if (!utils.isBrowserIE()) { - newSeparatorId = 'new_message'; + newSeparatorId = 'new_message_' + this.props.channelId; } postCtls.push( <div @@ -547,7 +553,7 @@ export default class PostList extends React.Component { > <hr className='separator__hr' - /> + /> <div className='separator__text'>New Messages</div> </div> ); @@ -565,16 +571,16 @@ export default class PostList extends React.Component { var posts = this.state.postList.posts; var order = this.state.postList.order; - var channelId = this.state.channel.id; + var channelId = this.props.channelId; - $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...'); + $(React.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...'); Client.getPostsPage( channelId, order.length, Constants.POST_CHUNK_SIZE, function success(data) { - $(this.refs.loadmore.getDOMNode()).text('Load more messages'); + $(React.findDOMNode(this.refs.loadmore)).text('Load more messages'); this.gotMorePosts = true; this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE}); @@ -599,7 +605,7 @@ export default class PostList extends React.Component { Client.getProfiles(); }.bind(this), function fail(err) { - $(this.refs.loadmore.getDOMNode()).text('Load more messages'); + $(React.findDOMNode(this.refs.loadmore)).text('Load more messages'); AsyncClient.dispatchError(err, 'getPosts'); }.bind(this) ); @@ -607,7 +613,7 @@ export default class PostList extends React.Component { render() { var order = []; var posts; - var channel = this.state.channel; + var channel = ChannelStore.get(this.props.channelId); if (this.state.postList != null) { posts = this.state.postList.posts; @@ -616,7 +622,7 @@ export default class PostList extends React.Component { var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; if (channel != null) { - if (order.length > this.state.numToDisplay) { + if (order.length >= this.state.numToDisplay) { moreMessages = ( <a ref='loadmore' @@ -636,11 +642,23 @@ export default class PostList extends React.Component { if (posts && this.state.isFirstLoadComplete) { postCtls = this.createPosts(posts, order); } else { - postCtls.push(<LoadingScreen position='absolute' />); + postCtls.push( + <LoadingScreen + position='absolute' + key='loading' + />); + } + + var activeClass = ''; + if (!this.props.isActive) { + activeClass = 'inactive'; } return ( - <div ref='postlist' className='post-list-holder-by-time'> + <div + ref='postlist' + className={'post-list-holder-by-time ' + activeClass} + > <div className='post-list__table'> <div className='post-list__content'> {moreMessages} @@ -651,3 +669,12 @@ export default class PostList extends React.Component { ); } } + +PostList.defaultProps = { + isActive: false, + channelId: null +}; +PostList.propTypes = { + isActive: React.PropTypes.bool, + channelId: React.PropTypes.string +}; diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx new file mode 100644 index 000000000..0815ac883 --- /dev/null +++ b/web/react/components/post_list_container.jsx @@ -0,0 +1,62 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +const PostList = require('./post_list.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); + +export default class PostListContainer extends React.Component { + constructor() { + super(); + + this.onChange = this.onChange.bind(this); + this.onLeave = this.onLeave.bind(this); + + let currentChannelId = ChannelStore.getCurrentId(); + if (currentChannelId) { + this.state = {currentChannelId: currentChannelId, postLists: [currentChannelId]}; + } else { + this.state = {currentChannelId: null, postLists: []}; + } + } + componentDidMount() { + ChannelStore.addChangeListener(this.onChange); + ChannelStore.addLeaveListener(this.onLeave); + } + onChange() { + let channelId = ChannelStore.getCurrentId(); + if (channelId === this.state.currentChannelId) { + return; + } + + let postLists = this.state.postLists; + if (postLists.indexOf(channelId) === -1) { + postLists.push(channelId); + } + this.setState({currentChannelId: channelId, postLists: postLists}); + } + onLeave(id) { + let postLists = this.state.postLists; + var index = postLists.indexOf(id); + if (index !== -1) { + postLists.splice(index, 1); + } + } + render() { + let postLists = this.state.postLists; + let channelId = this.state.currentChannelId; + + let postListCtls = []; + for (let i = 0; i <= this.state.postLists.length - 1; i++) { + postListCtls.push( + <PostList + channelId={postLists[i]} + isActive={postLists[i] === channelId} + /> + ); + } + + return ( + <div>{postListCtls}</div> + ); + } +} diff --git a/web/react/components/removed_from_channel_modal.jsx b/web/react/components/removed_from_channel_modal.jsx index 4a49e1c98..b7ec85457 100644 --- a/web/react/components/removed_from_channel_modal.jsx +++ b/web/react/components/removed_from_channel_modal.jsx @@ -3,62 +3,99 @@ var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -var BrowserStore = require('../stores/browser_store.jsx') +var BrowserStore = require('../stores/browser_store.jsx'); var utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - handleShow: function() { - var newState = {}; - if(BrowserStore.getItem("channel-removed-state")) { - newState = BrowserStore.getItem("channel-removed-state"); - BrowserStore.removeItem("channel-removed-state"); - } +export default class RemovedFromChannelModal extends React.Component { + constructor(props) { + super(props); - this.setState(newState); - }, - handleClose: function() { - var townSquare = ChannelStore.getByName("town-square"); - utils.switchChannel(townSquare); + this.handleShow = this.handleShow.bind(this); + this.handleClose = this.handleClose.bind(this); - this.setState({channelName: "", remover: ""}); - }, - componentDidMount: function() { - $(this.getDOMNode()).on('show.bs.modal',this.handleShow); - $(this.getDOMNode()).on('hidden.bs.modal',this.handleClose); - }, - componentWillUnmount: function() { - $(this.getDOMNode()).off('show.bs.modal',this.handleShow); - $(this.getDOMNode()).off('hidden.bs.modal',this.handleClose); - }, - getInitialState: function() { - return {channelName: "", remover: ""} - }, - render: function() { + this.state = { + channelName: '', + remover: '' + }; + } + + handleShow() { + var newState = {}; + if (BrowserStore.getItem('channel-removed-state')) { + newState = BrowserStore.getItem('channel-removed-state'); + BrowserStore.removeItem('channel-removed-state'); + } + + this.setState(newState); + } + + handleClose() { + var townSquare = ChannelStore.getByName('town-square'); + utils.switchChannel(townSquare); + + this.setState({channelName: '', remover: ''}); + } + + componentDidMount() { + $(React.findDOMNode(this)).on('show.bs.modal', this.handleShow); + $(React.findDOMNode(this)).on('hidden.bs.modal', this.handleClose); + } + + componentWillUnmount() { + $(React.findDOMNode(this)).off('show.bs.modal', this.handleShow); + $(React.findDOMNode(this)).off('hidden.bs.modal', this.handleClose); + } + + render() { var currentUser = UserStore.getCurrentUser(); - var channelName = this.state.channelName ? this.state.channelName : "the channel" - var remover = this.state.remover ? this.state.remover : "Someone" + + var channelName = 'the channel'; + if (this.state.channelName) { + channelName = this.state.channelName; + } + + var remover = 'Someone'; + if (this.state.remover) { + remover = this.state.remover; + } if (currentUser != null) { return ( - <div className='modal fade' ref='modal' id='removed_from_channel' tabIndex='-1' role='dialog' aria-hidden='true'> - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title'>Removed from <span className='name'>{channelName}</span></h4> - </div> - <div className='modal-body'> - <p>{remover} removed you from {channelName}</p> + <div + className='modal fade' + ref='modal' + id='removed_from_channel' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + ><span aria-hidden='true'>×</span></button> + <h4 className='modal-title'>Removed from <span className='name'>{channelName}</span></h4> + </div> + <div className='modal-body'> + <p>{remover} removed you from {channelName}</p> + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-primary' + data-dismiss='modal' + >Okay</button> + </div> </div> - <div className='modal-footer'> - <button type='button' className='btn btn-primary' data-dismiss='modal'>Okay</button> - </div> - </div> - </div> + </div> </div> ); - } else { - return <div/>; } + + return <div/>; } -});
\ No newline at end of file +}
\ No newline at end of file diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index 2fe6dd96b..37958b649 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -1,147 +1,217 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +const Utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); -var utils = require('../utils/utils.jsx'); -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); -var Constants = require('../utils/constants.jsx'); +export default class RenameChannelModal extends React.Component { + constructor(props) { + super(props); -module.exports = React.createClass({ - handleSubmit: function(e) { + this.handleSubmit = this.handleSubmit.bind(this); + this.onNameChange = this.onNameChange.bind(this); + this.onDisplayNameChange = this.onDisplayNameChange.bind(this); + this.displayNameKeyUp = this.displayNameKeyUp.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + displayName: '', + channelName: '', + channelId: '', + serverError: '', + nameError: '', + displayNameError: '', + invalid: false + }; + } + handleSubmit(e) { e.preventDefault(); - if (this.state.channel_id.length !== 26) return; + if (this.state.channelId.length !== 26) { + return; + } - var channel = ChannelStore.get(this.state.channel_id); - var oldName = channel.name - var oldDisplayName = channel.display_name - var state = { server_error: "" }; + let channel = ChannelStore.get(this.state.channelId); + const oldName = channel.name; + const oldDisplayName = channel.displayName; + let state = {serverError: ''}; - channel.display_name = this.state.display_name.trim(); + channel.display_name = this.state.displayName.trim(); if (!channel.display_name) { - state.display_name_error = "This field is required"; - state.inValid = true; - } - else if (channel.display_name.length > 22) { - state.display_name_error = "This field must be less than 22 characters"; - state.inValid = true; - } - else { - state.display_name_error = ""; + state.displayNameError = 'This field is required'; + state.invalid = true; + } else if (channel.display_name.length > 22) { + state.displayNameError = 'This field must be less than 22 characters'; + state.invalid = true; + } else { + state.displayNameError = ''; } - channel.name = this.state.channel_name.trim(); + channel.name = this.state.channelName.trim(); if (!channel.name) { - state.name_error = "This field is required"; - state.inValid = true; - } - else if(channel.name.length > 22){ - state.name_error = "This field must be less than 22 characters"; - state.inValid = true; - } - else { - var cleaned_name = utils.cleanUpUrlable(channel.name); - if (cleaned_name != channel.name) { - state.name_error = "Must be lowercase alphanumeric characters"; - state.inValid = true; - } - else { - state.name_error = ""; + state.nameError = 'This field is required'; + state.invalid = true; + } else if (channel.name.length > 22) { + state.nameError = 'This field must be less than 22 characters'; + state.invalid = true; + } else { + let cleanedName = Utils.cleanUpUrlable(channel.name); + if (cleanedName !== channel.name) { + state.nameError = 'Must be lowercase alphanumeric characters'; + state.invalid = true; + } else { + state.nameError = ''; } } this.setState(state); - if (state.inValid) - return; - - if (oldName == channel.name && oldDisplayName == channel.display_name) + if (state.invalid || (oldName === channel.name && oldDisplayName === channel.display_name)) { return; + } Client.updateChannel(channel, - function(data, text, req) { - $(this.refs.modal.getDOMNode()).modal('hide'); + function handleUpdateSuccess() { + $(React.findDOMNode(this.refs.modal)).modal('hide'); AsyncClient.getChannel(channel.id); - utils.updateTabTitle(channel.display_name); - utils.updateAddressBar(channel.name); + Utils.updateTabTitle(channel.display_name); + Utils.updateAddressBar(channel.name); - this.refs.display_name.getDOMNode().value = ""; - this.refs.channel_name.getDOMNode().value = ""; + React.findDOMNode(this.refs.displayName).value = ''; + React.findDOMNode(this.refs.channelName).value = ''; }.bind(this), - function(err) { - state.server_error = err.message; - state.inValid = true; + function handleUpdateError(err) { + state.serverError = err.message; + state.invalid = true; this.setState(state); }.bind(this) ); - }, - onNameChange: function() { - this.setState({ channel_name: this.refs.channel_name.getDOMNode().value }) - }, - onDisplayNameChange: function() { - this.setState({ display_name: this.refs.display_name.getDOMNode().value }) - }, - displayNameKeyUp: function(e) { - var display_name = this.refs.display_name.getDOMNode().value.trim(); - var channel_name = utils.cleanUpUrlable(display_name); - this.refs.channel_name.getDOMNode().value = channel_name; - this.setState({ channel_name: channel_name }) - }, - handleClose: function() { - this.setState({display_name: "", channel_name: "", display_name_error: "", server_error: "", name_error: ""}); - }, - componentDidMount: function() { - var self = this; - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { - var button = $(e.relatedTarget); - self.setState({ display_name: button.attr('data-display'), channel_name: button.attr('data-name'), channel_id: button.attr('data-channelid') }); - }); - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { - $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose); - }, - getInitialState: function() { - return { display_name: "", channel_name: "", channel_id: "" }; - }, - render: function() { - - var display_name_error = this.state.display_name_error ? <label className='control-label'>{ this.state.display_name_error }</label> : null; - var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null; - var server_error = this.state.server_error ? <div className='form-group has-error'><label className='control-label'>{ this.state.server_error }</label></div> : null; + } + onNameChange() { + this.setState({channelName: React.findDOMNode(this.refs.channelName).value}); + } + onDisplayNameChange() { + this.setState({displayName: React.findDOMNode(this.refs.displayName).value}); + } + displayNameKeyUp() { + const displayName = React.findDOMNode(this.refs.displayName).value.trim(); + const channelName = Utils.cleanUpUrlable(displayName); + React.findDOMNode(this.refs.channelName).value = channelName; + this.setState({channelName: channelName}); + } + handleClose() { + this.state = { + displayName: '', + channelName: '', + channelId: '', + serverError: '', + nameError: '', + displayNameError: '', + invalid: false + }; + } + componentDidMount() { + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function handleShow(e) { + const button = $(e.relatedTarget); + this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')}); + }.bind(this)); + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); + } + render() { + let displayNameError = null; + let displayNameClass = 'form-group'; + if (this.state.displayNameError) { + displayNameError = <label className='control-label'>{this.state.displayNameError}</label>; + displayNameClass += ' has-error'; + } + + let nameError = null; + let nameClass = 'form-group'; + if (this.state.nameError) { + nameError = <label className='control-label'>{this.state.nameError}</label>; + nameClass += ' has-error'; + } + + let serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } return ( - <div className="modal fade" ref="modal" id="rename_channel" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal"> - <span aria-hidden="true">×</span> - <span className="sr-only">Close</span> + <div + className='modal fade' + ref='modal' + id='rename_channel' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + > + <span aria-hidden='true'>×</span> + <span className='sr-only'>Close</span> </button> - <h4 className="modal-title">Rename Channel</h4> + <h4 className='modal-title'>Rename Channel</h4> </div> - <form role="form"> - <div className="modal-body"> - <div className={ this.state.display_name_error ? "form-group has-error" : "form-group" }> + <form role='form'> + <div className='modal-body'> + <div className={displayNameClass}> <label className='control-label'>Display Name</label> - <input onKeyUp={this.displayNameKeyUp} onChange={this.onDisplayNameChange} type="text" ref="display_name" className="form-control" placeholder="Enter display name" value={this.state.display_name} maxLength="64" /> - { display_name_error } + <input + onKeyUp={this.displayNameKeyUp} + onChange={this.onDisplayNameChange} + type='text' + ref='displayName' + className='form-control' + placeholder='Enter display name' + value={this.state.displayName} + maxLength='64' + /> + {displayNameError} </div> - <div className={ this.state.name_error ? "form-group has-error" : "form-group" }> + <div className={nameClass}> <label className='control-label'>Handle</label> - <input onChange={this.onNameChange} type="text" className="form-control" ref="channel_name" placeholder="lowercase alphanumeric's only" value={this.state.channel_name} maxLength="64" /> - { name_error } + <input + onChange={this.onNameChange} + type='text' + className='form-control' + ref='channelName' + placeholder='lowercase alphanumeric's only' + value={this.state.channelName} + maxLength='64' + /> + {nameError} </div> - { server_error } + {serverError} </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> - <button onClick={this.handleSubmit} type="submit" className="btn btn-primary">Save</button> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + > + Cancel + </button> + <button + onClick={this.handleSubmit} + type='submit' + className='btn btn-primary' + > + Save + </button> </div> </form> </div> @@ -149,4 +219,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 7df2fed9e..f1a90102c 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -6,18 +6,20 @@ var ChannelStore = require('../stores/channel_store.jsx'); var UserProfile = require('./user_profile.jsx'); var UserStore = require('../stores/user_store.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); var FileAttachmentList = require('./file_attachment_list.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var ActionTypes = Constants.ActionTypes; +var twemoji = require('twemoji'); export default class RhsComment extends React.Component { constructor(props) { super(props); this.retryComment = this.retryComment.bind(this); + this.parseEmojis = this.parseEmojis.bind(this); this.state = {}; } @@ -25,7 +27,7 @@ export default class RhsComment extends React.Component { e.preventDefault(); var post = this.props.post; - client.createPost(post, post.channel_id, + Client.createPost(post, post.channel_id, function success(data) { AsyncClient.getPosts(post.channel_id); @@ -51,13 +53,22 @@ export default class RhsComment extends React.Component { PostStore.updatePendingPost(post); this.forceUpdate(); } + parseEmojis() { + twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); + } + componentDidMount() { + this.parseEmojis(); + } shouldComponentUpdate(nextProps) { - if (!utils.areStatesEqual(nextProps.post, this.props.post)) { + if (!Utils.areStatesEqual(nextProps.post, this.props.post)) { return true; } return false; } + componentDidUpdate() { + this.parseEmojis(); + } render() { var post = this.props.post; @@ -73,7 +84,7 @@ export default class RhsComment extends React.Component { type = 'Comment'; } - var message = utils.textToJsx(post.message); + var message = Utils.textToJsx(post.message); var timestamp = UserStore.getCurrentUser().update_at; var loading; @@ -161,7 +172,8 @@ export default class RhsComment extends React.Component { filenames={post.filenames} modalId={'rhs_comment_view_image_modal_' + post.id} channelId={post.channel_id} - userId={post.user_id} /> + userId={post.user_id} + /> ); } @@ -182,7 +194,7 @@ export default class RhsComment extends React.Component { </li> <li className='post-header-col'> <time className='post-right-comment-time'> - {utils.displayCommentDateTime(post.create_at)} + {Utils.displayCommentDateTime(post.create_at)} </time> </li> <li className='post-header-col post-header__reply'> diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx index 4cf4231e9..5156ec4d7 100644 --- a/web/react/components/rhs_header_post.jsx +++ b/web/react/components/rhs_header_post.jsx @@ -1,9 +1,9 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; export default class RhsHeaderPost extends React.Component { constructor(props) { @@ -43,7 +43,7 @@ export default class RhsHeaderPost extends React.Component { }); } render() { - var back; + let back; if (this.props.fromSearch) { back = ( <a diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index a407e6470..83b57b955 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -6,12 +6,23 @@ var UserProfile = require('./user_profile.jsx'); var UserStore = require('../stores/user_store.jsx'); var utils = require('../utils/utils.jsx'); var FileAttachmentList = require('./file_attachment_list.jsx'); +var twemoji = require('twemoji'); +var Constants = require('../utils/constants.jsx'); export default class RhsRootPost extends React.Component { constructor(props) { super(props); + + this.parseEmojis = this.parseEmojis.bind(this); + this.state = {}; } + parseEmojis() { + twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE}); + } + componentDidMount() { + this.parseEmojis(); + } shouldComponentUpdate(nextProps) { if (!utils.areStatesEqual(nextProps.post, this.props.post)) { return true; @@ -19,6 +30,9 @@ export default class RhsRootPost extends React.Component { return false; } + componentDidUpdate() { + this.parseEmojis(); + } render() { var post = this.props.post; var message = utils.textToJsx(post.message); @@ -99,7 +113,8 @@ export default class RhsRootPost extends React.Component { filenames={post.filenames} modalId={'rhs_view_image_modal_' + post.id} channelId={post.channel_id} - userId={post.user_id} /> + userId={post.user_id} + /> ); } diff --git a/web/react/components/rhs_thread.jsx b/web/react/components/rhs_thread.jsx index adddeccf0..2f23d80d9 100644 --- a/web/react/components/rhs_thread.jsx +++ b/web/react/components/rhs_thread.jsx @@ -18,7 +18,6 @@ export default class RhsThread extends React.Component { this.onChange = this.onChange.bind(this); this.onChangeAll = this.onChangeAll.bind(this); - this.onTimeChange = this.onTimeChange.bind(this); this.state = this.getStateFromStores(); } @@ -33,7 +32,9 @@ export default class RhsThread extends React.Component { if (pendingPostList) { for (var pid in pendingPostList.posts) { - postList.posts[pid] = pendingPostList.posts[pid]; + if (pendingPostList.posts.hasOwnProperty(pid)) { + postList.posts[pid] = pendingPostList.posts[pid]; + } } } @@ -42,7 +43,6 @@ export default class RhsThread extends React.Component { componentDidMount() { PostStore.addSelectedPostChangeListener(this.onChange); PostStore.addChangeListener(this.onChangeAll); - UserStore.addStatusesChangeListener(this.onTimeChange); this.resize(); $(window).resize(function resize() { this.resize(); @@ -56,7 +56,6 @@ export default class RhsThread extends React.Component { componentWillUnmount() { PostStore.removeSelectedPostChangeListener(this.onChange); PostStore.removeChangeListener(this.onChangeAll); - UserStore.removeStatusesChangeListener(this.onTimeChange); } onChange() { var newState = this.getStateFromStores(); @@ -81,7 +80,9 @@ export default class RhsThread extends React.Component { if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) { currentSelected.posts = {}; for (var postId in currentPosts.posts) { - currentSelected.posts[postId] = currentPosts.posts[postId]; + if (currentPosts.posts.hasOwnProperty(postId)) { + currentSelected.posts[postId] = currentPosts.posts[postId]; + } } PostStore.storeSelectedPost(currentSelected); @@ -92,14 +93,6 @@ export default class RhsThread extends React.Component { this.setState(newState); } } - onTimeChange() { - for (var id in this.state.postList.posts) { - if (!this.refs[id]) { - continue; - } - this.refs[id].forceUpdate(); - } - } resize() { var height = $(window).height() - $('#error_bar').outerHeight() - 100; $('.post-right__scroll').css('height', height + 'px'); @@ -128,9 +121,11 @@ export default class RhsThread extends React.Component { var postsArray = []; for (var postId in postList.posts) { - var cpost = postList.posts[postId]; - if (cpost.root_id === rootPost.id) { - postsArray.push(cpost); + if (postList.posts.hasOwnProperty(postId)) { + var cpost = postList.posts[postId]; + if (cpost.root_id === rootPost.id) { + postsArray.push(cpost); + } } } @@ -167,8 +162,7 @@ export default class RhsThread extends React.Component { return ( <div className='post-right__container'> - <FileUploadOverlay - overlayType='right' /> + <FileUploadOverlay overlayType='right' /> <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div> <div className='sidebar-right__body'> <RhsHeaderPost @@ -185,7 +179,7 @@ export default class RhsThread extends React.Component { return ( <Comment ref={comPost.id} - key={comPost.id} + key={comPost.id + 'commentKey'} post={comPost} selected={(comPost.id === selectedPost.id)} /> @@ -209,6 +203,7 @@ RhsThread.defaultProps = { fromSearch: '', isMentionSearch: false }; + RhsThread.propTypes = { fromSearch: React.PropTypes.string, isMentionSearch: React.PropTypes.bool diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index b11b39e9e..006d15459 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var PostStore = require('../stores/post_store.jsx'); @@ -10,36 +9,47 @@ var utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -function getSearchTermStateFromStores() { - var term = PostStore.getSearchTerm() || ''; - return { - search_term: term - }; -} +export default class SearchBar extends React.Component { + constructor() { + super(); + this.mounted = false; -module.exports = React.createClass({ - displayName: 'SearchBar', - componentDidMount: function() { - PostStore.addSearchTermChangeListener(this._onChange); - }, - componentWillUnmount: function() { - PostStore.removeSearchTermChangeListener(this._onChange); - }, - _onChange: function(doSearch, isMentionSearch) { - if (this.isMounted()) { - var newState = getSearchTermStateFromStores(); + this.onListenerChange = this.onListenerChange.bind(this); + this.handleUserInput = this.handleUserInput.bind(this); + this.performSearch = this.performSearch.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = this.getSearchTermStateFromStores(); + } + getSearchTermStateFromStores() { + var term = PostStore.getSearchTerm() || ''; + return { + searchTerm: term + }; + } + componentDidMount() { + PostStore.addSearchTermChangeListener(this.onListenerChange); + this.mounted = true; + } + componentWillUnmount() { + PostStore.removeSearchTermChangeListener(this.onListenerChange); + this.mounted = false; + } + onListenerChange(doSearch, isMentionSearch) { + if (this.mounted) { + var newState = this.getSearchTermStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { this.setState(newState); } if (doSearch) { - this.performSearch(newState.search_term, isMentionSearch); + this.performSearch(newState.searchTerm, isMentionSearch); } } - }, - clearFocus: function(e) { + } + clearFocus() { $('.search-bar__container').removeClass('focused'); - }, - handleClose: function(e) { + } + handleClose(e) { e.preventDefault(); AppDispatcher.handleServerAction({ @@ -58,23 +68,23 @@ module.exports = React.createClass({ type: ActionTypes.RECIEVED_POST_SELECTED, results: null }); - }, - handleUserInput: function(e) { + } + handleUserInput(e) { var term = e.target.value; PostStore.storeSearchTerm(term); PostStore.emitSearchTermChange(false); - this.setState({ search_term: term }); - }, - handleUserFocus: function(e) { + this.setState({searchTerm: term}); + } + handleUserFocus(e) { e.target.select(); $('.search-bar__container').addClass('focused'); - }, - performSearch: function(terms, isMentionSearch) { + } + performSearch(terms, isMentionSearch) { if (terms.length) { this.setState({isSearching: true}); client.search( terms, - function(data) { + function success(data) { this.setState({isSearching: false}); if (utils.isMobile()) { React.findDOMNode(this.refs.search).value = ''; @@ -86,38 +96,54 @@ module.exports = React.createClass({ is_mention_search: isMentionSearch }); }.bind(this), - function(err) { + function error(err) { this.setState({isSearching: false}); - AsyncClient.dispatchError(err, "search"); + AsyncClient.dispatchError(err, 'search'); }.bind(this) ); } - }, - handleSubmit: function(e) { + } + handleSubmit(e) { e.preventDefault(); - this.performSearch(this.state.search_term.trim()); - }, - getInitialState: function() { - return getSearchTermStateFromStores(); - }, - render: function() { + this.performSearch(this.state.searchTerm.trim()); + } + render() { + var isSearching = null; + if (this.state.isSearching) { + isSearching = <span className={'glyphicon glyphicon-refresh glyphicon-refresh-animate'}></span>; + } return ( <div> - <div className="sidebar__collapse" onClick={this.handleClose}><span className="fa fa-angle-left"></span></div> - <span onClick={this.clearFocus} className="search__clear">Cancel</span> - <form role="form" className="search__form relative-div" onSubmit={this.handleSubmit}> - <span className="glyphicon glyphicon-search sidebar__search-icon"></span> + <div + className='sidebar__collapse' + onClick={this.handleClose} + > + <span className='fa fa-angle-left'></span> + </div> + <span + className='search__clear' + onClick={this.clearFocus} + > + Cancel + </span> + <form + role='form' + className='search__form relative-div' + onSubmit={this.handleSubmit} + > + <span className='glyphicon glyphicon-search sidebar__search-icon' /> <input - type="text" - ref="search" - className="form-control search-bar" - placeholder="Search" - value={this.state.search_term} + type='text' + ref='search' + className='form-control search-bar' + placeholder='Search' + value={this.state.searchTerm} onFocus={this.handleUserFocus} - onChange={this.handleUserInput} /> - {this.state.isSearching ? <span className={"glyphicon glyphicon-refresh glyphicon-refresh-animate"}></span> : null} + onChange={this.handleUserInput} + /> + {isSearching} </form> </div> ); } -}); +} diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index b1efd7685..6ee817171 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -1,181 +1,109 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - var PostStore = require('../stores/post_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -var UserProfile = require( './user_profile.jsx' ); -var SearchBox =require('./search_bar.jsx'); +var SearchBox = require('./search_bar.jsx'); var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; - -var RhsHeaderSearch = React.createClass({ - handleClose: function(e) { - e.preventDefault(); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH, - results: null - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH_TERM, - term: null, - do_search: false, - is_mention_search: false - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POST_SELECTED, - results: null - }); - }, - render: function() { - var title = this.props.isMentionSearch ? "Recent Mentions" : "Search Results"; - return ( - <div className="sidebar--right__header"> - <span className="sidebar--right__title">{title}</span> - <button type="button" className="sidebar--right__close" aria-label="Close" title="Close" onClick={this.handleClose}></button> - </div> - ); - } -}); - -var SearchItem = React.createClass({ - handleClick: function(e) { - e.preventDefault(); +var SearchResultsHeader = require('./search_results_header.jsx'); +var SearchResultsItem = require('./search_results_item.jsx'); - var self = this; - - client.getPost( - this.props.post.channel_id, - this.props.post.id, - function(data) { - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_POST_SELECTED, - post_list: data, - from_search: PostStore.getSearchTerm() - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH, - results: null, - is_mention_search: self.props.isMentionSearch - }); - }, - function(err) { - AsyncClient.dispatchError(err, "getPost"); - } - ); - - var postChannel = ChannelStore.get(this.props.post.channel_id); - var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : ""; - - utils.switchChannel(postChannel, teammate); - }, +function getStateFromStores() { + return {results: PostStore.getSearchResults()}; +} - render: function() { +export default class SearchResults extends React.Component { + constructor(props) { + super(props); - var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch}); - var channelName = ""; - var channel = ChannelStore.get(this.props.post.channel_id); - var timestamp = UserStore.getCurrentUser().update_at; + this.mounted = false; - if (channel) { - channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; - } + this.onChange = this.onChange.bind(this); + this.resize = this.resize.bind(this); - return ( - <div className="search-item-container post" onClick={this.handleClick}> - <div className="search-channel__name">{ channelName }</div> - <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> - </div> - <div className="post__content"> - <ul className="post-header"> - <li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li> - <li className="post-header-col"> - <time className="search-item-time"> - { utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at) } - </time> - </li> - </ul> - <div className="search-item-snippet"><span>{message}</span></div> - </div> - </div> - ); + this.state = getStateFromStores(); } -}); - -function getStateFromStores() { - return { results: PostStore.getSearchResults() }; -} - -module.exports = React.createClass({ - displayName: 'SearchResults', - componentDidMount: function() { - PostStore.addSearchChangeListener(this._onChange); + componentDidMount() { + this.mounted = true; + PostStore.addSearchChangeListener(this.onChange); this.resize(); var self = this; - $(window).resize(function(){ + $(window).resize(function resize() { self.resize(); }); - }, - componentDidUpdate: function() { + } + + componentDidUpdate() { this.resize(); - }, - componentWillUnmount: function() { - PostStore.removeSearchChangeListener(this._onChange); - }, - _onChange: function() { - if (this.isMounted()) { + } + + componentWillUnmount() { + PostStore.removeSearchChangeListener(this.onChange); + this.mounted = false; + } + + onChange() { + if (this.mounted) { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } - }, - getInitialState: function() { - return getStateFromStores(); - }, - resize: function() { + } + + resize() { var height = $(window).height() - $('#error_bar').outerHeight() - 100; - $("#search-items-container").css("height", height + "px"); - $("#search-items-container").scrollTop(0); - $("#search-items-container").perfectScrollbar(); - }, - render: function() { + $('#search-items-container').css('height', height + 'px'); + $('#search-items-container').scrollTop(0); + $('#search-items-container').perfectScrollbar(); + } + render() { var results = this.state.results; var currentId = UserStore.getCurrentId(); - var searchForm = currentId ? <SearchBox /> : null; + var searchForm = null; + if (currentId) { + searchForm = <SearchBox />; + } var noResults = (!results || !results.order || !results.order.length); var searchTerm = PostStore.getSearchTerm(); - return ( - <div className="sidebar--right__content"> - <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> - <div className="sidebar-right__body"> - <RhsHeaderSearch isMentionSearch={this.props.isMentionSearch} /> - <div id="search-items-container" className="search-items-container"> - - { noResults ? <div className="sidebar--right__subheader">No results</div> - : results.order.map(function(id) { - var post = results.posts[id]; - return <SearchItem key={post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} /> - }, this) - } + var ctls = null; + + if (noResults) { + ctls = <div className='sidebar--right__subheader'>No results</div>; + } else { + ctls = results.order.map(function mymap(id) { + var post = results.posts[id]; + return ( + <SearchResultsItem + key={post.id} + post={post} + term={searchTerm} + isMentionSearch={this.props.isMentionSearch} + /> + ); + }, this); + } + return ( + <div className='sidebar--right__content'> + <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div> + <div className='sidebar-right__body'> + <SearchResultsHeader isMentionSearch={this.props.isMentionSearch} /> + <div + id='search-items-container' + className='search-items-container' + > + {ctls} </div> </div> </div> ); } -}); +} + +SearchResults.propTypes = { + isMentionSearch: React.PropTypes.bool +}; diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx new file mode 100644 index 000000000..694f0c55d --- /dev/null +++ b/web/react/components/search_results_header.jsx @@ -0,0 +1,61 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var Constants = require('../utils/constants.jsx'); +var ActionTypes = Constants.ActionTypes; + +export default class SearchResultsHeader extends React.Component { + constructor(props) { + super(props); + + this.handleClose = this.handleClose.bind(this); + } + + handleClose(e) { + e.preventDefault(); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_SEARCH, + results: null + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_SEARCH_TERM, + term: null, + do_search: false, + is_mention_search: false + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST_SELECTED, + results: null + }); + } + + render() { + var title = 'Search Results'; + + if (this.props.isMentionSearch) { + title = 'Recent Mentions'; + } + + return ( + <div className='sidebar--right__header'> + <span className='sidebar--right__title'>{title}</span> + <button + type='button' + className='sidebar--right__close' + aria-label='Close' + title='Close' + onClick={this.handleClose} + > + </button> + </div> + ); + } +} + +SearchResultsHeader.propTypes = { + isMentionSearch: React.PropTypes.bool +};
\ No newline at end of file diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx new file mode 100644 index 000000000..aa56f1174 --- /dev/null +++ b/web/react/components/search_results_item.jsx @@ -0,0 +1,105 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var PostStore = require('../stores/post_store.jsx'); +var ChannelStore = require('../stores/channel_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var UserProfile = require('./user_profile.jsx'); +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var Constants = require('../utils/constants.jsx'); +var ActionTypes = Constants.ActionTypes; + +export default class SearchResultsItem extends React.Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + } + + handleClick(e) { + e.preventDefault(); + + var self = this; + + client.getPost( + this.props.post.channel_id, + this.props.post.id, + function success(data) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST_SELECTED, + post_list: data, + from_search: PostStore.getSearchTerm() + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_SEARCH, + results: null, + is_mention_search: self.props.isMentionSearch + }); + }, + function success(err) { + AsyncClient.dispatchError(err, 'getPost'); + } + ); + + var postChannel = ChannelStore.get(this.props.post.channel_id); + var teammate = ''; + + if (postChannel.type === 'D') { + teammate = utils.getDirectTeammate(this.props.post.channel_id).username; + } + + utils.switchChannel(postChannel, teammate); + } + + render() { + var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch}); + var channelName = ''; + var channel = ChannelStore.get(this.props.post.channel_id); + var timestamp = UserStore.getCurrentUser().update_at; + + if (channel) { + channelName = channel.display_name; + if (channel.type === 'D') { + channelName = 'Private Message'; + } + } + + return ( + <div + className='search-item-container post' + onClick={this.handleClick} + > + <div className='search-channel__name'>{channelName}</div> + <div className='post-profile-img__container'> + <img + className='post-profile-img' + src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp} + height='36' + width='36' + /> + </div> + <div className='post__content'> + <ul className='post-header'> + <li className='post-header-col'><strong><UserProfile userId={this.props.post.user_id} /></strong></li> + <li className='post-header-col'> + <time className='search-item-time'> + {utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at)} + </time> + </li> + </ul> + <div className='search-item-snippet'><span>{message}</span></div> + </div> + </div> + ); + } +} + +SearchResultsItem.propTypes = { + post: React.PropTypes.object, + isMentionSearch: React.PropTypes.bool, + term: React.PropTypes.string +};
\ No newline at end of file diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index 1599041b0..b1bab1d48 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -1,31 +1,73 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - render: function() { - var clientError = this.props.client_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.client_error }</label></div> : null; - var server_error = this.props.server_error ? <div className='form-group'><label className='col-sm-12 has-error'>{ this.props.server_error }</label></div> : null; +export default class SettingItemMax extends React.Component { + render() { + var clientError = null; + if (this.props.client_error) { + clientError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.client_error}</label></div>); + } + + var serverError = null; + if (this.props.server_error) { + serverError = (<div className='form-group'><label className='col-sm-12 has-error'>{this.props.server_error}</label></div>); + } + + var extraInfo = null; + if (this.props.extraInfo) { + extraInfo = (<div className='setting-list__hint'>{this.props.extraInfo}</div>); + } + + var submit = ''; + if (this.props.submit) { + submit = ( + <a + className='btn btn-sm btn-primary' + href='#' + onClick={this.props.submit} + > + Submit + </a> + ); + } var inputs = this.props.inputs; return ( - <ul className="section-max form-horizontal"> - <li className="col-sm-12 section-title">{this.props.title}</li> - <li className="col-sm-9 col-sm-offset-3"> - <ul className="setting-list"> - <li className="setting-list-item"> + <ul className='section-max form-horizontal'> + <li className='col-sm-12 section-title'>{this.props.title}</li> + <li className='col-sm-9 col-sm-offset-3'> + <ul className='setting-list'> + <li className='setting-list-item'> {inputs} + {extraInfo} </li> - <li className="setting-list-item"> + <li className='setting-list-item'> <hr /> - { server_error } - { clientError } - { this.props.submit ? <a className="btn btn-sm btn-primary" href="#" onClick={this.props.submit}>Submit</a> : "" } - <a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a> + {serverError} + {clientError} + {submit} + <a + className='btn btn-sm theme' + href='#' + onClick={this.props.updateSection} + > + Cancel + </a> </li> </ul> </li> </ul> ); } -}); +} + +SettingItemMax.propTypes = { + inputs: React.PropTypes.array, + client_error: React.PropTypes.string, + server_error: React.PropTypes.string, + extraInfo: React.PropTypes.element, + updateSection: React.PropTypes.func, + submit: React.PropTypes.func, + title: React.PropTypes.string +}; diff --git a/web/react/components/setting_item_min.jsx b/web/react/components/setting_item_min.jsx index 3c87e416e..2c0fdf2f4 100644 --- a/web/react/components/setting_item_min.jsx +++ b/web/react/components/setting_item_min.jsx @@ -1,25 +1,40 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - displayName: 'SettingsItemMin', - propTypes: { - title: React.PropTypes.string, - disableOpen: React.PropTypes.bool, - updateSection: React.PropTypes.func, - describe: React.PropTypes.string - }, - render: function() { - var editButton = ''; +export default class SettingItemMin extends React.Component { + render() { + let editButton = null; if (!this.props.disableOpen) { - editButton = <li className='col-sm-2 section-edit'><a className='section-edit theme' href='#' onClick={this.props.updateSection}>Edit</a></li>; + editButton = ( + <li className='col-sm-2 section-edit'> + <a + className='section-edit theme' + href='#' + onClick={this.props.updateSection} + > + <i className='fa fa-pencil'/> + {'Edit'} + </a> + </li> + ); } + return ( - <ul className='section-min'> + <ul + className='section-min' + onClick={this.props.updateSection} + > <li className='col-sm-10 section-title'>{this.props.title}</li> {editButton} <li className='col-sm-7 section-describe'>{this.props.describe}</li> </ul> ); } -}); +} + +SettingItemMin.propTypes = { + title: React.PropTypes.string, + disableOpen: React.PropTypes.bool, + updateSection: React.PropTypes.func, + describe: React.PropTypes.string +}; diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index 5b12ad7e9..a53112651 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -1,25 +1,35 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - setPicture: function(file) { +import {config} from '../utils/config.js'; + +export default class SettingPicture extends React.Component { + constructor(props) { + super(props); + + this.setPicture = this.setPicture.bind(this); + } + + setPicture(file) { if (file) { var reader = new FileReader(); - var img = this.refs.image.getDOMNode(); - reader.onload = function(e) { + var img = React.findDOMNode(this.refs.image); + reader.onload = function load(e) { $(img).attr('src', e.target.result); }; reader.readAsDataURL(file); } - }, - componentWillReceiveProps: function(nextProps) { + } + + componentWillReceiveProps(nextProps) { if (nextProps.picture) { this.setPicture(nextProps.picture); } - }, - render: function() { + } + + render() { var clientError = null; if (this.props.client_error) { clientError = <div className='form-group has-error'><label className='control-label'>{this.props.client_error}</label></div>; @@ -31,14 +41,31 @@ module.exports = React.createClass({ var img = null; if (this.props.picture) { - img = (<img ref='image' className='profile-img' src=''/>); + img = ( + <img + ref='image' + className='profile-img' + src='' + /> + ); } else { - img = (<img ref='image' className='profile-img' src={this.props.src}/>); + img = ( + <img + ref='image' + className='profile-img' + src={this.props.src} + /> + ); } var confirmButton; if (this.props.loadingPicture) { - confirmButton = <img className='spinner' src='/static/images/load.gif'/>; + confirmButton = ( + <img + className='spinner' + src='/static/images/load.gif' + /> + ); } else { var confirmButtonClass = 'btn btn-sm'; if (this.props.submitActive) { @@ -46,9 +73,15 @@ module.exports = React.createClass({ } else { confirmButtonClass += ' btn-inactive disabled'; } - confirmButton = <a className={confirmButtonClass} onClick={this.props.submit}>Save</a>; + + confirmButton = ( + <a + className={confirmButtonClass} + onClick={this.props.submit} + >Save</a> + ); } - var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + config.ProfileWidth + 'px in width and ' + config.ProfileHeight + 'px height.' + var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + config.ProfileWidth + 'px in width and ' + config.ProfileHeight + 'px height.'; var self = this; return ( @@ -65,13 +98,37 @@ module.exports = React.createClass({ <li className='setting-list-item'> {serverError} {clientError} - <span className='btn btn-sm btn-primary btn-file sel-btn'>Select<input ref='input' accept='.jpg,.png,.bmp' type='file' onChange={this.props.pictureChange}/></span> + <span className='btn btn-sm btn-primary btn-file sel-btn'> + Select + <input + ref='input' + accept='.jpg,.png,.bmp' + type='file' + onChange={this.props.pictureChange} + /> + </span> {confirmButton} - <a className='btn btn-sm theme' href='#' onClick={self.props.updateSection}>Cancel</a> + <a + className='btn btn-sm theme' + href='#' + onClick={self.props.updateSection} + >Cancel</a> </li> </ul> </li> </ul> ); } -}); +} + +SettingPicture.propTypes = { + client_error: React.PropTypes.string, + server_error: React.PropTypes.string, + src: React.PropTypes.string, + picture: React.PropTypes.object, + loadingPicture: React.PropTypes.bool, + submitActive: React.PropTypes.bool, + submit: React.PropTypes.func, + title: React.PropTypes.string, + pictureChange: React.PropTypes.func +}; diff --git a/web/react/components/setting_upload.jsx b/web/react/components/setting_upload.jsx index 596324308..fad27b355 100644 --- a/web/react/components/setting_upload.jsx +++ b/web/react/components/setting_upload.jsx @@ -1,53 +1,55 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -module.exports = React.createClass({ - displayName: 'Setting Upload', - propTypes: { - title: React.PropTypes.string.isRequired, - submit: React.PropTypes.func.isRequired, - fileTypesAccepted: React.PropTypes.string.isRequired, - clientError: React.PropTypes.string, - serverError: React.PropTypes.string, - helpText: React.PropTypes.string - }, - getInitialState: function() { - return { +export default class SettingsUpload extends React.Component { + constructor(props) { + super(props); + + this.doFileSelect = this.doFileSelect.bind(this); + this.doSubmit = this.doSubmit.bind(this); + this.onFileSelect = this.onFileSelect.bind(this); + + this.state = { clientError: this.props.clientError, serverError: this.props.serverError }; - }, - componentWillReceiveProps: function() { + } + + componentWillReceiveProps() { this.setState({ clientError: this.props.clientError, serverError: this.props.serverError }); - }, - doFileSelect: function(e) { + } + + doFileSelect(e) { e.preventDefault(); this.setState({ clientError: '', serverError: '' }); - }, - doSubmit: function(e) { + } + + doSubmit(e) { e.preventDefault(); - var inputnode = this.refs.uploadinput.getDOMNode(); + var inputnode = React.findDOMNode(this.refs.uploadinput); if (inputnode.files && inputnode.files[0]) { this.props.submit(inputnode.files[0]); } else { this.setState({clientError: 'No file selected.'}); } - }, - onFileSelect: function(e) { + } + + onFileSelect(e) { var filename = $(e.target).val(); if (filename.substring(3, 11) === 'fakepath') { filename = filename.substring(12); } $(e.target).closest('li').find('.file-status').addClass('hide'); $(e.target).closest('li').find('.file-name').removeClass('hide').html(filename); - }, - render: function() { + } + + render() { var clientError = null; if (this.state.clientError) { clientError = ( @@ -62,15 +64,24 @@ module.exports = React.createClass({ } return ( <ul className='section-max'> - <li className='col-xs-12 section-title'>{this.props.title}</li> - <li className='col-xs-offset-3'>{this.props.helpText}</li> - <li className='col-xs-offset-3 col-xs-8'> + <li className='col-sm-12 section-title'>{this.props.title}</li> + <li className='col-sm-offset-3 col-sm-9'>{this.props.helpText}</li> + <li className='col-sm-offset-3 col-sm-9'> <ul className='setting-list'> <li className='setting-list-item'> - <span className='btn btn-sm btn-primary btn-file sel-btn'>Select file<input ref='uploadinput' accept={this.props.fileTypesAccepted} type='file' onChange={this.onFileSelect}/></span> + <span className='btn btn-sm btn-primary btn-file sel-btn'> + Select file + <input + ref='uploadinput' + accept={this.props.fileTypesAccepted} + type='file' + onChange={this.onFileSelect} + /> + </span> <a className={'btn btn-sm btn-primary'} - onClick={this.doSubmit}> + onClick={this.doSubmit} + > Import </a> <div className='file-status file-name hide'></div> @@ -82,4 +93,13 @@ module.exports = React.createClass({ </ul> ); } -}); +} + +SettingsUpload.propTypes = { + title: React.PropTypes.string.isRequired, + submit: React.PropTypes.func.isRequired, + fileTypesAccepted: React.PropTypes.string.isRequired, + clientError: React.PropTypes.string, + serverError: React.PropTypes.string, + helpText: React.PropTypes.object +}; diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index d8091ec28..e5cbd6e92 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -1,24 +1,56 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); +export default class SettingsSidebar extends React.Component { + constructor(props) { + super(props); -module.exports = React.createClass({ - displayName:'SettingsSidebar', - updateTab: function(tab) { - this.props.updateTab(tab); + this.handleClick = this.handleClick.bind(this); + } + handleClick(tab) { + this.props.updateTab(tab.name); $('.settings-modal').addClass('display--content'); - }, - render: function() { - var self = this; + } + render() { + let tabList = this.props.tabs.map(function makeTab(tab) { + let key = `${tab.name}_li`; + let className = ''; + if (this.props.activeTab === tab.name) { + className = 'active'; + } + + return ( + <li + key={key} + className={className} + > + <a + href='#' + onClick={this.handleClick.bind(null, tab)} + > + <i className={tab.icon} /> + {tab.uiName} + </a> + </li> + ); + }.bind(this)); + return ( - <div className=""> - <ul className="nav nav-pills nav-stacked"> - {this.props.tabs.map(function(tab) { - return <li key={tab.name+'_li'} className={self.props.activeTab == tab.name ? 'active' : ''}><a key={tab.name + '_a'} href="#" onClick={function(){self.updateTab(tab.name);}}><i key={tab.name+'_i'} className={tab.icon}></i>{tab.uiName}</a></li> - })} + <div> + <ul className='nav nav-pills nav-stacked'> + {tabList} </ul> </div> ); } -}); +} + +SettingsSidebar.propTypes = { + tabs: React.PropTypes.arrayOf(React.PropTypes.shape({ + name: React.PropTypes.string.isRequired, + uiName: React.PropTypes.string.isRequired, + icon: React.PropTypes.string.isRequired + })).isRequired, + activeTab: React.PropTypes.string, + updateTab: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 5b74165f3..ad934d271 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -8,127 +8,137 @@ var SocketStore = require('../stores/socket_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var SidebarHeader = require('./sidebar_header.jsx'); var SearchBox = require('./search_bar.jsx'); var Constants = require('../utils/constants.jsx'); -function getStateFromStores() { - var members = ChannelStore.getAllMembers(); - var teamMemberMap = UserStore.getActiveOnlyProfiles(); - var currentId = ChannelStore.getCurrentId(); +export default class Sidebar extends React.Component { + constructor(props) { + super(props); - var teammates = []; - for (var id in teamMemberMap) { - if (id === UserStore.getCurrentId()) { - continue; - } - teammates.push(teamMemberMap[id]); - } + this.badgesActive = false; + this.firstUnreadChannel = null; + this.lastUnreadChannel = null; - // Create lists of all read and unread direct channels - var showDirectChannels = []; - var readDirectChannels = []; - for (var i = 0; i < teammates.length; i++) { - var teammate = teammates[i]; + this.onChange = this.onChange.bind(this); + this.onScroll = this.onScroll.bind(this); + this.onResize = this.onResize.bind(this); + this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this); + this.createChannelElement = this.createChannelElement.bind(this); - if (teammate.id === UserStore.getCurrentId()) { - continue; + this.state = this.getStateFromStores(); + this.state.loadingDMChannel = -1; + } + getStateFromStores() { + var members = ChannelStore.getAllMembers(); + var teamMemberMap = UserStore.getActiveOnlyProfiles(); + var currentId = ChannelStore.getCurrentId(); + + var teammates = []; + for (var id in teamMemberMap) { + if (id === UserStore.getCurrentId()) { + continue; + } + teammates.push(teamMemberMap[id]); } - var channelName = ''; - if (teammate.id > UserStore.getCurrentId()) { - channelName = UserStore.getCurrentId() + '__' + teammate.id; - } else { - channelName = teammate.id + '__' + UserStore.getCurrentId(); - } + // Create lists of all read and unread direct channels + var showDirectChannels = []; + var readDirectChannels = []; + for (var i = 0; i < teammates.length; i++) { + var teammate = teammates[i]; - var channel = ChannelStore.getByName(channelName); + if (teammate.id === UserStore.getCurrentId()) { + continue; + } - if (channel != null) { - channel.display_name = teammate.username; - channel.teammate_username = teammate.username; + var channelName = ''; + if (teammate.id > UserStore.getCurrentId()) { + channelName = UserStore.getCurrentId() + '__' + teammate.id; + } else { + channelName = teammate.id + '__' + UserStore.getCurrentId(); + } - channel.status = UserStore.getStatus(teammate.id); + var channel = ChannelStore.getByName(channelName); - var channelMember = members[channel.id]; - var msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - showDirectChannels.push(channel); - } else if (currentId === channel.id) { - showDirectChannels.push(channel); + if (channel != null) { + channel.display_name = teammate.username; + channel.teammate_username = teammate.username; + + channel.status = UserStore.getStatus(teammate.id); + + var channelMember = members[channel.id]; + var msgCount = channel.total_msg_count - channelMember.msg_count; + if (msgCount > 0) { + showDirectChannels.push(channel); + } else if (currentId === channel.id) { + showDirectChannels.push(channel); + } else { + readDirectChannels.push(channel); + } } else { - readDirectChannels.push(channel); + var tempChannel = {}; + tempChannel.fake = true; + tempChannel.name = channelName; + tempChannel.display_name = teammate.username; + tempChannel.teammate_username = teammate.username; + tempChannel.status = UserStore.getStatus(teammate.id); + tempChannel.last_post_at = 0; + tempChannel.total_msg_count = 0; + tempChannel.type = 'D'; + readDirectChannels.push(tempChannel); } - } else { - var tempChannel = {}; - tempChannel.fake = true; - tempChannel.name = channelName; - tempChannel.display_name = teammate.username; - tempChannel.teammate_username = teammate.username; - tempChannel.status = UserStore.getStatus(teammate.id); - tempChannel.last_post_at = 0; - tempChannel.total_msg_count = 0; - tempChannel.type = 'D'; - readDirectChannels.push(tempChannel); } - } - // If we don't have MAX_DMS unread channels, sort the read list by last_post_at - if (showDirectChannels.length < Constants.MAX_DMS) { - readDirectChannels.sort(function sortByLastPost(a, b) { - // sort by last_post_at first - if (a.last_post_at > b.last_post_at) { - return -1; - } - if (a.last_post_at < b.last_post_at) { - return 1; - } + // If we don't have MAX_DMS unread channels, sort the read list by last_post_at + if (showDirectChannels.length < Constants.MAX_DMS) { + readDirectChannels.sort(function sortByLastPost(a, b) { + // sort by last_post_at first + if (a.last_post_at > b.last_post_at) { + return -1; + } + if (a.last_post_at < b.last_post_at) { + return 1; + } - // if last_post_at is equal, sort by name - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; + // if last_post_at is equal, sort by name + if (a.display_name < b.display_name) { + return -1; + } + if (a.display_name > b.display_name) { + return 1; + } + return 0; + }); + + var index = 0; + while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { + showDirectChannels.push(readDirectChannels[index]); + index++; } - return 0; - }); + readDirectChannels = readDirectChannels.slice(index); - var index = 0; - while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { - showDirectChannels.push(readDirectChannels[index]); - index++; + showDirectChannels.sort(function directSort(a, b) { + if (a.display_name < b.display_name) { + return -1; + } + if (a.display_name > b.display_name) { + return 1; + } + return 0; + }); } - readDirectChannels = readDirectChannels.slice(index); - showDirectChannels.sort(function directSort(a, b) { - if (a.display_name < b.display_name) { - return -1; - } - if (a.display_name > b.display_name) { - return 1; - } - return 0; - }); + return { + activeId: currentId, + channels: ChannelStore.getAll(), + members: members, + showDirectChannels: showDirectChannels, + hideDirectChannels: readDirectChannels + }; } - - return { - activeId: currentId, - channels: ChannelStore.getAll(), - members: members, - showDirectChannels: showDirectChannels, - hideDirectChannels: readDirectChannels - }; -} - -module.exports = React.createClass({ - displayName: 'Sidebar', - propTypes: { - teamType: React.PropTypes.string, - teamDisplayName: React.PropTypes.string - }, - componentDidMount: function() { + componentDidMount() { ChannelStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange); @@ -140,12 +150,12 @@ module.exports = React.createClass({ this.updateUnreadIndicators(); $(window).on('resize', this.onResize); - }, - componentDidUpdate: function() { + } + componentDidUpdate() { this.updateTitle(); this.updateUnreadIndicators(); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $(window).off('resize', this.onResize); ChannelStore.removeChangeListener(this.onChange); @@ -153,14 +163,14 @@ module.exports = React.createClass({ UserStore.removeStatusesChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange); SocketStore.removeChangeListener(this.onSocketChange); - }, - onChange: function() { - var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { + } + onChange() { + var newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - }, - onSocketChange: function(msg) { + } + onSocketChange(msg) { if (msg.action === 'posted') { if (ChannelStore.getCurrentId() === msg.channel_id) { if (window.isActive) { @@ -208,17 +218,17 @@ module.exports = React.createClass({ if (notifyText.length === 0) { if (msgProps.image) { - utils.notifyMe(title, username + ' uploaded an image', channel); + Utils.notifyMe(title, username + ' uploaded an image', channel); } else if (msgProps.otherFile) { - utils.notifyMe(title, username + ' uploaded a file', channel); + Utils.notifyMe(title, username + ' uploaded a file', channel); } else { - utils.notifyMe(title, username + ' did something new', channel); + Utils.notifyMe(title, username + ' did something new', channel); } } else { - utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); + Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); } if (!user.notify_props || user.notify_props.desktop_sound === 'true') { - utils.ding(); + Utils.ding(); } } } else if (msg.action === 'viewed') { @@ -243,186 +253,198 @@ module.exports = React.createClass({ } } } - }, - updateTitle: function() { + } + updateTitle() { var channel = ChannelStore.getCurrent(); if (channel) { if (channel.type === 'D') { - var teammateUsername = utils.getDirectTeammate(channel.id).username; + var teammateUsername = Utils.getDirectTeammate(channel.id).username; document.title = teammateUsername + ' ' + document.title.substring(document.title.lastIndexOf('-')); } else { document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-')); } } - }, - onScroll: function() { + } + onScroll() { this.updateUnreadIndicators(); - }, - onResize: function() { + } + onResize() { this.updateUnreadIndicators(); - }, - updateUnreadIndicators: function() { - var container = $(this.refs.container.getDOMNode()); + } + updateUnreadIndicators() { + var container = $(React.findDOMNode(this.refs.container)); if (this.firstUnreadChannel) { - var firstUnreadElement = $(this.refs[this.firstUnreadChannel].getDOMNode()); + var firstUnreadElement = $(React.findDOMNode(this.refs[this.firstUnreadChannel])); if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) { - $(this.refs.topUnreadIndicator.getDOMNode()).css('display', 'initial'); + $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'initial'); } else { - $(this.refs.topUnreadIndicator.getDOMNode()).css('display', 'none'); + $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'none'); } } if (this.lastUnreadChannel) { - var lastUnreadElement = $(this.refs[this.lastUnreadChannel].getDOMNode()); + var lastUnreadElement = $(React.findDOMNode(this.refs[this.lastUnreadChannel])); if (lastUnreadElement.position().top > container.height()) { - $(this.refs.bottomUnreadIndicator.getDOMNode()).css('display', 'initial'); + $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'initial'); } else { - $(this.refs.bottomUnreadIndicator.getDOMNode()).css('display', 'none'); + $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'none'); } } - }, - getInitialState: function() { - var newState = getStateFromStores(); - newState.loadingDMChannel = -1; - - return newState; - }, - render: function() { + } + createChannelElement(channel, index) { var members = this.state.members; var activeId = this.state.activeId; - var badgesActive = false; + var channelMember = members[channel.id]; + var msgCount; - // keep track of the first and last unread channels so we can use them to set the unread indicators - var self = this; - this.firstUnreadChannel = null; - this.lastUnreadChannel = null; - - function createChannelElement(channel, index) { - var channelMember = members[channel.id]; - var msgCount; - - var linkClass = ''; - if (channel.id === activeId) { - linkClass = 'active'; - } - - var unread = false; - if (channelMember) { - msgCount = channel.total_msg_count - channelMember.msg_count; - unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0; - } + var linkClass = ''; + if (channel.id === activeId) { + linkClass = 'active'; + } - var titleClass = ''; - if (unread) { - titleClass = 'unread-title'; + var unread = false; + if (channelMember) { + msgCount = channel.total_msg_count - channelMember.msg_count; + unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0; + } - if (!self.firstUnreadChannel) { - self.firstUnreadChannel = channel.name; - } - self.lastUnreadChannel = channel.name; - } + var titleClass = ''; + if (unread) { + titleClass = 'unread-title'; - var badge = null; - if (channelMember) { - if (channel.type === 'D') { - // direct message channels show badges for any number of unread posts - msgCount = channel.total_msg_count - channelMember.msg_count; - if (msgCount > 0) { - badge = <span className='badge pull-right small'>{msgCount}</span>; - badgesActive = true; - } - } else if (channelMember.mention_count > 0) { - // public and private channels only show badges for mentions - badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; - badgesActive = true; + if (channel.id !== activeId) { + if (!this.firstUnreadChannel) { + this.firstUnreadChannel = channel.name; } - } else if (self.state.loadingDMChannel === index && channel.type === 'D') { - badge = <img className='channel-loading-gif pull-right' src='/static/images/load.gif'/>; + this.lastUnreadChannel = channel.name; } + } - // set up status icon for direct message channels - var status = null; + var badge = null; + if (channelMember) { if (channel.type === 'D') { - var statusIcon = ''; - if (channel.status === 'online') { - statusIcon = Constants.ONLINE_ICON_SVG; - } else if (channel.status === 'away') { - statusIcon = Constants.ONLINE_ICON_SVG; - } else { - statusIcon = Constants.OFFLINE_ICON_SVG; + // direct message channels show badges for any number of unread posts + msgCount = channel.total_msg_count - channelMember.msg_count; + if (msgCount > 0) { + badge = <span className='badge pull-right small'>{msgCount}</span>; + this.badgesActive = true; } - status = <span className='status' dangerouslySetInnerHTML={{__html: statusIcon}} />; + } else if (channelMember.mention_count > 0) { + // public and private channels only show badges for mentions + badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>; + this.badgesActive = true; } + } else if (this.state.loadingDMChannel === index && channel.type === 'D') { + badge = ( + <img + className='channel-loading-gif pull-right' + src='/static/images/load.gif' + /> + ); + } - // set up click handler to switch channels (or create a new channel for non-existant ones) - var handleClick = null; - var href = '#'; - var teamURL = TeamStore.getCurrentTeamUrl(); + // set up status icon for direct message channels + var status = null; + if (channel.type === 'D') { + var statusIcon = ''; + if (channel.status === 'online') { + statusIcon = Constants.ONLINE_ICON_SVG; + } else if (channel.status === 'away') { + statusIcon = Constants.ONLINE_ICON_SVG; + } else { + statusIcon = Constants.OFFLINE_ICON_SVG; + } + status = ( + <span + className='status' + dangerouslySetInnerHTML={{__html: statusIcon}} + /> + ); + } - if (!channel.fake) { + // set up click handler to switch channels (or create a new channel for non-existant ones) + var handleClick = null; + var href = '#'; + var teamURL = TeamStore.getCurrentTeamUrl(); + + if (!channel.fake) { + handleClick = function clickHandler(e) { + e.preventDefault(); + Utils.switchChannel(channel); + }; + } else if (channel.fake && teamURL) { + // It's a direct message channel that doesn't exist yet so let's create it now + var otherUserId = Utils.getUserIdFromChannelName(channel); + + if (this.state.loadingDMChannel === -1) { handleClick = function clickHandler(e) { e.preventDefault(); - utils.switchChannel(channel); - }; - } else if (channel.fake && teamURL) { - // It's a direct message channel that doesn't exist yet so let's create it now - var otherUserId = utils.getUserIdFromChannelName(channel); - - if (self.state.loadingDMChannel === -1) { - handleClick = function clickHandler(e) { - e.preventDefault(); - self.setState({loadingDMChannel: index}); - - Client.createDirectChannel(channel, otherUserId, - function success(data) { - self.setState({loadingDMChannel: -1}); - AsyncClient.getChannel(data.id); - utils.switchChannel(data); - }, - function error() { - self.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; - } - ); - }; - } + this.setState({loadingDMChannel: index}); + + Client.createDirectChannel(channel, otherUserId, + function success(data) { + this.setState({loadingDMChannel: -1}); + AsyncClient.getChannel(data.id); + Utils.switchChannel(data); + }.bind(this), + function error() { + this.setState({loadingDMChannel: -1}); + window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + }.bind(this) + ); + }.bind(this); } - - return ( - <li key={channel.name} ref={channel.name} className={linkClass}> - <a className={'sidebar-channel ' + titleClass} href={href} onClick={handleClick}> - {status} - {channel.display_name} - {badge} - </a> - </li> - ); } + return ( + <li + key={channel.name} + ref={channel.name} + className={linkClass} + > + <a + className={'sidebar-channel ' + titleClass} + href={href} + onClick={handleClick} + > + {status} + {channel.display_name} + {badge} + </a> + </li> + ); + } + render() { + this.badgesActive = false; + + // keep track of the first and last unread channels so we can use them to set the unread indicators + this.firstUnreadChannel = null; + this.lastUnreadChannel = null; + // create elements for all 3 types of channels var channelItems = this.state.channels.filter( function filterPublicChannels(channel) { return channel.type === 'O'; } - ).map(createChannelElement); + ).map(this.createChannelElement); var privateChannelItems = this.state.channels.filter( function filterPrivateChannels(channel) { return channel.type === 'P'; } - ).map(createChannelElement); + ).map(this.createChannelElement); - var directMessageItems = this.state.showDirectChannels.map(createChannelElement); + var directMessageItems = this.state.showDirectChannels.map(this.createChannelElement); // update the favicon to show if there are any notifications var link = document.createElement('link'); link.type = 'image/x-icon'; link.rel = 'shortcut icon'; link.id = 'favicon'; - if (badgesActive) { + if (this.badgesActive) { link.href = '/static/images/redfavicon.ico'; } else { link.href = '/static/images/favicon.ico'; @@ -438,7 +460,13 @@ module.exports = React.createClass({ if (this.state.hideDirectChannels.length > 0) { directMessageMore = ( <li> - <a href='#' data-toggle='modal' className='nav-more' data-target='#more_direct_channels' data-channels={JSON.stringify(this.state.hideDirectChannels)}> + <a + href='#' + data-toggle='modal' + className='nav-more' + data-target='#more_direct_channels' + data-channels={JSON.stringify(this.state.hideDirectChannels)} + > {'More (' + this.state.hideDirectChannels.length + ')'} </a> </li> @@ -447,21 +475,76 @@ module.exports = React.createClass({ return ( <div> - <SidebarHeader teamDisplayName={this.props.teamDisplayName} teamType={this.props.teamType} /> + <SidebarHeader + teamDisplayName={this.props.teamDisplayName} + teamType={this.props.teamType} + /> <SearchBox /> - <div ref='topUnreadIndicator' className='nav-pills__unread-indicator nav-pills__unread-indicator-top' style={{display: 'none'}}>Unread post(s) above</div> - <div ref='bottomUnreadIndicator' className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom' style={{display: 'none'}}>Unread post(s) below</div> + <div + ref='topUnreadIndicator' + className='nav-pills__unread-indicator nav-pills__unread-indicator-top' + style={{display: 'none'}} + > + Unread post(s) above + </div> + <div + ref='bottomUnreadIndicator' + className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom' + style={{display: 'none'}} + > + Unread post(s) below + </div> - <div ref='container' className='nav-pills__container' onScroll={this.onScroll}> + <div + ref='container' + className='nav-pills__container' + onScroll={this.onScroll} + > <ul className='nav nav-pills nav-stacked'> - <li><h4>Channels<a className='add-channel-btn' href='#' data-toggle='modal' data-target='#new_channel' data-channeltype='O'>+</a></h4></li> + <li> + <h4> + Channels + <a + className='add-channel-btn' + href='#' + data-toggle='modal' + data-target='#new_channel' + data-channeltype='O' + > + + + </a> + </h4> + </li> {channelItems} - <li><a href='#' data-toggle='modal' className='nav-more' data-target='#more_channels' data-channeltype='O'>More...</a></li> + <li> + <a + href='#' + data-toggle='modal' + className='nav-more' + data-target='#more_channels' + data-channeltype='O' + > + More... + </a> + </li> </ul> <ul className='nav nav-pills nav-stacked'> - <li><h4>Private Groups<a className='add-channel-btn' href='#' data-toggle='modal' data-target='#new_channel' data-channeltype='P'>+</a></h4></li> + <li> + <h4> + Private Groups + <a + className='add-channel-btn' + href='#' + data-toggle='modal' + data-target='#new_channel' + data-channeltype='P' + > + + + </a> + </h4> + </li> {privateChannelItems} </ul> <ul className='nav nav-pills nav-stacked'> @@ -473,4 +556,13 @@ module.exports = React.createClass({ </div> ); } -}); +} + +Sidebar.defaultProps = { + teamType: '', + teamDisplayName: '' +}; +Sidebar.propTypes = { + teamType: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index d5d16816f..0056d7a2f 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -1,133 +1,26 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +var NavbarDropdown = require('./navbar_dropdown.jsx'); var UserStore = require('../stores/user_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); +import {config} from '../utils/config.js'; -var Constants = require('../utils/constants.jsx'); +export default class SidebarHeader extends React.Component { + constructor(props) { + super(props); -function getStateFromStores() { - return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()}; -} - -var NavbarDropdown = React.createClass({ - handleLogoutClick: function(e) { - e.preventDefault(); - client.logout(); - }, - blockToggle: false, - componentDidMount: function() { - UserStore.addTeamsChangeListener(this.onListenerChange); - TeamStore.addChangeListener(this.onListenerChange); - - var self = this; - $(this.refs.dropdown.getDOMNode()).on('hide.bs.dropdown', function() { - self.blockToggle = true; - setTimeout(function() { - self.blockToggle = false; - }, 100); - }); - }, - componentWillUnmount: function() { - UserStore.removeTeamsChangeListener(this.onListenerChange); - TeamStore.removeChangeListener(this.onListenerChange); - - $(this.refs.dropdown.getDOMNode()).off('hide.bs.dropdown'); - }, - onListenerChange: function() { - if (this.isMounted()) { - var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { - this.setState(newState); - } - } - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { - var teamLink = ''; - var inviteLink = ''; - var manageLink = ''; - var currentUser = UserStore.getCurrentUser(); - var isAdmin = false; - var teamSettings = null; - - if (currentUser != null) { - isAdmin = currentUser.roles.indexOf('admin') > -1; - - inviteLink = (<li> <a href='#' data-toggle='modal' data-target='#invite_member'>Invite New Member</a> </li>); - - if (this.props.teamType === 'O') { - teamLink = ( - <li> - <a href='#' data-toggle='modal' data-target='#get_link' data-title='Team Invite' data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}>Get Team Invite Link</a> - </li> - ); - } - } - - if (isAdmin) { - manageLink = (<li> <a href='#' data-toggle='modal' data-target='#team_members'>Manage Team</a> </li>); - teamSettings = (<li> <a href='#' data-toggle='modal' data-target='#team_settings'>Team Settings</a> </li>); - } + this.toggleDropdown = this.toggleDropdown.bind(this); - var teams = []; - - teams.push(<li className='divider' key='div'></li>); - if (this.state.teams.length > 1 && this.state.currentTeam) { - var curTeamName = this.state.currentTeam.name; - this.state.teams.forEach(function(teamName) { - if (teamName !== curTeamName) { - teams.push(<li key={teamName}><a href={utils.getWindowLocationOrigin() + '/' + teamName}>Switch to {teamName}</a></li>); - } - }); - } - teams.push(<li key='newTeam_li'><a key='newTeam_a' href={utils.getWindowLocationOrigin() + '/signup_team' }>Create a New Team</a></li>); - - return ( - <ul className='nav navbar-nav navbar-right'> - <li ref='dropdown' className='dropdown'> - <a href='#' className='dropdown-toggle' data-toggle='dropdown' role='button' aria-expanded='false'> - <span className='dropdown__icon' dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} /> - </a> - <ul className='dropdown-menu' role='menu'> - <li><a href='#' data-toggle='modal' data-target='#user_settings'>Account Settings</a></li> - {teamSettings} - {inviteLink} - {teamLink} - {manageLink} - <li><a href='#' onClick={this.handleLogoutClick}>Logout</a></li> - {teams} - <li className='divider'></li> - <li><a target='_blank' href={config.HelpLink}>Help</a></li> - <li><a target='_blank' href={config.ReportProblemLink}>Report a Problem</a></li> - </ul> - </li> - </ul> - ); + this.state = {}; } -}); - -module.exports = React.createClass({ - displayName: 'SidebarHeader', - getDefaultProps: function() { - return { - teamDisplayName: config.SiteName - }; - }, - - toggleDropdown: function() { + toggleDropdown() { if (this.refs.dropdown.blockToggle) { this.refs.dropdown.blockToggle = false; return; } $('.team__header').find('.dropdown-toggle').dropdown('toggle'); - }, - - render: function() { + } + render() { var me = UserStore.getCurrentUser(); var profilePicture = null; @@ -136,20 +29,40 @@ module.exports = React.createClass({ } if (me.last_picture_update) { - profilePicture = (<img className='user__picture' src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} />); + profilePicture = ( + <img + className='user__picture' + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} + /> + ); } return ( <div className='team__header theme'> - <a href='#' onClick={this.toggleDropdown}> + <a + href='#' + onClick={this.toggleDropdown} + > {profilePicture} <div className='header__info'> <div className='user__name'>{'@' + me.username}</div> - <div className='team__name'>{this.props.teamDisplayName }</div> + <div className='team__name'>{this.props.teamDisplayName}</div> </div> </a> - <NavbarDropdown ref='dropdown' teamType={this.props.teamType} /> + <NavbarDropdown + ref='dropdown' + teamType={this.props.teamType} + /> </div> ); } -}); +} + +SidebarHeader.defaultProps = { + teamDisplayName: config.SiteName, + teamType: '' +}; +SidebarHeader.propTypes = { + teamDisplayName: React.PropTypes.string, + teamType: React.PropTypes.string +}; diff --git a/web/react/components/sidebar_right.jsx b/web/react/components/sidebar_right.jsx index df75e3adf..913715154 100644 --- a/web/react/components/sidebar_right.jsx +++ b/web/react/components/sidebar_right.jsx @@ -4,47 +4,49 @@ var SearchResults = require('./search_results.jsx'); var RhsThread = require('./rhs_thread.jsx'); var PostStore = require('../stores/post_store.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -function getStateFromStores(from_search) { - return { search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch() }; +function getStateFromStores() { + return {search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch()}; } -module.exports = React.createClass({ - componentDidMount: function() { - PostStore.addSearchChangeListener(this._onSearchChange); - PostStore.addSelectedPostChangeListener(this._onSelectedChange); - }, - componentWillUnmount: function() { - PostStore.removeSearchChangeListener(this._onSearchChange); - PostStore.removeSelectedPostChangeListener(this._onSelectedChange); - }, - _onSelectedChange: function(from_search) { - if (this.isMounted()) { - var newState = getStateFromStores(from_search); - newState.from_search = from_search; - if (!utils.areStatesEqual(newState, this.state)) { - this.setState(newState); - } +export default class SidebarRight extends React.Component { + constructor(props) { + super(props); + + this.onSelectedChange = this.onSelectedChange.bind(this); + this.onSearchChange = this.onSearchChange.bind(this); + this.resize = this.resize.bind(this); + + this.state = getStateFromStores(); + } + componentDidMount() { + PostStore.addSearchChangeListener(this.onSearchChange); + PostStore.addSelectedPostChangeListener(this.onSelectedChange); + } + componentWillUnmount() { + PostStore.removeSearchChangeListener(this.onSearchChange); + PostStore.removeSelectedPostChangeListener(this.onSelectedChange); + } + onSelectedChange(fromSearch) { + var newState = getStateFromStores(fromSearch); + newState.from_search = fromSearch; + if (!Utils.areStatesEqual(newState, this.state)) { + this.setState(newState); } - }, - _onSearchChange: function() { - if (this.isMounted()) { - var newState = getStateFromStores(); - if (!utils.areStatesEqual(newState, this.state)) { - this.setState(newState); - } + } + onSearchChange() { + var newState = getStateFromStores(); + if (!Utils.areStatesEqual(newState, this.state)) { + this.setState(newState); } - }, - resize: function() { + } + resize() { var postHolder = $('.post-list-holder-by-time'); postHolder[0].scrollTop = postHolder[0].scrollHeight - 224; - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { - if (! (this.state.search_visible || this.state.post_right_visible)) { + } + render() { + if (!(this.state.search_visible || this.state.post_right_visible)) { $('.inner__wrap').removeClass('move--left').removeClass('move--right'); $('.sidebar--right').removeClass('move--left'); this.resize(); @@ -58,25 +60,29 @@ module.exports = React.createClass({ $('.sidebar--right').addClass('move--left'); $('.sidebar--right').prepend('<div class="sidebar__overlay"></div>'); this.resize(); - setTimeout(function(){ - $('.sidebar__overlay').fadeOut("200", function(){ + setTimeout(function overlayTimer() { + $('.sidebar__overlay').fadeOut('200', function fadeOverlay() { $(this).remove(); }); - },500) + }, 500); - var content = ""; + var content = ''; if (this.state.search_visible) { content = <SearchResults isMentionSearch={this.state.is_mention_search} />; - } - else if (this.state.post_right_visible) { - content = <RhsThread fromSearch={this.state.from_search} isMentionSearch={this.state.is_mention_search} />; + } else if (this.state.post_right_visible) { + content = ( + <RhsThread + fromSearch={this.state.from_search} + isMentionSearch={this.state.is_mention_search} + /> + ); } return ( - <div className="sidebar-right-container"> - { content } + <div className='sidebar-right-container'> + {content} </div> ); } -}); +} diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 615bc4ef2..bd10a6ef1 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -4,13 +4,21 @@ var UserStore = require('../stores/user_store.jsx'); var client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); +import {config} from '../utils/config.js'; -module.exports = React.createClass({ - handleLogoutClick: function(e) { +export default class SidebarRightMenu extends React.Component { + constructor(props) { + super(props); + + this.handleLogoutClick = this.handleLogoutClick.bind(this); + } + + handleLogoutClick(e) { e.preventDefault(); client.logout(); - }, - render: function() { + } + + render() { var teamLink = ''; var inviteLink = ''; var teamSettingsLink = ''; @@ -23,14 +31,22 @@ module.exports = React.createClass({ inviteLink = ( <li> - <a href='#' data-toggle='modal' data-target='#invite_member'><i className='glyphicon glyphicon-user'></i>Invite New Member</a> + <a href='#' + data-toggle='modal' + data-target='#invite_member' + ><i className='glyphicon glyphicon-user'></i>Invite New Member</a> </li> ); if (this.props.teamType === 'O') { teamLink = ( <li> - <a href='#' data-toggle='modal' data-target='#get_link' data-title='Team Invite' data-value={utils.getWindowLocationOrigin()+'/signup_user_complete/?id='+currentUser.team_id}><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a> + <a href='#' + data-toggle='modal' + data-target='#get_link' + data-title='Team Invite' + data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id} + ><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a> </li> ); } @@ -39,12 +55,21 @@ module.exports = React.createClass({ if (isAdmin) { teamSettingsLink = ( <li> - <a href='#' data-toggle='modal' data-target='#team_settings'><i className='glyphicon glyphicon-globe'></i>Team Settings</a> + <a + href='#' + data-toggle='modal' + data-target='#team_settings' + ><i className='glyphicon glyphicon-globe'></i>Team Settings</a> </li> ); manageLink = ( <li> - <a href='#' data-toggle='modal' data-target='#team_members'><i className='glyphicon glyphicon-wrench'></i>Manage Team</a> + <a + href='#' + data-toggle='modal' + data-target='#team_members' + > + <i className='glyphicon glyphicon-wrench'></i>Manage Team</a> </li> ); } @@ -61,23 +86,48 @@ module.exports = React.createClass({ return ( <div> <div className='team__header theme'> - <a className='team__name' href='/channels/town-square'>{teamDisplayName}</a> + <a + className='team__name' + href='/channels/town-square' + >{teamDisplayName}</a> </div> <div className='nav-pills__container'> <ul className='nav nav-pills nav-stacked'> - <li><a href='#' data-toggle='modal' data-target='#user_settings'><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> + <li> + <a + href='#' + data-toggle='modal' + data-target='#user_settings' + ><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> {teamSettingsLink} {inviteLink} {teamLink} {manageLink} - <li><a href='#' onClick={this.handleLogoutClick}><i className='glyphicon glyphicon-log-out'></i>Logout</a></li> + <li> + <a + href='#' + onClick={this.handleLogoutClick} + ><i className='glyphicon glyphicon-log-out'></i>Logout</a></li> <li className='divider'></li> - <li><a target='_blank' href='/static/help/configure_links.html'><i className='glyphicon glyphicon-question-sign'></i>Help</a></li> - <li><a target='_blank' href='/static/help/configure_links.html'><i className='glyphicon glyphicon-earphone'></i>Report a Problem</a></li> + <li> + <a + target='_blank' + href='/static/help/configure_links.html' + ><i className='glyphicon glyphicon-question-sign'></i>Help</a></li> + <li> + <a + target='_blank' + href='/static/help/configure_links.html' + ><i className='glyphicon glyphicon-earphone'></i>Report a Problem</a></li> </ul> </div> </div> ); } -}); +} + +SidebarRightMenu.propTypes = { + teamType: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 13640b1e5..bf08e6508 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -1,10 +1,10 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var ChoosePage = require('./team_signup_choose_auth.jsx'); -var EmailSignUpPage = require('./team_signup_with_email.jsx'); -var SSOSignupPage = require('./team_signup_with_sso.jsx'); -var Constants = require('../utils/constants.jsx'); +const ChoosePage = require('./team_signup_choose_auth.jsx'); +const EmailSignUpPage = require('./team_signup_with_email.jsx'); +const SSOSignupPage = require('./team_signup_with_sso.jsx'); +const Constants = require('../utils/constants.jsx'); export default class TeamSignUp extends React.Component { constructor(props) { @@ -30,14 +30,14 @@ export default class TeamSignUp extends React.Component { return <EmailSignUpPage />; } else if (this.state.page === 'service' && this.state.service !== '') { return <SSOSignupPage service={this.state.service} />; - } else { - return ( - <ChoosePage - services={this.props.services} - updatePage={this.updatePage} - /> - ); } + + return ( + <ChoosePage + services={this.props.services} + updatePage={this.updatePage} + /> + ); } } diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx index 756aae638..dc0d1d376 100644 --- a/web/react/components/signup_team_complete.jsx +++ b/web/react/components/signup_team_complete.jsx @@ -10,69 +10,113 @@ var UsernamePage = require('./team_signup_username_page.jsx'); var PasswordPage = require('./team_signup_password_page.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); -module.exports = React.createClass({ - displayName: 'SignupTeamComplete', - propTypes: { - hash: React.PropTypes.string, - email: React.PropTypes.string, - data: React.PropTypes.string - }, - updateParent: function(state, skipSet) { +export default class SignupTeamComplete extends React.Component { + constructor(props) { + super(props); + + this.updateParent = this.updateParent.bind(this); + + var initialState = BrowserStore.getGlobalItem(props.hash); + + if (!initialState) { + initialState = {}; + initialState.wizard = 'welcome'; + initialState.team = {}; + initialState.team.email = this.props.email; + initialState.team.allowed_domains = ''; + initialState.invites = []; + initialState.invites.push(''); + initialState.invites.push(''); + initialState.invites.push(''); + initialState.user = {}; + initialState.hash = this.props.hash; + initialState.data = this.props.data; + } + + this.state = initialState; + } + updateParent(state, skipSet) { BrowserStore.setGlobalItem(this.props.hash, state); if (!skipSet) { this.setState(state); } - }, - getInitialState: function() { - var props = BrowserStore.getGlobalItem(this.props.hash); - - if (!props) { - props = {}; - props.wizard = 'welcome'; - props.team = {}; - props.team.email = this.props.email; - props.team.allowed_domains = ''; - props.invites = []; - props.invites.push(''); - props.invites.push(''); - props.invites.push(''); - props.user = {}; - props.hash = this.props.hash; - props.data = this.props.data; - } - - return props; - }, - render: function() { + } + render() { if (this.state.wizard === 'welcome') { - return <WelcomePage state={this.state} updateParent={this.updateParent} />; + return ( + <WelcomePage + state={this.state} + updateParent={this.updateParent} + /> + ); } if (this.state.wizard === 'team_display_name') { - return <TeamDisplayNamePage state={this.state} updateParent={this.updateParent} />; + return ( + <TeamDisplayNamePage + state={this.state} + updateParent={this.updateParent} + /> + ); } if (this.state.wizard === 'team_url') { - return <TeamURLPage state={this.state} updateParent={this.updateParent} />; + return ( + <TeamURLPage + state={this.state} + updateParent={this.updateParent} + /> + ); } if (this.state.wizard === 'allowed_domains') { - return <AllowedDomainsPage state={this.state} updateParent={this.updateParent} />; + return ( + <AllowedDomainsPage + state={this.state} + updateParent={this.updateParent} + /> + ); } if (this.state.wizard === 'send_invites') { - return <SendInivtesPage state={this.state} updateParent={this.updateParent} />; + return ( + <SendInivtesPage + state={this.state} + updateParent={this.updateParent} + /> + ); } if (this.state.wizard === 'username') { - return <UsernamePage state={this.state} updateParent={this.updateParent} />; + return ( + <UsernamePage + state={this.state} + updateParent={this.updateParent} + /> + ); } if (this.state.wizard === 'password') { - return <PasswordPage state={this.state} updateParent={this.updateParent} />; + return ( + <PasswordPage + state={this.state} + updateParent={this.updateParent} + /> + ); } return (<div>You've already completed the signup process for this invitation or this invitation has expired.</div>); } -}); +} + +SignupTeamComplete.defaultProps = { + hash: '', + email: '', + data: '' +}; +SignupTeamComplete.propTypes = { + hash: React.PropTypes.string, + email: React.PropTypes.string, + data: React.PropTypes.string +}; diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 2080cc191..f078f6169 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -6,12 +6,33 @@ var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); var Constants = require('../utils/constants.jsx'); +import {config} from '../utils/config.js'; -module.exports = React.createClass({ - handleSubmit: function(e) { +export default class SignupUserComplete extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + var initialState = BrowserStore.getGlobalItem(this.props.hash); + + if (!initialState) { + initialState = {}; + initialState.wizard = 'welcome'; + initialState.user = {}; + initialState.user.team_id = this.props.teamId; + initialState.user.email = this.props.email; + initialState.hash = this.props.hash; + initialState.data = this.props.data; + initialState.original_email = this.props.email; + } + + this.state = initialState; + } + handleSubmit(e) { e.preventDefault(); - this.state.user.username = this.refs.name.getDOMNode().value.trim(); + this.state.user.username = React.findDOMNode(this.refs.name).value.trim(); if (!this.state.user.username) { this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''}); return; @@ -31,14 +52,14 @@ module.exports = React.createClass({ return; } - this.state.user.email = this.refs.email.getDOMNode().value.trim(); + this.state.user.email = React.findDOMNode(this.refs.email).value.trim(); if (!this.state.user.email) { this.setState({nameError: '', emailError: 'This field is required', passwordError: ''}); return; } - this.state.user.password = this.refs.password.getDOMNode().value.trim(); - if (!this.state.user.password || this.state.user.password .length < 5) { + this.state.user.password = React.findDOMNode(this.refs.password).value.trim(); + if (!this.state.user.password || this.state.user.password .length < 5) { this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''}); return; } @@ -48,11 +69,11 @@ module.exports = React.createClass({ this.state.user.allow_marketing = true; client.createUser(this.state.user, this.state.data, this.state.hash, - function(data) { + function createUserSuccess() { client.track('signup', 'signup_user_02_complete'); client.loginByEmail(this.props.teamName, this.state.user.email, this.state.user.password, - function(data) { + function emailLoginSuccess(data) { UserStore.setLastEmail(this.state.user.email); UserStore.setCurrentUser(data); if (this.props.hash > 0) { @@ -60,7 +81,7 @@ module.exports = React.createClass({ } window.location.href = '/'; }.bind(this), - function(err) { + function emailLoginFailure(err) { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(this.state.user.email) + '&teamname=' + encodeURIComponent(this.props.teamName); } else { @@ -69,28 +90,12 @@ module.exports = React.createClass({ }.bind(this) ); }.bind(this), - function(err) { + function createUserFailure(err) { this.setState({serverError: err.message}); }.bind(this) ); - }, - getInitialState: function() { - var state = BrowserStore.getGlobalItem(this.props.hash); - - if (!state) { - state = {}; - state.wizard = 'welcome'; - state.user = {}; - state.user.team_id = this.props.teamId; - state.user.email = this.props.email; - state.hash = this.props.hash; - state.data = this.props.data; - state.original_email = this.props.email; - } - - return state; - }, - render: function() { + } + render() { client.track('signup', 'signup_user_01_welcome'); if (this.state.wizard === 'finished') { @@ -134,16 +139,24 @@ module.exports = React.createClass({ yourEmailIs = <span>Your email address is {this.state.user.email}. You'll use this address to sign in to {config.SiteName}.</span>; } - var emailContainerStyle = "margin--extra"; + var emailContainerStyle = 'margin--extra'; if (this.state.original_email) { - emailContainerStyle = "hidden"; + emailContainerStyle = 'hidden'; } var email = ( <div className={emailContainerStyle}> <h5><strong>What's your email address?</strong></h5> <div className={emailDivStyle}> - <input type='email' ref='email' className='form-control' defaultValue={this.state.user.email} placeholder='' maxLength='128' autoFocus={true} /> + <input + type='email' + ref='email' + className='form-control' + defaultValue={this.state.user.email} + placeholder='' + maxLength='128' + autoFocus={true} + /> {emailError} </div> </div> @@ -155,7 +168,10 @@ module.exports = React.createClass({ var signupMessage = []; if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) { signupMessage.push( - <a className='btn btn-custom-login gitlab' href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search}> + <a + className='btn btn-custom-login gitlab' + href={'/' + this.props.teamName + '/signup/gitlab' + window.location.search} + > <span className='icon' /> <span>with GitLab</span> </a> @@ -172,7 +188,13 @@ module.exports = React.createClass({ <div className='margin--extra'> <h5><strong>Choose your username</strong></h5> <div className={nameDivStyle}> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' /> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + /> {nameError} <p className='form__hint'>Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'</p> </div> @@ -180,12 +202,26 @@ module.exports = React.createClass({ <div className='margin--extra'> <h5><strong>Choose your password</strong></h5> <div className={passwordDivStyle}> - <input type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + <input + type='password' + ref='password' + className='form-control' + placeholder='' + maxLength='128' + /> {passwordError} </div> </div> </div> - <p className='margin--extra'><button type='submit' onClick={this.handleSubmit} className='btn-primary btn'>Create Account</button></p> + <p className='margin--extra'> + <button + type='submit' + onClick={this.handleSubmit} + className='btn-primary btn' + > + Create Account + </button> + </p> </div> ); } @@ -209,7 +245,10 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h5 className='margin--less'>Welcome to:</h5> <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2> <h2 className='signup-team__subdomain'>on {config.SiteName}</h2> @@ -222,6 +261,23 @@ module.exports = React.createClass({ </div> ); } -}); - +} +SignupUserComplete.defaultProps = { + teamName: '', + hash: '', + teamId: '', + email: '', + data: null, + authServices: '', + teamDisplayName: '' +}; +SignupUserComplete.propTypes = { + teamName: React.PropTypes.string, + hash: React.PropTypes.string, + teamId: React.PropTypes.string, + email: React.PropTypes.string, + data: React.PropTypes.string, + authServices: React.PropTypes.string, + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/team_export_tab.jsx b/web/react/components/team_export_tab.jsx new file mode 100644 index 000000000..2914904ad --- /dev/null +++ b/web/react/components/team_export_tab.jsx @@ -0,0 +1,94 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../utils/client.jsx'); + +export default class TeamExportTab extends React.Component { + constructor(props) { + super(props); + this.state = {status: 'request', link: '', err: ''}; + + this.onExportSuccess = this.onExportSuccess.bind(this); + this.onExportFailure = this.onExportFailure.bind(this); + this.doExport = this.doExport.bind(this); + } + onExportSuccess(data) { + this.setState({status: 'ready', link: data.link, err: ''}); + } + onExportFailure(e) { + this.setState({status: 'failure', link: '', err: e.message}); + } + doExport() { + if (this.state.status === 'in-progress') { + return; + } + this.setState({status: 'in-progress'}); + Client.exportTeam(this.onExportSuccess, this.onExportFailure); + } + render() { + var messageSection = ''; + switch (this.state.status) { + case 'request': + messageSection = ''; + break; + case 'in-progress': + messageSection = ( + <p className='confirm-import alert alert-warning'> + <i className='fa fa-spinner fa-pulse' /> + {' Exporting...'} + </p> + ); + break; + case 'ready': + messageSection = ( + <p className='confirm-import alert alert-success'> + <i className='fa fa-check' /> + {' Ready for '} + <a + href={this.state.link} + download={true} + > + {'download'} + </a> + </p> + ); + break; + case 'failure': + messageSection = ( + <p className='confirm-import alert alert-warning'> + <i className='fa fa-warning' /> + {' Unable to export: ' + this.state.err} + </p> + ); + break; + } + + return ( + <div + ref='wrapper' + className='user-settings' + > + <h3 className='tab-header'>{'Export'}</h3> + <div className='divider-dark first'/> + <ul className='section-max'> + <li className='col-xs-12 section-title'>{'Export your team'}</li> + <li className='col-xs-offset-3 col-xs-8'> + <ul className='setting-list'> + <li className='setting-list-item'> + <a + className='btn btn-sm btn-primary btn-file sel-btn' + href='#' + onClick={this.doExport} + > + {'Export'} + </a> + </li> + </ul> + </li> + </ul> + <div className='divider-dark'/> + {messageSection} + </div> + ); + } +} diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx index 4f8f0b2cf..3251746b8 100644 --- a/web/react/components/team_feature_tab.jsx +++ b/web/react/components/team_feature_tab.jsx @@ -4,65 +4,71 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -module.exports = React.createClass({ - displayName: 'Feature Tab', - propTypes: { - updateSection: React.PropTypes.func.isRequired, - team: React.PropTypes.object.isRequired, - activeSection: React.PropTypes.string.isRequired - }, - submitValetFeature: function() { +export default class FeatureTab extends React.Component { + constructor(props) { + super(props); + + this.submitValetFeature = this.submitValetFeature.bind(this); + this.handleValetRadio = this.handleValetRadio.bind(this); + this.onUpdateSection = this.onUpdateSection.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(); + } + componentWillReceiveProps(newProps) { + var team = newProps.team; + + var allowValet = 'false'; + if (team && team.allow_valet) { + allowValet = 'true'; + } + + this.setState({allowValet: allowValet}); + } + submitValetFeature() { var data = {}; data.allow_valet = this.state.allowValet; - client.updateValetFeature(data, - function() { + Client.updateValetFeature(data, + function success() { this.props.updateSection(''); AsyncClient.getMyTeam(); }.bind(this), - function(err) { - var state = this.getInitialState(); + function fail(err) { + var state = this.setupInitialState(); state.serverError = err; this.setState(state); }.bind(this) ); - }, - handleValetRadio: function(val) { + } + handleValetRadio(val) { this.setState({allowValet: val}); - this.refs.wrapper.getDOMNode().focus(); - }, - componentWillReceiveProps: function(newProps) { - var team = newProps.team; - - var allowValet = 'false'; - if (team && team.allow_valet) { - allowValet = 'true'; + React.findDOMNode(this.refs.wrapper).focus(); + } + onUpdateSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'valet') { + this.props.updateSection(''); + } else { + this.props.updateSection('valet'); } - - this.setState({allowValet: allowValet}); - }, - getInitialState: function() { + } + setupInitialState() { + var allowValet; var team = this.props.team; - var allowValet = 'false'; if (team && team.allow_valet) { allowValet = 'true'; + } else { + allowValet = 'false'; } return {allowValet: allowValet}; - }, - onUpdateSection: function(e) { - e.preventDefault(); - if (this.props.activeSection === 'valet') { - this.props.updateSection(''); - } else { - this.props.updateSection('valet'); - } - }, - render: function() { + } + render() { var clientError = null; var serverError = null; if (this.state.clientError) { @@ -73,7 +79,6 @@ module.exports = React.createClass({ } var valetSection; - var self = this; if (this.props.activeSection === 'valet') { var valetActive = [false, false]; @@ -92,7 +97,7 @@ module.exports = React.createClass({ <input type='radio' checked={valetActive[0]} - onChange={self.handleValetRadio.bind(this, 'true')} + onChange={this.handleValetRadio.bind(this, 'true')} > On </input> @@ -104,7 +109,7 @@ module.exports = React.createClass({ <input type='radio' checked={valetActive[1]} - onChange={self.handleValetRadio.bind(this, 'false')} + onChange={this.handleValetRadio.bind(this, 'false')} > Off </input> @@ -145,10 +150,25 @@ module.exports = React.createClass({ return ( <div> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'><i className='modal-back'></i>Advanced Features</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i>Advanced Features + </h4> </div> - <div ref='wrapper' className='user-settings'> + <div + ref='wrapper' + className='user-settings' + > <h3 className='tab-header'>Advanced Features</h3> <div className='divider-dark first'/> {valetSection} @@ -157,4 +177,14 @@ module.exports = React.createClass({ </div> ); } -}); +} + +FeatureTab.defaultProps = { + team: {}, + activeSection: '' +}; +FeatureTab.propTypes = { + updateSection: React.PropTypes.func.isRequired, + team: React.PropTypes.object.isRequired, + activeSection: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index fd2a22731..25139bb95 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var SettingItemMin = require('./setting_item_min.jsx'); -var SettingItemMax = require('./setting_item_max.jsx'); +const SettingItemMin = require('./setting_item_min.jsx'); +const SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const Utils = require('../utils/utils.jsx'); +import {strings} from '../utils/config.js'; export default class GeneralTab extends React.Component { constructor(props) { @@ -21,10 +22,10 @@ export default class GeneralTab extends React.Component { handleNameSubmit(e) { e.preventDefault(); - var state = {serverError: '', clientError: ''}; - var valid = true; + let state = {serverError: '', clientError: ''}; + let valid = true; - var name = this.state.name.trim(); + const name = this.state.name.trim(); if (!name) { state.clientError = 'This field is required'; valid = false; @@ -41,10 +42,10 @@ export default class GeneralTab extends React.Component { return; } - var data = {}; + let data = {}; data.new_name = name; - client.updateTeamDisplayName(data, + Client.updateTeamDisplayName(data, function nameChangeSuccess() { this.props.updateSection(''); $('#team_settings').modal('hide'); @@ -84,8 +85,8 @@ export default class GeneralTab extends React.Component { this.setState({name: e.target.value}); } render() { - var clientError = null; - var serverError = null; + let clientError = null; + let serverError = null; if (this.state.clientError) { clientError = this.state.clientError; } @@ -93,18 +94,21 @@ export default class GeneralTab extends React.Component { serverError = this.state.serverError; } - var nameSection; + let nameSection; if (this.props.activeSection === 'name') { let inputs = []; - let teamNameLabel = utils.toTitleCase(strings.Team) + ' Name'; - if (utils.isMobile()) { + let teamNameLabel = Utils.toTitleCase(strings.Team) + ' Name'; + if (Utils.isMobile()) { teamNameLabel = ''; } inputs.push( - <div key='teamNameSetting' className='form-group'> + <div + key='teamNameSetting' + className='form-group' + > <label className='col-sm-5 control-label'>{teamNameLabel}</label> <div className='col-sm-7'> <input @@ -119,7 +123,7 @@ export default class GeneralTab extends React.Component { nameSection = ( <SettingItemMax - title={utils.toTitleCase(strings.Team) + ' Name'} + title={`${Utils.toTitleCase(strings.Team)} Name`} inputs={inputs} submit={this.handleNameSubmit} server_error={serverError} @@ -132,7 +136,7 @@ export default class GeneralTab extends React.Component { nameSection = ( <SettingItemMin - title={utils.toTitleCase(strings.Team) + ' Name'} + title={`${Utils.toTitleCase(strings.Team)} Name`} describe={describe} updateSection={this.onUpdateSection} /> diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx index e3415d7f4..031abc36a 100644 --- a/web/react/components/team_import_tab.jsx +++ b/web/react/components/team_import_tab.jsx @@ -4,57 +4,87 @@ var utils = require('../utils/utils.jsx'); var SettingUpload = require('./setting_upload.jsx'); -module.exports = React.createClass({ - displayName: 'Import Tab', - getInitialState: function() { - return {status: 'ready', link: ''}; - }, - onImportFailure: function() { +export default class TeamImportTab extends React.Component { + constructor(props) { + super(props); + + this.onImportFailure = this.onImportFailure.bind(this); + this.onImportSuccess = this.onImportSuccess.bind(this); + this.doImportSlack = this.doImportSlack.bind(this); + + this.state = { + status: 'ready', + link: '' + }; + } + + onImportFailure() { this.setState({status: 'fail', link: ''}); - }, - onImportSuccess: function(data) { + } + + onImportSuccess(data) { this.setState({status: 'done', link: 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(data)}); - }, - doImportSlack: function(file) { + } + + doImportSlack(file) { this.setState({status: 'in-progress', link: ''}); utils.importSlack(file, this.onImportSuccess, this.onImportFailure); - }, - render: function() { + } + + render() { var uploadHelpText = ( <div> - <br/> - Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team's public channels. - <br/><br/> - The Slack import to Mattermost is in "Preview". Slack bot posts and channels with underscores do not yet import. - <br/><br/> + <p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p> + <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts and channels with underscores do not yet import.'}</p> </div> ); + var uploadSection = ( <SettingUpload title='Import from Slack' submit={this.doImportSlack} helpText={uploadHelpText} - fileTypesAccepted='.zip'/> + fileTypesAccepted='.zip' + /> ); var messageSection; switch (this.state.status) { - case 'ready': - messageSection = ''; + + case 'ready': + messageSection = ''; break; - case 'in-progress': - messageSection = ( - <p className="confirm-import alert alert-warning"><i className="fa fa-spinner fa-pulse"></i> Importing...</p> + case 'in-progress': + messageSection = ( + <p className='confirm-import alert alert-warning'><i className='fa fa-spinner fa-pulse'></i>{' Importing...'}</p> ); break; - case 'done': - messageSection = ( - <p className="confirm-import alert alert-success"><i className="fa fa-check"></i> Import successful: <a href={this.state.link} download='MattermostImportSummary.txt'>View Summary</a></p> - ); + case 'done': + messageSection = ( + <p className='confirm-import alert alert-success'> + <i className='fa fa-check' /> + {' Import successful: '} + <a + href={this.state.link} + download='MattermostImportSummary.txt' + > + {'View Summary'} + </a> + </p> + ); break; - case 'fail': - messageSection = ( - <p className="confirm-import alert alert-warning"><i className="fa fa-warning"></i> Import failure: <a href={this.state.link} download='MattermostImportSummary.txt'>View Summary</a></p> + case 'fail': + messageSection = ( + <p className='confirm-import alert alert-warning'> + <i className='fa fa-warning' /> + {' Import failure: '} + <a + href={this.state.link} + download='MattermostImportSummary.txt' + > + {'View Summary'} + </a> + </p> ); break; } @@ -62,11 +92,23 @@ module.exports = React.createClass({ return ( <div> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'><i className='modal-back'></i>Import</h4> + <button type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'×'}</span> + </button> + <h4 + className='modal-title' + ref='title' + ><i className='modal-back'></i>{'Import'}</h4> </div> - <div ref='wrapper' className='user-settings'> - <h3 className='tab-header'>Import</h3> + <div + ref='wrapper' + className='user-settings' + > + <h3 className='tab-header'>{'Import'}</h3> <div className='divider-dark first'/> {uploadSection} <div className='divider-dark'/> @@ -75,4 +117,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx index 616fd2c99..0cd22437e 100644 --- a/web/react/components/team_members.jsx +++ b/web/react/components/team_members.jsx @@ -2,77 +2,131 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); var MemberListTeam = require('./member_list_team.jsx'); -var Client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); function getStateFromStores() { var users = UserStore.getProfiles(); - var member_list = []; - for (var id in users) member_list.push(users[id]); + var memberList = []; + for (var id in users) { + if (users.hasOwnProperty(id)) { + memberList.push(users[id]); + } + } + + memberList.sort(function sort(a, b) { + if (a.username < b.username) { + return -1; + } + + if (a.username > b.username) { + return 1; + } - member_list.sort(function(a,b) { - if (a.username < b.username) return -1; - if (a.username > b.username) return 1; return 0; }); return { - member_list: member_list + member_list: memberList }; } -module.exports = React.createClass({ - componentDidMount: function() { - UserStore.addChangeListener(this._onChange); +export default class TeamMembers extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + + this.state = getStateFromStores(); + } + + componentDidMount() { + UserStore.addChangeListener(this.onChange); var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ render_members: false }); + $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function show() { + self.setState({render_members: false}); }); - $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { - self.setState({ render_members: true }); + $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function hide() { + self.setState({render_members: true}); }); - }, - componentWillUnmount: function() { - UserStore.removeChangeListener(this._onChange); - }, - _onChange: function() { + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + } + + onChange() { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { this.setState(newState); } - }, - getInitialState: function() { - return getStateFromStores(); - }, - render: function() { - var server_error = this.state.server_error ? <label className='has-error control-label'>{this.state.server_error}</label> : null; + } + + render() { + var serverError = null; + + if (this.state.server_error) { + serverError = <label className='has-error control-label'>{this.state.server_error}</label>; + } + + var renderMembers = ''; + + if (this.state.render_members) { + renderMembers = <MemberListTeam users={this.state.member_list} />; + } return ( - <div className="modal fade" ref="modal" id="team_members" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close" data-reactid=".5.0.0.0.0"><span aria-hidden="true" data-reactid=".5.0.0.0.0.0">×</span></button> - <h4 className="modal-title" id="myModalLabel">{this.props.teamDisplayName + " Members"}</h4> - </div> - <div ref="modalBody" className="modal-body"> - <div className="channel-settings"> - <div className="team-member-list"> - { this.state.render_members ? <MemberListTeam users={this.state.member_list} /> : "" } + <div + className='modal fade' + ref='modal' + id='team_members' + tabIndex='-1' + role='dialog' + aria-hidden='true' + > + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + id='myModalLabel' + >{this.props.teamDisplayName + ' Members'}</h4> + </div> + <div + ref='modalBody' + className='modal-body' + > + <div className='channel-settings'> + <div className='team-member-list'> + {renderMembers} + </div> + {serverError} </div> - { server_error } + </div> + <div className='modal-footer'> + <button + type='button' + className='btn btn-default' + data-dismiss='modal' + >Close</button> </div> </div> - <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> </div> </div> ); } -}); +} + +TeamMembers.propTypes = { + teamDisplayName: React.PropTypes.string +}; diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index 1a79eef1d..396521af9 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -3,68 +3,93 @@ var TeamStore = require('../stores/team_store.jsx'); var ImportTab = require('./team_import_tab.jsx'); +var ExportTab = require('./team_export_tab.jsx'); var FeatureTab = require('./team_feature_tab.jsx'); var GeneralTab = require('./team_general_tab.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); -module.exports = React.createClass({ - displayName: 'Team Settings', - propTypes: { - activeTab: React.PropTypes.string.isRequired, - activeSection: React.PropTypes.string.isRequired, - updateSection: React.PropTypes.func.isRequired, - teamDisplayName: React.PropTypes.string.isRequired - }, - componentDidMount: function() { +export default class TeamSettings extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + + this.state = {team: TeamStore.getCurrent()}; + } + componentDidMount() { TeamStore.addChangeListener(this.onChange); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { TeamStore.removeChangeListener(this.onChange); - }, - onChange: function() { + } + onChange() { var team = TeamStore.getCurrent(); - if (!utils.areStatesEqual(this.state.team, team)) { + if (!Utils.areStatesEqual(this.state.team, team)) { this.setState({team: team}); } - }, - getInitialState: function() { - return {team: TeamStore.getCurrent()}; - }, - render: function() { + } + render() { var result; switch (this.props.activeTab) { - case 'general': - result = ( - <div> - <GeneralTab - team={this.state.team} - activeSection={this.props.activeSection} - updateSection={this.props.updateSection} - teamDisplayName={this.props.teamDisplayName} - /> - </div> - ); - break; - case 'feature': - result = ( - <div> - <FeatureTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - break; - case 'import': - result = ( - <div> - <ImportTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> - </div> - ); - break; - default: - result = ( - <div/> - ); - break; + case 'general': + result = ( + <div> + <GeneralTab + team={this.state.team} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + teamDisplayName={this.props.teamDisplayName} + /> + </div> + ); + break; + case 'feature': + result = ( + <div> + <FeatureTab + team={this.state.team} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + /> + </div> + ); + break; + case 'import': + result = ( + <div> + <ImportTab + team={this.state.team} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + /> + </div> + ); + break; + case 'export': + result = ( + <div> + <ExportTab /> + </div> + ); + break; + default: + result = ( + <div/> + ); + break; } return result; } -}); +} + +TeamSettings.defaultProps = { + activeTab: '', + activeSection: '', + teamDisplayName: '' +}; +TeamSettings.propTypes = { + activeTab: React.PropTypes.string.isRequired, + activeSection: React.PropTypes.string.isRequired, + updateSection: React.PropTypes.func.isRequired, + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index 7e65e8cab..0513c811f 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -1,70 +1,97 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var SettingsSidebar = require('./settings_sidebar.jsx'); -var TeamSettings = require('./team_settings.jsx'); +const SettingsSidebar = require('./settings_sidebar.jsx'); +const TeamSettings = require('./team_settings.jsx'); -module.exports = React.createClass({ - displayName: 'Team Settings Modal', - propTypes: { - teamDisplayName: React.PropTypes.string.isRequired - }, - componentDidMount: function() { - $('body').on('click', '.modal-back', function onClick() { +export default class TeamSettingsModal extends React.Component { + constructor(props) { + super(props); + + this.updateTab = this.updateTab.bind(this); + this.updateSection = this.updateSection.bind(this); + + this.state = { + activeTab: 'general', + activeSection: '' + }; + } + componentDidMount() { + $('body').on('click', '.modal-back', function handleBackClick() { $(this).closest('.modal-dialog').removeClass('display--content'); }); - $('body').on('click', '.modal-header .close', function onClick() { + $('body').on('click', '.modal-header .close', function handleCloseClick() { setTimeout(function removeContent() { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); - }, - updateTab: function(tab) { + } + updateTab(tab) { this.setState({activeTab: tab, activeSection: ''}); - }, - updateSection: function(section) { + } + updateSection(section) { this.setState({activeSection: section}); - }, - getInitialState: function() { - return {activeTab: 'general', activeSection: ''}; - }, - render: function() { - var tabs = []; + } + render() { + let tabs = []; tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'}); tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'}); + tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'}); tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'}); return ( - <div className='modal fade' ref='modal' id='team_settings' role='dialog' tabIndex='-1' aria-hidden='true'> - <div className='modal-dialog settings-modal'> - <div className='modal-content'> - <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'>Team Settings</h4> - </div> - <div className='modal-body'> - <div className='settings-table'> - <div className='settings-links'> - <SettingsSidebar - tabs={tabs} - activeTab={this.state.activeTab} - updateTab={this.updateTab} - /> + <div + className='modal fade' + ref='modal' + id='team_settings' + role='dialog' + tabIndex='-1' + aria-hidden='true' + > + <div className='modal-dialog settings-modal'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + ref='title' + > + Team Settings + </h4> </div> - <div className='settings-content minimize-settings'> - <TeamSettings - activeTab={this.state.activeTab} - activeSection={this.state.activeSection} - updateSection={this.updateSection} - teamDisplayName={this.props.teamDisplayName} - /> + <div className='modal-body'> + <div className='settings-table'> + <div className='settings-links'> + <SettingsSidebar + tabs={tabs} + activeTab={this.state.activeTab} + updateTab={this.updateTab} + /> + </div> + <div className='settings-content minimize-settings'> + <TeamSettings + activeTab={this.state.activeTab} + activeSection={this.state.activeSection} + updateSection={this.updateSection} + teamDisplayName={this.props.teamDisplayName} + /> + </div> + </div> </div> </div> - </div> </div> - </div> </div> ); } -}); +} +TeamSettingsModal.propTypes = { + teamDisplayName: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/team_signup_allowed_domains_page.jsx b/web/react/components/team_signup_allowed_domains_page.jsx index 90c7ff668..721fa142a 100644 --- a/web/react/components/team_signup_allowed_domains_page.jsx +++ b/web/react/components/team_signup_allowed_domains_page.jsx @@ -1,31 +1,35 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); +import {strings} from '../utils/config.js'; -module.exports = React.createClass({ - displayName: 'TeamSignupAllowedDomainsPage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +export default class TeamSignupAllowedDomainsPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'team_url'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - if (this.refs.open_network.getDOMNode().checked) { + if (React.findDOMNode(this.refs.open_network).checked) { this.props.state.wizard = 'send_invites'; this.props.state.team.type = 'O'; this.props.updateParent(this.props.state); return; } - if (this.refs.allow.getDOMNode().checked) { - var name = this.refs.name.getDOMNode().value.trim(); + if (React.findDOMNode(this.refs.allow).checked) { + var name = React.findDOMNode(this.refs.name).value.trim(); var domainRegex = /^\w+\.\w+$/; if (!name) { this.setState({nameError: 'This field is required'}); @@ -46,12 +50,9 @@ module.exports = React.createClass({ this.props.state.team.type = 'I'; this.props.updateParent(this.props.state); } - }, - getInitialState: function() { - return {}; - }, - render: function() { - client.track('signup', 'signup_team_04_allow_domains'); + } + render() { + Client.track('signup', 'signup_team_04_allow_domains'); var nameError = null; var nameDivClass = 'form-group'; @@ -63,11 +64,21 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h2>Email Domain</h2> <p> <div className='checkbox'> - <label><input type='checkbox' ref='allow' defaultChecked={true} />{' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'}</label> + <label> + <input + type='checkbox' + ref='allow' + defaultChecked={true} + /> + {' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'} + </label> </div> </p> <p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p> @@ -77,7 +88,16 @@ module.exports = React.createClass({ <div className='col-sm-9'> <div className='input-group'> <span className='input-group-addon'>@</span> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.allowed_domains} autoFocus={true} onFocus={this.handleFocus}/> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + defaultValue={this.props.state.team.allowed_domains} + autoFocus={true} + onFocus={this.handleFocus} + /> </div> </div> </div> @@ -86,13 +106,38 @@ module.exports = React.createClass({ <p>To allow signups from multiple domains, separate each with a comma.</p> <p> <div className='checkbox'> - <label><input type='checkbox' ref='open_network' defaultChecked={this.props.state.team.type === 'O'} /> Allow anyone to signup to this domain without an invitation.</label> + <label> + <input + type='checkbox' + ref='open_network' + defaultChecked={this.props.state.team.type === 'O'} + /> Allow anyone to signup to this domain without an invitation.</label> </div> </p> - <button type='button' className='btn btn-default' onClick={this.submitBack}><i className='glyphicon glyphicon-chevron-left'></i> Back</button> - <button type='submit' className='btn-primary btn' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <button + type='button' + className='btn btn-default' + onClick={this.submitBack} + > + <i className='glyphicon glyphicon-chevron-left'></i> Back + </button> + <button + type='submit' + className='btn-primary btn' + onClick={this.submitNext} + > + Next<i className='glyphicon glyphicon-chevron-right'></i> + </button> </form> </div> ); } -}); +} + +TeamSignupAllowedDomainsPage.defaultProps = { + state: {} +}; +TeamSignupAllowedDomainsPage.propTypes = { + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index 92ade5d24..acce6ab49 100644 --- a/web/react/components/team_signup_choose_auth.jsx +++ b/web/react/components/team_signup_choose_auth.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var Constants = require('../utils/constants.jsx'); +import {strings} from '../utils/config.js'; export default class ChooseAuthPage extends React.Component { constructor(props) { @@ -21,7 +22,7 @@ export default class ChooseAuthPage extends React.Component { this.props.updatePage('service', Constants.GITLAB_SERVICE); }.bind(this) } - > + > <span className='icon' /> <span>Create new {strings.Team} with GitLab Account</span> </a> @@ -39,7 +40,7 @@ export default class ChooseAuthPage extends React.Component { this.props.updatePage('email', ''); }.bind(this) } - > + > <span className='fa fa-envelope' /> <span>Create new {strings.Team} with email address</span> </a> diff --git a/web/react/components/team_signup_display_name_page.jsx b/web/react/components/team_signup_display_name_page.jsx index b5e93de1b..1849f8222 100644 --- a/web/react/components/team_signup_display_name_page.jsx +++ b/web/react/components/team_signup_display_name_page.jsx @@ -3,22 +3,26 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); +import {strings} from '../utils/config.js'; -module.exports = React.createClass({ - displayName: 'TeamSignupDisplayNamePage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +export default class TeamSignupDisplayNamePage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'welcome'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - var displayName = this.refs.name.getDOMNode().value.trim(); + var displayName = React.findDOMNode(this.refs.name).value.trim(); if (!displayName) { this.setState({nameError: 'This field is required'}); return; @@ -28,15 +32,12 @@ module.exports = React.createClass({ this.props.state.team.display_name = displayName; this.props.state.team.name = utils.cleanUpUrlable(displayName); this.props.updateParent(this.props.state); - }, - getInitialState: function() { - return {}; - }, - handleFocus: function(e) { + } + handleFocus(e) { e.preventDefault(); e.currentTarget.select(); - }, - render: function() { + } + render() { client.track('signup', 'signup_team_02_name'); var nameError = null; @@ -49,24 +50,53 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> - + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h2>{utils.toTitleCase(strings.Team) + ' Name'}</h2> <div className={nameDivClass}> <div className='row'> <div className='col-sm-9'> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.display_name} autoFocus={true} onFocus={this.handleFocus} /> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + defaultValue={this.props.state.team.display_name} + autoFocus={true} + onFocus={this.handleFocus} + /> </div> </div> {nameError} </div> - <div>{'Name your ' + strings.Team + ' in any language. Your ' + strings.Team + ' name shows in menus and headings.'}</div> - <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <div> + {'Name your ' + strings.Team + ' in any language. Your ' + strings.Team + ' name shows in menus and headings.'} + </div> + <button + type='submit' + className='btn btn-primary margin--extra' + onClick={this.submitNext} + > + Next<i className='glyphicon glyphicon-chevron-right'></i> + </button> <div className='margin--extra'> - <a href='#' onClick={this.submitBack}>Back to previous step</a> + <a + href='#' + onClick={this.submitBack} + > + Back to previous step + </a> </div> </form> </div> ); } -}); +} + +TeamSignupDisplayNamePage.propTypes = { + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_email_item.jsx b/web/react/components/team_signup_email_item.jsx index 11cd17e74..10bb2d69e 100644 --- a/web/react/components/team_signup_email_item.jsx +++ b/web/react/components/team_signup_email_item.jsx @@ -1,28 +1,28 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); - -module.exports = React.createClass({ - displayName: 'TeamSignupEmailItem', - propTypes: { - focus: React.PropTypes.bool, - email: React.PropTypes.string - }, - getInitialState: function() { - return {}; - }, - getValue: function() { - return this.refs.email.getDOMNode().value.trim(); - }, - validate: function(teamEmail) { - var email = this.refs.email.getDOMNode().value.trim().toLowerCase(); +const Utils = require('../utils/utils.jsx'); + +export default class TeamSignupEmailItem extends React.Component { + constructor(props) { + super(props); + + this.getValue = this.getValue.bind(this); + this.validate = this.validate.bind(this); + + this.state = {}; + } + getValue() { + return React.findDOMNode(this.refs.email).value.trim(); + } + validate(teamEmail) { + const email = React.findDOMNode(this.refs.email).value.trim().toLowerCase(); if (!email) { return true; } - if (!utils.isEmail(email)) { + if (!Utils.isEmail(email)) { this.state.emailError = 'Please enter a valid email address'; this.setState(this.state); return false; @@ -31,13 +31,14 @@ module.exports = React.createClass({ this.setState(this.state); return false; } + this.state.emailError = ''; this.setState(this.state); return true; - }, - render: function() { - var emailError = null; - var emailDivClass = 'form-group'; + } + render() { + let emailError = null; + let emailDivClass = 'form-group'; if (this.state.emailError) { emailError = <label className='control-label'>{this.state.emailError}</label>; emailDivClass += ' has-error'; @@ -45,9 +46,22 @@ module.exports = React.createClass({ return ( <div className={emailDivClass}> - <input autoFocus={this.props.focus} type='email' ref='email' className='form-control' placeholder='Email Address' defaultValue={this.props.email} maxLength='128' /> + <input + autoFocus={this.props.focus} + type='email' + ref='email' + className='form-control' + placeholder='Email Address' + defaultValue={this.props.email} + maxLength='128' + /> {emailError} </div> ); } -}); +} + +TeamSignupEmailItem.propTypes = { + focus: React.PropTypes.bool, + email: React.PropTypes.string +}; diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index 18cf05dad..aa402846b 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -1,24 +1,29 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); +var BrowserStore = require('../stores/browser_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); +import {strings, config} from '../utils/config.js'; -module.exports = React.createClass({ - displayName: 'TeamSignupPasswordPage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +export default class TeamSignupPasswordPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'username'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - var password = this.refs.password.getDOMNode().value.trim(); + var password = React.findDOMNode(this.refs.password).value.trim(); if (!password || password.length < 5) { this.setState({passwordError: 'Please enter at least 5 characters'}); return; @@ -31,15 +36,14 @@ module.exports = React.createClass({ teamSignup.user.allow_marketing = true; delete teamSignup.wizard; - client.createTeamFromSignup(teamSignup, + Client.createTeamFromSignup(teamSignup, function success() { - client.track('signup', 'signup_team_08_complete'); + Client.track('signup', 'signup_team_08_complete'); var props = this.props; - - client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, - function(data) { + Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, + function loginSuccess(data) { UserStore.setLastEmail(teamSignup.team.email); UserStore.setCurrentUser(data); if (this.props.hash > 0) { @@ -52,7 +56,7 @@ module.exports = React.createClass({ window.location.href = '/'; }.bind(this), - function(err) { + function loginFail(err) { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); } else { @@ -67,12 +71,9 @@ module.exports = React.createClass({ $('#finish-button').button('reset'); }.bind(this) ); - }, - getInitialState: function() { - return {}; - }, - render: function() { - client.track('signup', 'signup_team_07_password'); + } + render() { + Client.track('signup', 'signup_team_07_password'); var passwordError = null; var passwordDivStyle = 'form-group'; @@ -89,7 +90,10 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h2 className='margin--less'>Your password</h2> <h5 className='color--light'>Select a password that you'll use to login with your email address:</h5> <div className='inner__content margin--extra'> @@ -99,7 +103,14 @@ module.exports = React.createClass({ <div className='row'> <div className='col-sm-11'> <h5><strong>Choose your password</strong></h5> - <input autoFocus={true} type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> + <input + autoFocus={true} + type='password' + ref='password' + className='form-control' + placeholder='' + maxLength='128' + /> <div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div> </div> </div> @@ -108,14 +119,37 @@ module.exports = React.createClass({ </div> </div> <div className='form-group'> - <button type='submit' className='btn btn-primary margin--extra' id='finish-button' data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} onClick={this.submitNext}>Finish</button> + <button + type='submit' + className='btn btn-primary margin--extra' + id='finish-button' + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} + onClick={this.submitNext} + > + Finish + </button> </div> <p>By proceeding to create your account and use {config.SiteName}, you agree to our <a href={config.TermsLink}>Terms of Service</a> and <a href={config.PrivacyLink}>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> <div className='margin--extra'> - <a href='#' onClick={this.submitBack}>Back to previous step</a> + <a + href='#' + onClick={this.submitBack} + > + Back to previous step + </a> </div> </form> </div> ); } -}); +} + +TeamSignupPasswordPage.defaultProps = { + state: {}, + hash: '' +}; +TeamSignupPasswordPage.propTypes = { + state: React.PropTypes.object, + hash: React.PropTypes.string, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index 646a742ba..11a9980d7 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -2,9 +2,10 @@ // See License.txt for license information. var EmailItem = require('./team_signup_email_item.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var ConfigStore = require('../stores/config_store.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); +import {strings, config} from '../utils/config.js'; export default class TeamSignupSendInvitesPage extends React.Component { constructor(props) { @@ -17,6 +18,11 @@ export default class TeamSignupSendInvitesPage extends React.Component { this.state = { emailEnabled: !ConfigStore.getSettingAsBoolean('ByPassEmail', false) }; + + if (!this.state.emailEnabled) { + this.props.state.wizard = 'username'; + this.props.updateParent(this.props.state); + } } submitBack(e) { e.preventDefault(); @@ -71,13 +77,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { } keySubmit(e) { if (e && e.keyCode === 13) { - this.submitNext(e) - } - } - componentWillMount() { - if (!this.state.emailEnabled) { - this.props.state.wizard = 'username'; - this.props.updateParent(this.props.state); + this.submitNext(e); } } componentDidMount() { @@ -92,7 +92,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { } } render() { - client.track('signup', 'signup_team_05_send_invites'); + Client.track('signup', 'signup_team_05_send_invites'); var content = null; var bottomContent = null; @@ -165,7 +165,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { className='signup-team-logo' src='/static/images/logo.png' /> - <h2>{'Invite ' + utils.toTitleCase(strings.Team) + ' Members'}</h2> + <h2>{'Invite ' + Utils.toTitleCase(strings.Team) + ' Members'}</h2> {content} <div className='form-group'> <button @@ -190,6 +190,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { ); } } + TeamSignupSendInvitesPage.propTypes = { state: React.PropTypes.object.isRequired, updateParent: React.PropTypes.func.isRequired diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx index beef725e2..ffe9d9fe8 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -1,33 +1,38 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var constants = require('../utils/constants.jsx'); - -module.exports = React.createClass({ - displayName: 'TeamSignupURLPage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +const Utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const Constants = require('../utils/constants.jsx'); +import {strings, config} from '../utils/config.js'; + +export default class TeamSignupUrlPage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + this.handleFocus = this.handleFocus.bind(this); + + this.state = {nameError: ''}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'team_display_name'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - var name = this.refs.name.getDOMNode().value.trim(); + const name = React.findDOMNode(this.refs.name).value.trim(); if (!name) { this.setState({nameError: 'This field is required'}); return; } - var cleanedName = utils.cleanUpUrlable(name); + const cleanedName = Utils.cleanUpUrlable(name); - var urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g; + const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g; if (cleanedName !== name || !urlRegex.test(name)) { this.setState({nameError: "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."}); return; @@ -36,14 +41,14 @@ module.exports = React.createClass({ return; } - for (var index = 0; index < constants.RESERVED_TEAM_NAMES.length; index++) { - if (cleanedName.indexOf(constants.RESERVED_TEAM_NAMES[index]) === 0) { - this.setState({nameError: 'This team name is unavailable'}); + for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) { + if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) { + this.setState({nameError: 'URL is taken or contains a reserved word'}); return; } } - client.findTeamByName(name, + Client.findTeamByName(name, function success(data) { if (!data) { if (config.AllowSignupDomainsWizard) { @@ -65,55 +70,88 @@ module.exports = React.createClass({ this.setState(this.state); }.bind(this) ); - }, - getInitialState: function() { - return {}; - }, - handleFocus: function(e) { + } + handleFocus(e) { e.preventDefault(); e.currentTarget.select(); - }, - render: function() { + } + render() { $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); - client.track('signup', 'signup_team_03_url'); + Client.track('signup', 'signup_team_03_url'); - var nameError = null; - var nameDivClass = 'form-group'; + let nameError = null; + let nameDivClass = 'form-group'; if (this.state.nameError) { nameError = <label className='control-label'>{this.state.nameError}</label>; nameDivClass += ' has-error'; } + const title = `${Utils.getWindowLocationOrigin()}/`; + return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> - <h2>{utils.toTitleCase(strings.Team) + ' URL'}</h2> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> + <h2>{`${Utils.toTitleCase(strings.Team)} URL`}</h2> <div className={nameDivClass}> <div className='row'> <div className='col-sm-11'> <div className='input-group input-group--limit'> - <span data-toggle='tooltip' title={utils.getWindowLocationOrigin() + '/'} className='input-group-addon'>{utils.getWindowLocationOrigin() + '/'}</span> - <input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/> + <span + data-toggle='tooltip' + title={title} + className='input-group-addon' + > + {title} + </span> + <input + type='text' + ref='name' + className='form-control' + placeholder='' + maxLength='128' + defaultValue={this.props.state.team.name} + autoFocus={true} + onFocus={this.handleFocus} + /> </div> </div> </div> {nameError} </div> - <p>{'Choose the web address of your new ' + strings.Team + ':'}</p> + <p>{`Choose the web address of your new ${strings.Team}:`}</p> <ul className='color--light'> <li>Short and memorable is best</li> <li>Use lowercase letters, numbers and dashes</li> <li>Must start with a letter and can't end in a dash</li> </ul> - <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <button + type='submit' + className='btn btn-primary margin--extra' + onClick={this.submitNext} + > + Next<i className='glyphicon glyphicon-chevron-right'></i> + </button> <div className='margin--extra'> - <a href='#' onClick={this.submitBack}>Back to previous step</a> + <a + href='#' + onClick={this.submitBack} + > + Back to previous step + </a> </div> </form> </div> ); } -}); +} + +TeamSignupUrlPage.propTypes = { + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx index 56882e6a1..b5c8b14df 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/team_signup_username_page.jsx @@ -1,26 +1,30 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +var Utils = require('../utils/utils.jsx'); +var Client = require('../utils/client.jsx'); +import {strings} from '../utils/config.js'; -module.exports = React.createClass({ - displayName: 'TeamSignupUsernamePage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitBack: function(e) { +export default class TeamSignupUsernamePage extends React.Component { + constructor(props) { + super(props); + + this.submitBack = this.submitBack.bind(this); + this.submitNext = this.submitNext.bind(this); + + this.state = {}; + } + submitBack(e) { e.preventDefault(); this.props.state.wizard = 'send_invites'; this.props.updateParent(this.props.state); - }, - submitNext: function(e) { + } + submitNext(e) { e.preventDefault(); - var name = this.refs.name.getDOMNode().value.trim(); + var name = React.findDOMNode(this.refs.name).value.trim(); - var usernameError = utils.isValidUsername(name); + var usernameError = Utils.isValidUsername(name); if (usernameError === 'Cannot use a reserved word as a username.') { this.setState({nameError: 'This username is reserved, please choose a new one.'}); return; @@ -32,12 +36,9 @@ module.exports = React.createClass({ this.props.state.wizard = 'password'; this.props.state.user.username = name; this.props.updateParent(this.props.state); - }, - getInitialState: function() { - return {}; - }, - render: function() { - client.track('signup', 'signup_team_06_username'); + } + render() { + Client.track('signup', 'signup_team_06_username'); var nameError = null; var nameDivClass = 'form-group'; @@ -49,7 +50,10 @@ module.exports = React.createClass({ return ( <div> <form> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h2 className='margin--less'>Your username</h2> <h5 className='color--light'>{'Select a memorable username that makes it easy for ' + strings.Team + 'mates to identify you:'}</h5> <div className='inner__content margin--extra'> @@ -57,19 +61,47 @@ module.exports = React.createClass({ <div className='row'> <div className='col-sm-11'> <h5><strong>Choose your username</strong></h5> - <input autoFocus={true} type='text' ref='name' className='form-control' placeholder='' defaultValue={this.props.state.user.username} maxLength='128' /> + <input + autoFocus={true} + type='text' + ref='name' + className='form-control' + placeholder='' + defaultValue={this.props.state.user.username} + maxLength='128' + /> <div className='color--light form__hint'>Usernames must begin with a letter and contain 3 to 15 characters made up of lowercase letters, numbers, and the symbols '.', '-' and '_'</div> </div> </div> {nameError} </div> </div> - <button type='submit' className='btn btn-primary margin--extra' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> + <button + type='submit' + className='btn btn-primary margin--extra' + onClick={this.submitNext} + > + Next + <i className='glyphicon glyphicon-chevron-right'></i> + </button> <div className='margin--extra'> - <a href='#' onClick={this.submitBack}>Back to previous step</a> + <a + href='#' + onClick={this.submitBack} + > + Back to previous step + </a> </div> </form> </div> ); } -}); +} + +TeamSignupUsernamePage.defaultProps = { + state: null +}; +TeamSignupUsernamePage.propTypes = { + state: React.PropTypes.object, + updateParent: React.PropTypes.func +}; diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index f0c680bd8..43b7aea0e 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -1,17 +1,25 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +var Utils = require('../utils/utils.jsx'); +var Client = require('../utils/client.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); +import {config} from '../utils/config.js'; -module.exports = React.createClass({ - displayName: 'TeamSignupWelcomePage', - propTypes: { - state: React.PropTypes.object, - updateParent: React.PropTypes.func - }, - submitNext: function(e) { +export default class TeamSignupWelcomePage extends React.Component { + constructor(props) { + super(props); + + this.submitNext = this.submitNext.bind(this); + this.handleDiffEmail = this.handleDiffEmail.bind(this); + this.handleDiffSubmit = this.handleDiffSubmit.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + + this.state = {useDiff: false}; + + document.addEventListener('keyup', this.handleKeyPress, false); + } + submitNext(e) { if (!BrowserStore.isLocalStorageSupported()) { this.setState({storageError: 'This service requires local storage to be enabled. Please enable it or exit private browsing.'}); return; @@ -19,18 +27,18 @@ module.exports = React.createClass({ e.preventDefault(); this.props.state.wizard = 'team_display_name'; this.props.updateParent(this.props.state); - }, - handleDiffEmail: function(e) { + } + handleDiffEmail(e) { e.preventDefault(); this.setState({useDiff: true}); - }, - handleDiffSubmit: function(e) { + } + handleDiffSubmit(e) { e.preventDefault(); var state = {useDiff: true, serverError: ''}; - var email = this.refs.email.getDOMNode().value.trim().toLowerCase(); - if (!email || !utils.isEmail(email)) { + var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase(); + if (!email || !Utils.isEmail(email)) { state.emailError = 'Please enter a valid email address'; this.setState(state); return; @@ -41,7 +49,7 @@ module.exports = React.createClass({ } state.emailError = ''; - client.signupTeam(email, + Client.signupTeam(email, function success(data) { if (data.follow_link) { window.location.href = data.follow_link; @@ -56,23 +64,17 @@ module.exports = React.createClass({ this.setState(this.state); }.bind(this) ); - }, - getInitialState: function() { - return {useDiff: false}; - }, - handleKeyPress: function(event) { + } + handleKeyPress(event) { if (event.keyCode === 13) { this.submitNext(event); } - }, - componentWillMount: function() { - document.addEventListener('keyup', this.handleKeyPress, false); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { document.removeEventListener('keyup', this.handleKeyPress, false); - }, - render: function() { - client.track('signup', 'signup_team_01_welcome'); + } + render() { + Client.track('signup', 'signup_team_01_welcome'); var storageError = null; if (this.state.storageError) { @@ -105,7 +107,10 @@ module.exports = React.createClass({ return ( <div> <p> - <img className='signup-team-logo' src='/static/images/logo.png' /> + <img + className='signup-team-logo' + src='/static/images/logo.png' + /> <h3 className='sub-heading'>Welcome to:</h3> <h1 className='margin--top-none'>{config.SiteName}</h1> </p> @@ -121,7 +126,14 @@ module.exports = React.createClass({ You can add other administrators later. </p> <div className='form-group'> - <button className='btn-primary btn form-group' type='submit' onClick={this.submitNext}><i className='glyphicon glyphicon-ok'></i>Yes, this address is correct</button> + <button + className='btn-primary btn form-group' + type='submit' + onClick={this.submitNext} + > + <i className='glyphicon glyphicon-ok'></i> + Yes, this address is correct + </button> {storageError} </div> <hr /> @@ -129,16 +141,42 @@ module.exports = React.createClass({ <div className={emailDivClass}> <div className='row'> <div className='col-sm-9'> - <input type='email' ref='email' className='form-control' placeholder='Email Address' maxLength='128' /> + <input + type='email' + ref='email' + className='form-control' + placeholder='Email Address' + maxLength='128' + /> </div> </div> {emailError} </div> {serverError} - <button className='btn btn-md btn-primary' type='button' onClick={this.handleDiffSubmit}>Use this instead</button> + <button + className='btn btn-md btn-primary' + type='button' + onClick={this.handleDiffSubmit} + > + Use this instead + </button> </div> - <a href='#' onClick={this.handleDiffEmail} className={differentEmailLinkClass}>Use a different email</a> + <a + href='#' + onClick={this.handleDiffEmail} + className={differentEmailLinkClass} + > + Use a different email + </a> </div> ); } -}); +} + +TeamSignupWelcomePage.defaultProps = { + state: {} +}; +TeamSignupWelcomePage.propTypes = { + updateParent: React.PropTypes.func.isRequired, + state: React.PropTypes.object +}; diff --git a/web/react/components/team_signup_with_email.jsx b/web/react/components/team_signup_with_email.jsx index c7204880f..d75736bd3 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -1,8 +1,9 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); +const Utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +import {strings} from '../utils/config.js'; export default class EmailSignUpPage extends React.Component { constructor() { @@ -14,11 +15,11 @@ export default class EmailSignUpPage extends React.Component { } handleSubmit(e) { e.preventDefault(); - var team = {}; - var state = {serverError: ''}; + let team = {}; + let state = {serverError: ''}; - team.email = this.refs.email.getDOMNode().value.trim().toLowerCase(); - if (!team.email || !utils.isEmail(team.email)) { + team.email = React.findDOMNode(this.refs.email).value.trim().toLowerCase(); + if (!team.email || !Utils.isEmail(team.email)) { state.emailError = 'Please enter a valid email address'; state.inValid = true; } else { @@ -30,12 +31,12 @@ export default class EmailSignUpPage extends React.Component { return; } - client.signupTeam(team.email, + Client.signupTeam(team.email, function success(data) { if (data.follow_link) { window.location.href = data.follow_link; } else { - window.location.href = '/signup_team_confirm/?email=' + encodeURIComponent(team.email); + window.location.href = `/signup_team_confirm/?email=${encodeURIComponent(team.email)}`; } }, function fail(err) { @@ -69,7 +70,7 @@ export default class EmailSignUpPage extends React.Component { </button> </div> <div className='form-group margin--extra-2x'> - <span><a href='/find_team'>{'Find my ' + strings.Team}</a></span> + <span><a href='/find_team'>{`Find my ${strings.Team}`}</a></span> </div> </form> ); diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx index 6cb62efc7..521c21733 100644 --- a/web/react/components/team_signup_with_sso.jsx +++ b/web/react/components/team_signup_with_sso.jsx @@ -4,6 +4,7 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); +import {strings} from '../utils/config.js'; export default class SSOSignUpPage extends React.Component { constructor(props) { @@ -52,7 +53,7 @@ export default class SSOSignUpPage extends React.Component { ); } nameChange() { - this.setState({name: this.refs.teamname.getDOMNode().value.trim()}); + this.setState({name: React.findDOMNode(this.refs.teamname).value.trim()}); } render() { var nameError = null; diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index efd2dd810..ea8126bec 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -1,66 +1,93 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var PostStore = require('../stores/post_store.jsx'); -var CommandList = require('./command_list.jsx'); -var ErrorStore = require('../stores/error_store.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); - -var utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const PostStore = require('../stores/post_store.jsx'); +const CommandList = require('./command_list.jsx'); +const ErrorStore = require('../stores/error_store.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); + +const Utils = require('../utils/utils.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; + +export default class Textbox extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + this.onRecievedError = this.onRecievedError.bind(this); + this.onTimerInterrupt = this.onTimerInterrupt.bind(this); + this.updateMentionTab = this.updateMentionTab.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleBackspace = this.handleBackspace.bind(this); + this.checkForNewMention = this.checkForNewMention.bind(this); + this.addMention = this.addMention.bind(this); + this.addCommand = this.addCommand.bind(this); + this.resize = this.resize.bind(this); + this.handleFocus = this.handleFocus.bind(this); + this.handleBlur = this.handleBlur.bind(this); + this.handlePaste = this.handlePaste.bind(this); + + this.state = { + mentionText: '-1', + mentions: [], + connection: '', + timerInterrupt: null + }; + + this.caret = -1; + this.addedMention = false; + this.doProcessMentions = false; + this.mentions = []; + } + getStateFromStores() { + const error = ErrorStore.getLastError(); -function getStateFromStores() { - var error = ErrorStore.getLastError(); + if (error) { + return {message: error.message}; + } - if (error) { - return {message: error.message}; + return {message: null}; } - return {message: null}; -} - -module.exports = React.createClass({ - displayName: 'Textbox', - caret: -1, - addedMention: false, - doProcessMentions: false, - mentions: [], - componentDidMount: function() { + componentDidMount() { PostStore.addAddMentionListener(this.onListenerChange); ErrorStore.addChangeListener(this.onRecievedError); this.resize(); this.updateMentionTab(null); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { PostStore.removeAddMentionListener(this.onListenerChange); ErrorStore.removeChangeListener(this.onRecievedError); - }, - onListenerChange: function(id, username) { + } + onListenerChange(id, username) { if (id === this.props.id) { this.addMention(username); } - }, - onRecievedError: function() { - var errorState = getStateFromStores(); + } + onRecievedError() { + const errorState = this.getStateFromStores(); - if (this.state.timerInterrupt != null) { + if (this.state.timerInterrupt !== null) { window.clearInterval(this.state.timerInterrupt); this.setState({timerInterrupt: null}); } if (errorState.message === 'There appears to be a problem with your internet connection') { this.setState({connection: 'bad-connection'}); - var timerInterrupt = window.setInterval(this.onTimerInterrupt, 5000); + const timerInterrupt = window.setInterval(this.onTimerInterrupt, 5000); this.setState({timerInterrupt: timerInterrupt}); } else { this.setState({connection: ''}); } - }, - onTimerInterrupt: function() { - //Since these should only happen when you have no connection and slightly briefly after any - //performance hit should not matter + } + onTimerInterrupt() { + // Since these should only happen when you have no connection and slightly briefly after any + // performance hit should not matter if (this.state.connection === 'bad-connection') { AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_ERROR, @@ -72,10 +99,10 @@ module.exports = React.createClass({ window.clearInterval(this.state.timerInterrupt); this.setState({timerInterrupt: null}); - }, - componentDidUpdate: function() { + } + componentDidUpdate() { if (this.caret >= 0) { - utils.setCaretPosition(this.refs.message.getDOMNode(), this.caret); + Utils.setCaretPosition(React.findDOMNode(this.refs.message), this.caret); this.caret = -1; } if (this.doProcessMentions) { @@ -83,40 +110,33 @@ module.exports = React.createClass({ this.doProcessMentions = false; } this.resize(); - }, - componentWillReceiveProps: function(nextProps) { + } + componentWillReceiveProps(nextProps) { if (!this.addedMention) { this.checkForNewMention(nextProps.messageText); } - var text = this.refs.message.getDOMNode().value; + const text = React.findDOMNode(this.refs.message).value; if (nextProps.channelId !== this.props.channelId || nextProps.messageText !== text) { this.doProcessMentions = true; } this.addedMention = false; this.refs.commands.getSuggestedCommands(nextProps.messageText); - this.resize(); - }, - getInitialState: function() { - return {mentionText: '-1', mentions: [], connection: '', timerInterrupt: null}; - }, - updateMentionTab: function(mentionText) { - var self = this; - + } + updateMentionTab(mentionText) { // using setTimeout so dispatch isn't called during an in progress dispatch - setTimeout(function() { + setTimeout(function updateMentionTabAfterTimeout() { AppDispatcher.handleViewAction({ type: ActionTypes.RECIEVED_MENTION_DATA, - id: self.props.id, + id: this.props.id, mention_text: mentionText }); - }, 1); - }, - handleChange: function() { - this.props.onUserInput(this.refs.message.getDOMNode().value); - this.resize(); - }, - handleKeyPress: function(e) { - var text = this.refs.message.getDOMNode().value; + }.bind(this), 1); + } + handleChange() { + this.props.onUserInput(React.findDOMNode(this.refs.message).value); + } + handleKeyPress(e) { + const text = React.findDOMNode(this.refs.message).value; if (!this.refs.commands.isEmpty() && text.indexOf('/') === 0 && e.which === 13) { this.refs.commands.addFirstCommand(); @@ -125,10 +145,10 @@ module.exports = React.createClass({ } if (!this.doProcessMentions) { - var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); - var preText = text.substring(0, caret); - var lastSpace = preText.lastIndexOf(' '); - var lastAt = preText.lastIndexOf('@'); + const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message)); + const preText = text.substring(0, caret); + const lastSpace = preText.lastIndexOf(' '); + const lastAt = preText.lastIndexOf('@'); if (caret > lastAt && lastSpace < lastAt) { this.doProcessMentions = true; @@ -136,18 +156,18 @@ module.exports = React.createClass({ } this.props.onKeyPress(e); - }, - handleKeyDown: function(e) { - if (utils.getSelectedText(this.refs.message.getDOMNode()) !== '') { + } + handleKeyDown(e) { + if (Utils.getSelectedText(React.findDOMNode(this.refs.message)) !== '') { this.doProcessMentions = true; } if (e.keyCode === 8) { this.handleBackspace(e); } - }, - handleBackspace: function() { - var text = this.refs.message.getDOMNode().value; + } + handleBackspace() { + const text = React.findDOMNode(this.refs.message).value; if (text.indexOf('/') === 0) { this.refs.commands.getSuggestedCommands(text.substring(0, text.length - 1)); } @@ -156,21 +176,21 @@ module.exports = React.createClass({ return; } - var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); - var preText = text.substring(0, caret); - var lastSpace = preText.lastIndexOf(' '); - var lastAt = preText.lastIndexOf('@'); + const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message)); + const preText = text.substring(0, caret); + const lastSpace = preText.lastIndexOf(' '); + const lastAt = preText.lastIndexOf('@'); if (caret > lastAt && (lastSpace > lastAt || lastSpace === -1)) { this.doProcessMentions = true; } - }, - checkForNewMention: function(text) { - var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); + } + checkForNewMention(text) { + const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message)); - var preText = text.substring(0, caret); + const preText = text.substring(0, caret); - var atIndex = preText.lastIndexOf('@'); + const atIndex = preText.lastIndexOf('@'); // The @ character not typed, so nothing to do. if (atIndex === -1) { @@ -178,8 +198,8 @@ module.exports = React.createClass({ return; } - var lastCharSpace = preText.lastIndexOf(String.fromCharCode(160)); - var lastSpace = preText.lastIndexOf(' '); + const lastCharSpace = preText.lastIndexOf(String.fromCharCode(160)); + const lastSpace = preText.lastIndexOf(' '); // If there is a space after the last @, nothing to do. if (lastSpace > atIndex || lastCharSpace > atIndex) { @@ -188,43 +208,45 @@ module.exports = React.createClass({ } // Get the name typed so far. - var name = preText.substring(atIndex + 1, preText.length).toLowerCase(); + const name = preText.substring(atIndex + 1, preText.length).toLowerCase(); this.updateMentionTab(name); - }, - addMention: function(name) { - var caret = utils.getCaretPosition(this.refs.message.getDOMNode()); + } + addMention(name) { + const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message)); - var text = this.props.messageText; + const text = this.props.messageText; - var preText = text.substring(0, caret); + const preText = text.substring(0, caret); - var atIndex = preText.lastIndexOf('@'); + const atIndex = preText.lastIndexOf('@'); // The @ character not typed, so nothing to do. if (atIndex === -1) { return; } - var prefix = text.substring(0, atIndex); - var suffix = text.substring(caret, text.length); + const prefix = text.substring(0, atIndex); + const suffix = text.substring(caret, text.length); this.caret = prefix.length + name.length + 2; this.addedMention = true; this.doProcessMentions = true; - this.props.onUserInput(prefix + '@' + name + ' ' + suffix); - }, - addCommand: function(cmd) { - var elm = this.refs.message.getDOMNode(); + this.props.onUserInput(`${prefix}@${name} ${suffix}`); + } + addCommand(cmd) { + const elm = React.findDOMNode(this.refs.message); elm.value = cmd; this.handleChange(); - }, - resize: function() { - var e = this.refs.message.getDOMNode(); - var w = this.refs.wrapper.getDOMNode(); + } + resize() { + const e = React.findDOMNode(this.refs.message); + const w = React.findDOMNode(this.refs.wrapper); + + const prevHeight = $(e).height(); - var lht = parseInt($(e).css('lineHeight'), 10); - var lines = e.scrollHeight / lht; - var mod = 15; + const lht = parseInt($(e).css('lineHeight'), 10); + const lines = e.scrollHeight / lht; + let mod = 15; if (lines < 2.5 || this.props.messageText === '') { mod = 30; @@ -237,28 +259,67 @@ module.exports = React.createClass({ $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167); $(w).css({height: 'auto'}).height(167); } - }, - handleFocus: function() { - var elm = this.refs.message.getDOMNode(); + + if (prevHeight !== $(e).height() && this.props.onHeightChange) { + this.props.onHeightChange(); + } + } + handleFocus() { + const elm = React.findDOMNode(this.refs.message); if (elm.title === elm.value) { elm.value = ''; } - }, - handleBlur: function() { - var elm = this.refs.message.getDOMNode(); + } + handleBlur() { + const elm = React.findDOMNode(this.refs.message); if (elm.value === '') { elm.value = elm.title; } - }, - handlePaste: function() { + } + handlePaste() { this.doProcessMentions = true; - }, - render: function() { + } + render() { return ( - <div ref='wrapper' className='textarea-wrapper'> - <CommandList ref='commands' addCommand={this.addCommand} channelId={this.props.channelId} /> - <textarea id={this.props.id} ref='message' className={'form-control custom-textarea ' + this.state.connection} spellCheck='true' autoComplete='off' autoCorrect='off' rows='1' maxLength={Constants.MAX_POST_LEN} placeholder={this.props.createMessage} value={this.props.messageText} onInput={this.handleChange} onChange={this.handleChange} onKeyPress={this.handleKeyPress} onKeyDown={this.handleKeyDown} onFocus={this.handleFocus} onBlur={this.handleBlur} onPaste={this.handlePaste} /> + <div + ref='wrapper' + className='textarea-wrapper' + > + <CommandList + ref='commands' + addCommand={this.addCommand} + channelId={this.props.channelId} + /> + <textarea + id={this.props.id} + ref='message' + className={`form-control custom-textarea ${this.state.connection}`} + spellCheck='true' + autoComplete='off' + autoCorrect='off' + rows='1' + maxLength={Constants.MAX_POST_LEN} + placeholder={this.props.createMessage} + value={this.props.messageText} + onInput={this.handleChange} + onChange={this.handleChange} + onKeyPress={this.handleKeyPress} + onKeyDown={this.handleKeyDown} + onFocus={this.handleFocus} + onBlur={this.handleBlur} + onPaste={this.handlePaste} + /> </div> ); } -}); +} + +Textbox.propTypes = { + id: React.PropTypes.string.isRequired, + channelId: React.PropTypes.string, + messageText: React.PropTypes.string.isRequired, + onUserInput: React.PropTypes.func.isRequired, + onKeyPress: React.PropTypes.func.isRequired, + onHeightChange: React.PropTypes.func, + createMessage: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 5c4d26a23..739084053 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -1,19 +1,9 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. - -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); var UserStore = require('../stores/user_store.jsx'); - -function getStateFromStores(userId) { - var profile = UserStore.getProfile(userId); - - if (profile == null) { - return { profile: { id: "0", username: "..."} }; - } else { - return { profile: profile }; - } -} +import {config} from '../utils/config.js'; var id = 0; @@ -22,49 +12,77 @@ function nextId() { return id; } +export default class UserProfile extends React.Component { + constructor(props) { + super(props); + + this.uniqueId = nextId(); + this.onChange = this.onChange.bind(this); + + this.state = this.getStateFromStores(this.props.userId); + } + getStateFromStores(userId) { + var profile = UserStore.getProfile(userId); + + if (profile == null) { + return {profile: {id: '0', username: '...'}}; + } -module.exports = React.createClass({ - uniqueId: null, - componentDidMount: function() { - UserStore.addChangeListener(this._onChange); - $("#profile_" + this.uniqueId).popover({placement : 'right', container: 'body', trigger: 'hover', html: true, delay: { "show": 200, "hide": 100 }}); - $('body').tooltip( {selector: '[data-toggle=tooltip]', trigger: 'hover click'} ); - }, - componentWillUnmount: function() { - UserStore.removeChangeListener(this._onChange); - }, - _onChange: function(id) { - if (id == this.props.userId) { - var newState = getStateFromStores(this.props.userId); - if (!utils.areStatesEqual(newState, this.state)) { + return {profile: profile}; + } + componentDidMount() { + UserStore.addChangeListener(this.onChange); + $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}}); + $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); + } + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + } + onChange(userId) { + if (userId === this.props.userId) { + var newState = this.getStateFromStores(this.props.userId); + if (!Utils.areStatesEqual(newState, this.state)) { this.setState(newState); } } - }, - componentWillReceiveProps: function(nextProps) { - if (this.props.userId != nextProps.userId) { - this.setState(getStateFromStores(nextProps.userId)); + } + componentWillReceiveProps(nextProps) { + if (this.props.userId !== nextProps.userId) { + this.setState(this.getStateFromStores(nextProps.userId)); + } + } + render() { + var name = this.state.profile.username; + if (this.props.overwriteName) { + name = this.props.overwriteName; } - }, - getInitialState: function() { - this.uniqueId = nextId(); - return getStateFromStores(this.props.userId); - }, - render: function() { - var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username; - - var data_content = "<img class='user-popover__image' src='/api/v1/users/" + this.state.profile.id + "/image?time=" + this.state.profile.update_at + "' height='128' width='128' />"; + var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />'; if (!config.ShowEmail) { - data_content += "<div class='text-nowrap'>Email not shared</div>"; + dataContent += '<div class="text-nowrap">Email not shared</div>'; } else { - data_content += "<div data-toggle='tooltip' title= '" + this.state.profile.email + "'><a href='mailto:" + this.state.profile.email + "' class='text-nowrap text-lowercase user-popover__email'>" + this.state.profile.email + "</a></div>"; + dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>'; } return ( - <div className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} > - { name } + <div + className='user-popover' + id={'profile_' + this.uniqueId} + data-toggle='popover' + data-content={dataContent} + data-original-title={this.state.profile.username} + > + {name} </div> ); } -}); +} + +UserProfile.defaultProps = { + userId: '', + overwriteName: '' +}; +UserProfile.propTypes = { + userId: React.PropTypes.string, + overwriteName: React.PropTypes.string +}; diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 9b0e906c5..2a607b3e0 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -8,56 +8,83 @@ var SecurityTab = require('./user_settings_security.jsx'); var GeneralTab = require('./user_settings_general.jsx'); var AppearanceTab = require('./user_settings_appearance.jsx'); -module.exports = React.createClass({ - displayName: 'UserSettings', - propTypes: { - activeTab: React.PropTypes.string, - activeSection: React.PropTypes.string, - updateSection: React.PropTypes.func, - updateTab: React.PropTypes.func - }, - componentDidMount: function() { +export default class UserSettings extends React.Component { + constructor(props) { + super(props); + + this.onListenerChange = this.onListenerChange.bind(this); + + this.state = {user: UserStore.getCurrentUser()}; + } + + componentDidMount() { UserStore.addChangeListener(this.onListenerChange); - }, - componentWillUnmount: function() { + } + + componentWillUnmount() { UserStore.removeChangeListener(this.onListenerChange); - }, - onListenerChange: function () { + } + + onListenerChange() { var user = UserStore.getCurrentUser(); if (!utils.areStatesEqual(this.state.user, user)) { this.setState({user: user}); } - }, - getInitialState: function() { - return {user: UserStore.getCurrentUser()}; - }, - render: function() { + } + + render() { if (this.props.activeTab === 'general') { return ( <div> - <GeneralTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> + <GeneralTab + user={this.state.user} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> </div> ); } else if (this.props.activeTab === 'security') { return ( <div> - <SecurityTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} /> + <SecurityTab + user={this.state.user} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> </div> ); } else if (this.props.activeTab === 'notifications') { return ( <div> - <NotificationsTab user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} /> + <NotificationsTab + user={this.state.user} + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> </div> ); } else if (this.props.activeTab === 'appearance') { return ( <div> - <AppearanceTab activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} /> + <AppearanceTab + activeSection={this.props.activeSection} + updateSection={this.props.updateSection} + updateTab={this.props.updateTab} + /> </div> ); - } else { - return <div/>; } + + return <div/>; } -}); +} + +UserSettings.propTypes = { + activeTab: React.PropTypes.string, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings_appearance.jsx index ba2d97ea8..3afdd7349 100644 --- a/web/react/components/user_settings_appearance.jsx +++ b/web/react/components/user_settings_appearance.jsx @@ -4,100 +4,138 @@ var UserStore = require('../stores/user_store.jsx'); var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); +var Client = require('../utils/client.jsx'); +var Utils = require('../utils/utils.jsx'); +import {config} from '../utils/config.js'; -module.exports = React.createClass({ - submitTheme: function(e) { +export default class UserSettingsAppearance extends React.Component { + constructor(props) { + super(props); + + this.submitTheme = this.submitTheme.bind(this); + this.updateTheme = this.updateTheme.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = this.getStateFromStores(); + } + getStateFromStores() { + var user = UserStore.getCurrentUser(); + var theme = '#2389d7'; + if (config.ThemeColors != null) { + theme = config.ThemeColors[0]; + } + if (user.props && user.props.theme) { + theme = user.props.theme; + } + + return {theme: theme.toLowerCase()}; + } + submitTheme(e) { e.preventDefault(); var user = UserStore.getCurrentUser(); - if (!user.props) user.props = {}; + if (!user.props) { + user.props = {}; + } user.props.theme = this.state.theme; - client.updateUser(user, - function(data) { - this.props.updateSection(""); + Client.updateUser(user, + function success() { + this.props.updateSection(''); window.location.reload(); }.bind(this), - function(err) { - state = this.getInitialState(); - state.server_error = err; + function fail(err) { + var state = this.getStateFromStores(); + state.serverError = err; this.setState(state); }.bind(this) ); - }, - updateTheme: function(e) { - var hex = utils.rgb2hex(e.target.style.backgroundColor); - this.setState({ theme: hex.toLowerCase() }); - }, - handleClose: function() { - this.setState({server_error: null}); + } + updateTheme(e) { + var hex = Utils.rgb2hex(e.target.style.backgroundColor); + this.setState({theme: hex.toLowerCase()}); + } + handleClose() { + this.setState({serverError: null}); this.props.updateTab('general'); - }, - componentDidMount: function() { - if (this.props.activeSection === "theme") { - $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); + } + componentDidMount() { + if (this.props.activeSection === 'theme') { + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentDidUpdate: function() { - if (this.props.activeSection === "theme") { + } + componentDidUpdate() { + if (this.props.activeSection === 'theme') { $('.color-btn').removeClass('active-border'); - $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); - }, - getInitialState: function() { - var user = UserStore.getCurrentUser(); - var theme = config.ThemeColors != null ? config.ThemeColors[0] : "#2389d7"; - if (user.props && user.props.theme) { - theme = user.props.theme; + } + render() { + var serverError; + if (this.state.serverError) { + serverError = this.state.serverError; } - return { theme: theme.toLowerCase() }; - }, - render: function() { - var server_error = this.state.server_error ? this.state.server_error : null; - var themeSection; var self = this; if (config.ThemeColors != null) { if (this.props.activeSection === 'theme') { - var theme_buttons = []; + var themeButtons = []; for (var i = 0; i < config.ThemeColors.length; i++) { - theme_buttons.push(<button ref={config.ThemeColors[i]} type="button" className="btn btn-lg color-btn" style={{backgroundColor: config.ThemeColors[i]}} onClick={this.updateTheme} />); + themeButtons.push( + <button + key={config.ThemeColors[i] + 'key' + i} + ref={config.ThemeColors[i]} + type='button' + className='btn btn-lg color-btn' + style={{backgroundColor: config.ThemeColors[i]}} + onClick={this.updateTheme} + /> + ); } var inputs = []; inputs.push( - <li className="setting-list-item"> - <div className="btn-group" data-toggle="buttons-radio"> - { theme_buttons } + <li + key='themeColorSetting' + className='setting-list-item' + > + <div + className='btn-group' + data-toggle='buttons-radio' + > + {themeButtons} </div> </li> ); themeSection = ( <SettingItemMax - title="Theme Color" + title='Theme Color' inputs={inputs} submit={this.submitTheme} - server_error={server_error} - updateSection={function(e){self.props.updateSection("");e.preventDefault;}} + serverError={serverError} + updateSection={function updateSection(e) { + self.props.updateSection(''); + e.preventDefault(); + }} /> ); } else { themeSection = ( <SettingItemMin - title="Theme Color" + title='Theme Color' describe={this.state.theme} - updateSection={function(){self.props.updateSection("theme");}} + updateSection={function updateSection() { + self.props.updateSection('theme'); + }} /> ); } @@ -105,17 +143,38 @@ module.exports = React.createClass({ return ( <div> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" ref="title"><i className="modal-back"></i>Appearance Settings</h4> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i>Appearance Settings + </h4> </div> - <div className="user-settings"> - <h3 className="tab-header">Appearance Settings</h3> - <div className="divider-dark first"/> + <div className='user-settings'> + <h3 className='tab-header'>Appearance Settings</h3> + <div className='divider-dark first'/> {themeSection} - <div className="divider-dark"/> + <div className='divider-dark'/> </div> </div> ); } -}); +} + +UserSettingsAppearance.defaultProps = { + activeSection: '' +}; +UserSettingsAppearance.propTypes = { + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx index fed11fbe9..184534a9a 100644 --- a/web/react/components/user_settings_general.jsx +++ b/web/react/components/user_settings_general.jsx @@ -11,10 +11,32 @@ var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); var assign = require('object-assign'); -module.exports = React.createClass({ - displayName: 'GeneralTab', - submitActive: false, - submitUsername: function(e) { +export default class UserSettingsGeneralTab extends React.Component { + constructor(props) { + super(props); + this.submitActive = false; + + this.submitUsername = this.submitUsername.bind(this); + this.submitNickname = this.submitNickname.bind(this); + this.submitName = this.submitName.bind(this); + this.submitEmail = this.submitEmail.bind(this); + this.submitUser = this.submitUser.bind(this); + this.submitPicture = this.submitPicture.bind(this); + + this.updateUsername = this.updateUsername.bind(this); + this.updateFirstName = this.updateFirstName.bind(this); + this.updateLastName = this.updateLastName.bind(this); + this.updateNickname = this.updateNickname.bind(this); + this.updateEmail = this.updateEmail.bind(this); + this.updatePicture = this.updatePicture.bind(this); + this.updateSection = this.updateSection.bind(this); + + this.handleClose = this.handleClose.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(props); + } + submitUsername(e) { e.preventDefault(); var user = this.props.user; @@ -37,8 +59,8 @@ module.exports = React.createClass({ user.username = username; this.submitUser(user); - }, - submitNickname: function(e) { + } + submitNickname(e) { e.preventDefault(); var user = UserStore.getCurrentUser(); @@ -52,8 +74,8 @@ module.exports = React.createClass({ user.nickname = nickname; this.submitUser(user); - }, - submitName: function(e) { + } + submitName(e) { e.preventDefault(); var user = UserStore.getCurrentUser(); @@ -69,8 +91,8 @@ module.exports = React.createClass({ user.last_name = lastName; this.submitUser(user); - }, - submitEmail: function(e) { + } + submitEmail(e) { e.preventDefault(); var user = UserStore.getCurrentUser(); @@ -88,15 +110,15 @@ module.exports = React.createClass({ user.email = email; this.submitUser(user); - }, - submitUser: function(user) { + } + submitUser(user) { client.updateUser(user, - function() { + function updateSuccess() { this.updateSection(''); AsyncClient.getMe(); }.bind(this), - function(err) { - var state = this.getInitialState(); + function updateFailure(err) { + var state = this.setupInitialState(this.props); if (err.message) { state.serverError = err.message; } else { @@ -105,8 +127,8 @@ module.exports = React.createClass({ this.setState(state); }.bind(this) ); - }, - submitPicture: function(e) { + } + submitPicture(e) { e.preventDefault(); if (!this.state.picture) { @@ -129,34 +151,34 @@ module.exports = React.createClass({ this.setState({loadingPicture: true}); client.uploadProfileImage(formData, - function() { + function imageUploadSuccess() { this.submitActive = false; AsyncClient.getMe(); window.location.reload(); }.bind(this), - function(err) { - var state = this.getInitialState(); + function imageUploadFailure(err) { + var state = this.setupInitialState(this.props); state.serverError = err; this.setState(state); }.bind(this) ); - }, - updateUsername: function(e) { + } + updateUsername(e) { this.setState({username: e.target.value}); - }, - updateFirstName: function(e) { + } + updateFirstName(e) { this.setState({firstName: e.target.value}); - }, - updateLastName: function(e) { + } + updateLastName(e) { this.setState({lastName: e.target.value}); - }, - updateNickname: function(e) { + } + updateNickname(e) { this.setState({nickname: e.target.value}); - }, - updateEmail: function(e) { + } + updateEmail(e) { this.setState({email: e.target.value}); - }, - updatePicture: function(e) { + } + updatePicture(e) { if (e.target.files && e.target.files[0]) { this.setState({picture: e.target.files[0]}); @@ -165,34 +187,33 @@ module.exports = React.createClass({ } else { this.setState({picture: null}); } - }, - updateSection: function(section) { - this.setState(assign({}, this.getInitialState(), {clientError: ''})); + } + updateSection(section) { + this.setState(assign({}, this.setupInitialState(this.props), {clientError: '', serverError: '', emailError: ''})); this.submitActive = false; this.props.updateSection(section); - }, - handleClose: function() { - $(this.getDOMNode()).find('.form-control').each(function() { + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function clearForms() { this.value = ''; }); - this.setState(assign({}, this.getInitialState(), {clientError: null, serverError: null, emailError: null})); + this.setState(assign({}, this.setupInitialState(this.props), {clientError: null, serverError: null, emailError: null})); this.props.updateSection(''); - }, - componentDidMount: function() { + } + componentDidMount() { $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $('#user_settings').off('hidden.bs.modal', this.handleClose); - }, - getInitialState: function() { - var user = this.props.user; + } + setupInitialState(props) { + var user = props.user; var emailEnabled = !ConfigStore.getSettingAsBoolean('ByPassEmail', false); - return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, - email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled}; - }, - render: function() { + email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled}; + } + render() { var user = this.props.user; var clientError = null; @@ -209,28 +230,65 @@ module.exports = React.createClass({ } var nameSection; - var self = this; var inputs = []; if (this.props.activeSection === 'name') { inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>First Name</label> + <div + key='firstNameSetting' + className='form-group' + > + <label className='col-sm-5 control-label'>{'First Name'}</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.firstName}/> + <input + className='form-control' + type='text' + onChange={this.updateFirstName} + value={this.state.firstName} + /> </div> </div> ); inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>Last Name</label> + <div + key='lastNameSetting' + className='form-group' + > + <label className='col-sm-5 control-label'>{'Last Name'}</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.lastName}/> + <input + className='form-control' + type='text' + onChange={this.updateLastName} + value={this.state.lastName} + /> </div> </div> ); + function notifClick(e) { + e.preventDefault(); + this.updateSection(''); + this.props.updateTab('notifications'); + } + + const notifLink = ( + <a + href='#' + onClick={notifClick.bind(this)} + > + {'Notifications'} + </a> + ); + + const extraInfo = ( + <span> + {'By default, you will receive mention notifications when someone types your first name. '} + {'Go to '} {notifLink} {'settings to change this default.'} + </span> + ); + nameSection = ( <SettingItemMax title='Full Name' @@ -238,10 +296,11 @@ module.exports = React.createClass({ submit={this.submitName} server_error={serverError} client_error={clientError} - updateSection={function(e) { - self.updateSection(''); + updateSection={function clearSection(e) { + this.updateSection(''); e.preventDefault(); - }} + }.bind(this)} + extraInfo={extraInfo} /> ); } else { @@ -259,24 +318,44 @@ module.exports = React.createClass({ <SettingItemMin title='Full Name' describe={fullName} - updateSection={function() { - self.updateSection('name'); - }} + updateSection={function updateNameSection() { + this.updateSection('name'); + }.bind(this)} /> ); } var nicknameSection; if (this.props.activeSection === 'nickname') { + let nicknameLabel = 'Nickname'; + if (utils.isMobile()) { + nicknameLabel = ''; + } + inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label> + <div + key='nicknameSetting' + className='form-group' + > + <label className='col-sm-5 control-label'>{nicknameLabel}</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/> + <input + className='form-control' + type='text' + onChange={this.updateNickname} + value={this.state.nickname} + /> </div> </div> ); + const extraInfo = ( + <span> + {'Use Nickname for a name you might be called that is different from your first name and user name.'} + {'This is most often used when two or more people have similar sounding names and usernames.'} + </span> + ); + nicknameSection = ( <SettingItemMax title='Nickname' @@ -284,10 +363,11 @@ module.exports = React.createClass({ submit={this.submitNickname} server_error={serverError} client_error={clientError} - updateSection={function(e) { - self.updateSection(''); + updateSection={function clearSection(e) { + this.updateSection(''); e.preventDefault(); - }} + }.bind(this)} + extraInfo={extraInfo} /> ); } else { @@ -295,24 +375,39 @@ module.exports = React.createClass({ <SettingItemMin title='Nickname' describe={UserStore.getCurrentUser().nickname} - updateSection={function() { - self.updateSection('nickname'); - }} + updateSection={function updateNicknameSection() { + this.updateSection('nickname'); + }.bind(this)} /> ); } var usernameSection; if (this.props.activeSection === 'username') { + let usernameLabel = 'Username'; + if (utils.isMobile()) { + usernameLabel = ''; + } + inputs.push( - <div className='form-group'> - <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Username'}</label> + <div + key='usernameSetting' + className='form-group' + > + <label className='col-sm-5 control-label'>{usernameLabel}</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/> + <input + className='form-control' + type='text' + onChange={this.updateUsername} + value={this.state.username} + /> </div> </div> ); + const extraInfo = (<span>{'Pick something easy for teammates to recognize and recall.'}</span>); + usernameSection = ( <SettingItemMax title='Username' @@ -320,10 +415,11 @@ module.exports = React.createClass({ submit={this.submitUsername} server_error={serverError} client_error={clientError} - updateSection={function(e) { - self.updateSection(''); + updateSection={function clearSection(e) { + this.updateSection(''); e.preventDefault(); - }} + }.bind(this)} + extraInfo={extraInfo} /> ); } else { @@ -331,9 +427,9 @@ module.exports = React.createClass({ <SettingItemMin title='Username' describe={UserStore.getCurrentUser().username} - updateSection={function() { - self.updateSection('username'); - }} + updateSection={function updateUsernameSection() { + this.updateSection('username'); + }.bind(this)} /> ); } @@ -342,15 +438,20 @@ module.exports = React.createClass({ let helpText = <div>Email is used for notifications, and requires verification if changed.</div>; if (!this.state.emailEnabled) { - helpText = <div className='text-danger'><br />Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.</div>; + helpText = <div className='setting-list__hint text-danger'>{'Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'}</div>; } inputs.push( - <div> + <div key='emailSetting'> <div className='form-group'> - <label className='col-sm-5 control-label'>Primary Email</label> + <label className='col-sm-5 control-label'>{'Primary Email'}</label> <div className='col-sm-7'> - <input className='form-control' type='text' onChange={this.updateEmail} value={this.state.email}/> + <input + className='form-control' + type='text' + onChange={this.updateEmail} + value={this.state.email} + /> </div> </div> {helpText} @@ -364,10 +465,10 @@ module.exports = React.createClass({ submit={this.submitEmail} server_error={serverError} client_error={emailError} - updateSection={function(e) { - self.updateSection(''); + updateSection={function clearSection(e) { + this.updateSection(''); e.preventDefault(); - }} + }.bind(this)} /> ); } else { @@ -375,9 +476,9 @@ module.exports = React.createClass({ <SettingItemMin title='Email' describe={UserStore.getCurrentUser().email} - updateSection={function() { - self.updateSection('email'); - }} + updateSection={function updateEmailSection() { + this.updateSection('email'); + }.bind(this)} /> ); } @@ -391,10 +492,10 @@ module.exports = React.createClass({ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} server_error={serverError} client_error={clientError} - updateSection={function(e) { - self.updateSection(''); + updateSection={function clearSection(e) { + this.updateSection(''); e.preventDefault(); - }} + }.bind(this)} picture={this.state.picture} pictureChange={this.updatePicture} submitActive={this.submitActive} @@ -410,20 +511,33 @@ module.exports = React.createClass({ <SettingItemMin title='Profile Picture' describe={minMessage} - updateSection={function() { - self.updateSection('picture'); - }} + updateSection={function updatePictureSection() { + this.updateSection('picture'); + }.bind(this)} /> ); } return ( <div> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'><i className='modal-back'></i>General Settings</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>{'×'}</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i> + {'General Settings'} + </h4> </div> <div className='user-settings'> - <h3 className='tab-header'>General Settings</h3> + <h3 className='tab-header'>{'General Settings'}</h3> <div className='divider-dark first'/> {nameSection} <div className='divider-light'/> @@ -439,4 +553,11 @@ module.exports = React.createClass({ </div> ); } -}); +} + +UserSettingsGeneralTab.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings_modal.jsx index f5a555951..7ec75e000 100644 --- a/web/react/components/user_settings_modal.jsx +++ b/web/react/components/user_settings_modal.jsx @@ -4,51 +4,75 @@ var SettingsSidebar = require('./settings_sidebar.jsx'); var UserSettings = require('./user_settings.jsx'); -module.exports = React.createClass({ - componentDidMount: function() { - $('body').on('click', '.modal-back', function(){ +export default class UserSettingsModal extends React.Component { + constructor(props) { + super(props); + + this.updateTab = this.updateTab.bind(this); + this.updateSection = this.updateSection.bind(this); + + this.state = {active_tab: 'general', active_section: ''}; + } + componentDidMount() { + $('body').on('click', '.modal-back', function changeDisplay() { $(this).closest('.modal-dialog').removeClass('display--content'); }); - $('body').on('click', '.modal-header .close', function(){ - setTimeout(function() { + $('body').on('click', '.modal-header .close', function closeModal() { + setTimeout(function finishClose() { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); }); - }, - updateTab: function(tab) { - this.setState({ active_tab: tab }); - }, - updateSection: function(section) { - this.setState({ active_section: section }); - }, - getInitialState: function() { - return { active_tab: "general", active_section: "" }; - }, - render: function() { + } + updateTab(tab) { + this.setState({active_tab: tab}); + } + updateSection(section) { + this.setState({active_section: section}); + } + render() { var tabs = []; - tabs.push({name: "general", uiName: "General", icon: "glyphicon glyphicon-cog"}); - tabs.push({name: "security", uiName: "Security", icon: "glyphicon glyphicon-lock"}); - tabs.push({name: "notifications", uiName: "Notifications", icon: "glyphicon glyphicon-exclamation-sign"}); - tabs.push({name: "appearance", uiName: "Appearance", icon: "glyphicon glyphicon-wrench"}); + tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'}); + tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'}); + tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'}); + tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'}); return ( - <div className="modal fade" ref="modal" id="user_settings" role="dialog" tabIndex="-1" aria-hidden="true"> - <div className="modal-dialog settings-modal"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" ref="title">Account Settings</h4> + <div + className='modal fade' + ref='modal' + id='user_settings' + role='dialog' + tabIndex='-1' + aria-hidden='true' + > + <div className='modal-dialog settings-modal'> + <div className='modal-content'> + <div className='modal-header'> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + ref='title' + > + Account Settings + </h4> </div> - <div className="modal-body"> - <div className="settings-table"> - <div className="settings-links"> + <div className='modal-body'> + <div className='settings-table'> + <div className='settings-links'> <SettingsSidebar tabs={tabs} activeTab={this.state.active_tab} updateTab={this.updateTab} /> </div> - <div className="settings-content minimize-settings"> + <div className='settings-content minimize-settings'> <UserSettings activeTab={this.state.active_tab} activeSection={this.state.active_section} @@ -63,4 +87,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings_notifications.jsx index b89f72987..33db1a332 100644 --- a/web/react/components/user_settings_notifications.jsx +++ b/web/react/components/user_settings_notifications.jsx @@ -8,6 +8,7 @@ var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); var assign = require('object-assign'); +import {config} from '../utils/config.js'; function getNotificationsStateFromStores() { var user = UserStore.getCurrentUser(); @@ -67,7 +68,9 @@ function getNotificationsStateFromStores() { } } - return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound, usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0, firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey}; + return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound, + usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0, + firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey}; } export default class NotificationsTab extends React.Component { @@ -136,7 +139,7 @@ export default class NotificationsTab extends React.Component { this.props.updateTab('general'); } updateSection(section) { - this.setState(this.getInitialState()); + this.setState(getNotificationsStateFromStores()); this.props.updateSection(section); } componentDidMount() { @@ -156,15 +159,15 @@ export default class NotificationsTab extends React.Component { } handleNotifyRadio(notifyLevel) { this.setState({notifyLevel: notifyLevel}); - this.refs.wrapper.getDOMNode().focus(); + React.findDOMNode(this.refs.wrapper).focus(); } handleEmailRadio(enableEmail) { this.setState({enableEmail: enableEmail}); - this.refs.wrapper.getDOMNode().focus(); + React.findDOMNode(this.refs.wrapper).focus(); } handleSoundRadio(enableSound) { this.setState({enableSound: enableSound}); - this.refs.wrapper.getDOMNode().focus(); + React.findDOMNode(this.refs.wrapper).focus(); } updateUsernameKey(val) { this.setState({usernameKey: val}); @@ -182,10 +185,10 @@ export default class NotificationsTab extends React.Component { this.setState({channelKey: val}); } updateCustomMentionKeys() { - var checked = this.refs.customcheck.getDOMNode().checked; + var checked = React.findDOMNode(this.refs.customcheck).checked; if (checked) { - var text = this.refs.custommentions.getDOMNode().value; + var text = React.findDOMNode(this.refs.custommentions).value; // remove all spaces and split string into individual keys this.setState({customKeys: text.replace(/ /g, ''), customKeysChecked: true}); @@ -194,7 +197,7 @@ export default class NotificationsTab extends React.Component { } } onCustomChange() { - this.refs.customcheck.getDOMNode().checked = true; + React.findDOMNode(this.refs.customcheck).checked = true; this.updateCustomMentionKeys(); } render() { @@ -203,8 +206,6 @@ export default class NotificationsTab extends React.Component { serverError = this.state.serverError; } - var self = this; - var user = this.props.user; var desktopSection; @@ -222,12 +223,12 @@ export default class NotificationsTab extends React.Component { let inputs = []; inputs.push( - <div> + <div key='userNotificationLevelOption'> <div className='radio'> <label> <input type='radio' checked={notifyActive[0]} - onChange={self.handleNotifyRadio.bind(this, 'all')} + onChange={this.handleNotifyRadio.bind(this, 'all')} > For all activity </input> @@ -239,7 +240,7 @@ export default class NotificationsTab extends React.Component { <input type='radio' checked={notifyActive[1]} - onChange={self.handleNotifyRadio.bind(this, 'mention')} + onChange={this.handleNotifyRadio.bind(this, 'mention')} > Only for mentions and private messages </input> @@ -251,7 +252,7 @@ export default class NotificationsTab extends React.Component { <input type='radio' checked={notifyActive[2]} - onChange={self.handleNotifyRadio.bind(this, 'none')} + onChange={this.handleNotifyRadio.bind(this, 'none')} > Never </input> @@ -261,9 +262,9 @@ export default class NotificationsTab extends React.Component { ); handleUpdateDesktopSection = function updateDesktopSection(e) { - self.props.updateSection(''); + this.props.updateSection(''); e.preventDefault(); - }; + }.bind(this); desktopSection = ( <SettingItemMax @@ -285,8 +286,8 @@ export default class NotificationsTab extends React.Component { } handleUpdateDesktopSection = function updateDesktopSection() { - self.props.updateSection('desktop'); - }; + this.props.updateSection('desktop'); + }.bind(this); desktopSection = ( <SettingItemMin @@ -310,13 +311,13 @@ export default class NotificationsTab extends React.Component { let inputs = []; inputs.push( - <div> + <div key='userNotificationSoundOptions'> <div className='radio'> <label> <input type='radio' checked={soundActive[0]} - onChange={self.handleSoundRadio.bind(this, 'true')} + onChange={this.handleSoundRadio.bind(this, 'true')} > On </input> @@ -328,7 +329,7 @@ export default class NotificationsTab extends React.Component { <input type='radio' checked={soundActive[1]} - onChange={self.handleSoundRadio.bind(this, 'false')} + onChange={this.handleSoundRadio.bind(this, 'false')} > Off </input> @@ -339,9 +340,9 @@ export default class NotificationsTab extends React.Component { ); handleUpdateSoundSection = function updateSoundSection(e) { - self.props.updateSection(''); + this.props.updateSection(''); e.preventDefault(); - }; + }.bind(this); soundSection = ( <SettingItemMax @@ -363,8 +364,8 @@ export default class NotificationsTab extends React.Component { } handleUpdateSoundSection = function updateSoundSection() { - self.props.updateSection('sound'); - }; + this.props.updateSection('sound'); + }.bind(this); soundSection = ( <SettingItemMin @@ -389,13 +390,13 @@ export default class NotificationsTab extends React.Component { let inputs = []; inputs.push( - <div> + <div key='userNotificationEmailOptions'> <div className='radio'> <label> <input type='radio' checked={emailActive[0]} - onChange={self.handleEmailRadio.bind(this, 'true')} + onChange={this.handleEmailRadio.bind(this, 'true')} > On </input> @@ -407,7 +408,7 @@ export default class NotificationsTab extends React.Component { <input type='radio' checked={emailActive[1]} - onChange={self.handleEmailRadio.bind(this, 'false')} + onChange={this.handleEmailRadio.bind(this, 'false')} > Off </input> @@ -419,9 +420,9 @@ export default class NotificationsTab extends React.Component { ); handleUpdateEmailSection = function updateEmailSection(e) { - self.props.updateSection(''); + this.props.updateSection(''); e.preventDefault(); - }; + }.bind(this); emailSection = ( <SettingItemMax @@ -441,8 +442,8 @@ export default class NotificationsTab extends React.Component { } handleUpdateEmailSection = function updateEmailSection() { - self.props.updateSection('email'); - }; + this.props.updateSection('email'); + }.bind(this); emailSection = ( <SettingItemMin @@ -466,10 +467,10 @@ export default class NotificationsTab extends React.Component { if (user.first_name) { handleUpdateFirstNameKey = function handleFirstNameKeyChange(e) { - self.updateFirstNameKey(e.target.checked); - }; + this.updateFirstNameKey(e.target.checked); + }.bind(this); inputs.push( - <div> + <div key='userNotificationFirstNameOption'> <div className='checkbox'> <label> <input @@ -486,10 +487,10 @@ export default class NotificationsTab extends React.Component { } handleUpdateUsernameKey = function handleUsernameKeyChange(e) { - self.updateUsernameKey(e.target.checked); - }; + this.updateUsernameKey(e.target.checked); + }.bind(this); inputs.push( - <div> + <div key='userNotificationUsernameOption'> <div className='checkbox'> <label> <input @@ -505,10 +506,10 @@ export default class NotificationsTab extends React.Component { ); handleUpdateMentionKey = function handleMentionKeyChange(e) { - self.updateMentionKey(e.target.checked); - }; + this.updateMentionKey(e.target.checked); + }.bind(this); inputs.push( - <div> + <div key='userNotificationMentionOption'> <div className='checkbox'> <label> <input @@ -524,10 +525,10 @@ export default class NotificationsTab extends React.Component { ); handleUpdateAllKey = function handleAllKeyChange(e) { - self.updateAllKey(e.target.checked); - }; + this.updateAllKey(e.target.checked); + }.bind(this); inputs.push( - <div> + <div key='userNotificationAllOption'> <div className='checkbox'> <label> <input @@ -543,10 +544,10 @@ export default class NotificationsTab extends React.Component { ); handleUpdateChannelKey = function handleChannelKeyChange(e) { - self.updateChannelKey(e.target.checked); - }; + this.updateChannelKey(e.target.checked); + }.bind(this); inputs.push( - <div> + <div key='userNotificationChannelOption'> <div className='checkbox'> <label> <input @@ -562,7 +563,7 @@ export default class NotificationsTab extends React.Component { ); inputs.push( - <div> + <div key='userNotificationCustomOption'> <div className='checkbox'> <label> <input @@ -586,9 +587,9 @@ export default class NotificationsTab extends React.Component { ); handleUpdateKeysSection = function updateKeysSection(e) { - self.props.updateSection(''); + this.props.updateSection(''); e.preventDefault(); - }; + }.bind(this); keysSection = ( <SettingItemMax title='Words that trigger mentions' @@ -631,8 +632,8 @@ export default class NotificationsTab extends React.Component { } handleUpdateKeysSection = function updateKeysSection() { - self.props.updateSection('keys'); - }; + this.props.updateSection('keys'); + }.bind(this); keysSection = ( <SettingItemMin diff --git a/web/react/components/user_settings_security.jsx b/web/react/components/user_settings_security.jsx index ae8a5f0bc..6ccd09cb1 100644 --- a/web/react/components/user_settings_security.jsx +++ b/web/react/components/user_settings_security.jsx @@ -3,13 +3,24 @@ var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMax = require('./setting_item_max.jsx'); -var client = require('../utils/client.jsx'); +var Client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); -module.exports = React.createClass({ - displayName: 'SecurityTab', - submitPassword: function(e) { +export default class SecurityTab extends React.Component { + constructor(props) { + super(props); + + this.submitPassword = this.submitPassword.bind(this); + this.updateCurrentPassword = this.updateCurrentPassword.bind(this); + this.updateNewPassword = this.updateNewPassword.bind(this); + this.updateConfirmPassword = this.updateConfirmPassword.bind(this); + this.handleClose = this.handleClose.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(); + } + submitPassword(e) { e.preventDefault(); var user = this.props.user; @@ -37,14 +48,14 @@ module.exports = React.createClass({ data.current_password = currentPassword; data.new_password = newPassword; - client.updatePassword(data, - function() { + Client.updatePassword(data, + function success() { this.props.updateSection(''); AsyncClient.getMe(); - this.setState({currentPassword: '', newPassword: '', confirmPassword: ''}); + this.setState(this.setupInitialState()); }.bind(this), - function(err) { - var state = this.getInitialState(); + function fail(err) { + var state = this.setupInitialState(); if (err.message) { state.serverError = err.message; } else { @@ -54,73 +65,102 @@ module.exports = React.createClass({ this.setState(state); }.bind(this) ); - }, - updateCurrentPassword: function(e) { + } + updateCurrentPassword(e) { this.setState({currentPassword: e.target.value}); - }, - updateNewPassword: function(e) { + } + updateNewPassword(e) { this.setState({newPassword: e.target.value}); - }, - updateConfirmPassword: function(e) { + } + updateConfirmPassword(e) { this.setState({confirmPassword: e.target.value}); - }, - handleHistoryOpen: function() { - $("#user_settings").modal('hide'); - }, - handleDevicesOpen: function() { - $("#user_settings").modal('hide'); - }, - handleClose: function() { - $(this.getDOMNode()).find('.form-control').each(function() { + } + handleHistoryOpen() { + $('#user_settings').modal('hide'); + } + handleDevicesOpen() { + $('#user_settings').modal('hide'); + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function resetValue() { this.value = ''; }); this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); this.props.updateTab('general'); - }, - componentDidMount: function() { + } + setupInitialState() { + return {currentPassword: '', newPassword: '', confirmPassword: ''}; + } + componentDidMount() { $('#user_settings').on('hidden.bs.modal', this.handleClose); - }, - componentWillUnmount: function() { + } + componentWillUnmount() { $('#user_settings').off('hidden.bs.modal', this.handleClose); this.props.updateSection(''); - }, - getInitialState: function() { - return {currentPassword: '', newPassword: '', confirmPassword: ''}; - }, - render: function() { - var serverError = this.state.serverError ? this.state.serverError : null; - var passwordError = this.state.passwordError ? this.state.passwordError : null; + } + render() { + var serverError; + if (this.state.serverError) { + serverError = this.state.serverError; + } + var passwordError; + if (this.state.passwordError) { + passwordError = this.state.passwordError; + } var updateSectionStatus; var passwordSection; - var self = this; if (this.props.activeSection === 'password') { var inputs = []; var submit = null; if (this.props.user.auth_service === '') { inputs.push( - <div className='form-group'> + <div + key='currentPasswordUpdateForm' + className='form-group' + > <label className='col-sm-5 control-label'>Current Password</label> <div className='col-sm-7'> - <input className='form-control' type='password' onChange={this.updateCurrentPassword} value={this.state.currentPassword}/> + <input + className='form-control' + type='password' + onChange={this.updateCurrentPassword} + value={this.state.currentPassword} + /> </div> </div> ); inputs.push( - <div className='form-group'> + <div + key='newPasswordUpdateForm' + className='form-group' + > <label className='col-sm-5 control-label'>New Password</label> <div className='col-sm-7'> - <input className='form-control' type='password' onChange={this.updateNewPassword} value={this.state.newPassword}/> + <input + className='form-control' + type='password' + onChange={this.updateNewPassword} + value={this.state.newPassword} + /> </div> </div> ); inputs.push( - <div className='form-group'> + <div + key='retypeNewPasswordUpdateForm' + className='form-group' + > <label className='col-sm-5 control-label'>Retype New Password</label> <div className='col-sm-7'> - <input className='form-control' type='password' onChange={this.updateConfirmPassword} value={this.state.confirmPassword}/> + <input + className='form-control' + type='password' + onChange={this.updateConfirmPassword} + value={this.state.confirmPassword} + /> </div> </div> ); @@ -128,17 +168,20 @@ module.exports = React.createClass({ submit = this.submitPassword; } else { inputs.push( - <div className='form-group'> + <div + key='oauthPasswordInfo' + className='form-group' + > <label className='col-sm-12'>Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> </div> ); } - updateSectionStatus = function(e) { - self.props.updateSection(''); - self.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); e.preventDefault(); - }; + }.bind(this); passwordSection = ( <SettingItemMax @@ -154,17 +197,27 @@ module.exports = React.createClass({ var describe; if (this.props.user.auth_service === '') { var d = new Date(this.props.user.last_password_update); - var hour = d.getHours() % 12 ? String(d.getHours() % 12) : '12'; - var min = d.getMinutes() < 10 ? '0' + d.getMinutes() : String(d.getMinutes()); - var timeOfDay = d.getHours() >= 12 ? ' pm' : ' am'; + var hour = '12'; + if (d.getHours() % 12) { + hour = String(d.getHours() % 12); + } + var min = String(d.getMinutes()); + if (d.getMinutes() < 10) { + min = '0' + d.getMinutes(); + } + var timeOfDay = ' am'; + if (d.getHours() >= 12) { + timeOfDay = ' pm'; + } + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; } else { describe = 'Log in done through GitLab'; } - updateSectionStatus = function() { - self.props.updateSection('password'); - }; + updateSectionStatus = function updateSection() { + this.props.updateSection('password'); + }.bind(this); passwordSection = ( <SettingItemMin @@ -178,8 +231,20 @@ module.exports = React.createClass({ return ( <div> <div className='modal-header'> - <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> - <h4 className='modal-title' ref='title'><i className='modal-back'></i>Security Settings</h4> + <button + type='button' + className='close' + data-dismiss='modal' + aria-label='Close' + > + <span aria-hidden='true'>×</span> + </button> + <h4 + className='modal-title' + ref='title' + > + <i className='modal-back'></i>Security Settings + </h4> </div> <div className='user-settings'> <h3 className='tab-header'>Security Settings</h3> @@ -187,11 +252,38 @@ module.exports = React.createClass({ {passwordSection} <div className='divider-dark'/> <br></br> - <a data-toggle='modal' className='security-links theme' data-target='#access-history' href='#' onClick={this.handleHistoryOpen}><i className='fa fa-clock-o'></i>View Access History</a> + <a + data-toggle='modal' + className='security-links theme' + data-target='#access-history' + href='#' + onClick={this.handleHistoryOpen} + > + <i className='fa fa-clock-o'></i>View Access History + </a> <b> </b> - <a data-toggle='modal' className='security-links theme' data-target='#activity-log' href='#' onClick={this.handleDevicesOpen}><i className='fa fa-globe'></i>View and Logout of Active Sessions</a> + <a + data-toggle='modal' + className='security-links theme' + data-target='#activity-log' + href='#' + onClick={this.handleDevicesOpen} + > + <i className='fa fa-globe'></i>View and Logout of Active Sessions + </a> </div> </div> ); } -}); +} + +SecurityTab.defaultProps = { + user: {}, + activeSection: '' +}; +SecurityTab.propTypes = { + user: React.PropTypes.object, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 24efce0f1..b0eaba5d6 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -2,35 +2,47 @@ // See License.txt for license information. var Client = require('../utils/client.jsx'); -var utils = require('../utils/utils.jsx'); +var Utils = require('../utils/utils.jsx'); +import {config} from '../utils/config.js'; -module.exports = React.createClass({ - displayName: 'ViewImageModal', - propTypes: { - filenames: React.PropTypes.array, - modalId: React.PropTypes.string, - channelId: React.PropTypes.string, - userId: React.PropTypes.string, - startId: React.PropTypes.number - }, - canSetState: false, - handleNext: function() { +export default class ViewImageModal extends React.Component { + constructor(props) { + super(props); + + this.canSetState = false; + + this.loadImage = this.loadImage.bind(this); + this.handleNext = this.handleNext.bind(this); + this.handlePrev = this.handlePrev.bind(this); + this.handleKeyPress = this.handleKeyPress.bind(this); + this.getPublicLink = this.getPublicLink.bind(this); + this.getPreviewImagePath = this.getPreviewImagePath.bind(this); + + var loaded = []; + var progress = []; + for (var i = 0; i < this.props.filenames.length; i++) { + loaded.push(false); + progress.push(0); + } + this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}}; + } + handleNext() { var id = this.state.imgId + 1; if (id > this.props.filenames.length - 1) { id = 0; } this.setState({imgId: id}); this.loadImage(id); - }, - handlePrev: function() { + } + handlePrev() { var id = this.state.imgId - 1; if (id < 0) { id = this.props.filenames.length - 1; } this.setState({imgId: id}); this.loadImage(id); - }, - handleKeyPress: function handleKeyPress(e) { + } + handleKeyPress(e) { if (!e) { return; } else if (e.keyCode === 39) { @@ -38,11 +50,11 @@ module.exports = React.createClass({ } else if (e.keyCode === 37) { this.handlePrev(); } - }, - componentWillReceiveProps: function(nextProps) { + } + componentWillReceiveProps(nextProps) { this.setState({imgId: nextProps.startId}); - }, - loadImage: function(id) { + } + loadImage(id) { var imgHeight = $(window).height() - 100; if (this.state.loaded[id] || this.state.images[id]) { $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight); @@ -51,26 +63,25 @@ module.exports = React.createClass({ var filename = this.props.filenames[id]; - var fileInfo = utils.splitFileLocation(filename); - var fileType = utils.getFileType(fileInfo.ext); + var fileInfo = Utils.splitFileLocation(filename); + var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { - var self = this; var img = new Image(); img.load(this.getPreviewImagePath(filename), - function() { - var progress = self.state.progress; + function load() { + var progress = this.state.progress; progress[id] = img.completedPercentage; - self.setState({progress: progress}); - }); - img.onload = function onload(imgid) { + this.setState({progress: progress}); + }.bind(this)); + img.onload = (function onload(imgid) { return function onloadReturn() { - var loaded = self.state.loaded; + var loaded = this.state.loaded; loaded[imgid] = true; - self.setState({loaded: loaded}); - $(self.refs.image.getDOMNode()).css('max-height', imgHeight); - }; - }(id); + this.setState({loaded: loaded}); + $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight); + }.bind(this); + }.bind(this)(id)); var images = this.state.images; images[id] = img; this.setState({images: images}); @@ -80,52 +91,51 @@ module.exports = React.createClass({ loaded[id] = true; this.setState({loaded: loaded}); } - }, - componentDidUpdate: function() { + } + componentDidUpdate() { if (this.state.loaded[this.state.imgId]) { if (this.refs.imageWrap) { - $(this.refs.imageWrap.getDOMNode()).removeClass('default'); + $(React.findDOMNode(this.refs.imageWrap)).removeClass('default'); } } - }, - componentDidMount: function() { - var self = this; + } + componentDidMount() { $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() { - self.setState({viewed: true}); - self.loadImage(self.state.imgId); - }); + this.setState({viewed: true}); + this.loadImage(this.state.imgId); + }.bind(this)); - $(this.refs.modal.getDOMNode()).click(function onModalClick(e) { - if (e.target === this || e.target === self.refs.imageBody.getDOMNode()) { + $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) { + if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) { $('.image_modal').modal('hide'); } - }); + }.bind(this)); - $(this.refs.imageWrap.getDOMNode()).hover( + $(React.findDOMNode(this.refs.imageWrap)).hover( function onModalHover() { - $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function offModalHover() { - $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); - } + $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); + }.bind(this), function offModalHover() { + $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); + }.bind(this) ); if (this.refs.previewArrowLeft) { - $(this.refs.previewArrowLeft.getDOMNode()).hover( + $(React.findDOMNode(this.refs.previewArrowLeft)).hover( function onModalHover() { - $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function offModalHover() { - $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); - } + $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); + }.bind(this), function offModalHover() { + $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); + }.bind(this) ); } if (this.refs.previewArrowRight) { - $(this.refs.previewArrowRight.getDOMNode()).hover( + $(React.findDOMNode(this.refs.previewArrowRight)).hover( function onModalHover() { - $(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); - }, function offModalHover() { - $(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); - } + $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show'); + }.bind(this), function offModalHover() { + $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show'); + }.bind(this) ); } @@ -133,90 +143,93 @@ module.exports = React.createClass({ // keep track of whether or not this component is mounted so we can safely set the state asynchronously this.canSetState = true; - }, - componentWillUnmount: function() { + } + componentWillUnmount() { this.canSetState = false; $(window).off('keyup', this.handleKeyPress); - }, - getPublicLink: function() { + } + getPublicLink() { var data = {}; data.channel_id = this.props.channelId; data.user_id = this.props.userId; data.filename = this.props.filenames[this.state.imgId]; Client.getPublicLink(data, function sucess(serverData) { - if (utils.isMobile()) { + if (Utils.isMobile()) { window.location.href = serverData.public_link; } else { window.open(serverData.public_link); } }, - function error() { - } + function error() {} ); - }, - getPreviewImagePath: function(filename) { + } + getPreviewImagePath(filename) { // Returns the path to a preview image that can be used to represent a file. - var fileInfo = utils.splitFileLocation(filename); - var fileType = utils.getFileType(fileInfo.ext); + var fileInfo = Utils.splitFileLocation(filename); + var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { // This is a temporary patch to fix issue with old files using absolute paths if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) { fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1]; } - fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; + fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; return fileInfo.path + '_preview.jpg'; } // only images have proper previews, so just use a placeholder icon for non-images - return utils.getPreviewImagePathForFileType(fileType); - }, - getInitialState: function() { - var loaded = []; - var progress = []; - for (var i = 0; i < this.props.filenames.length; i++) { - loaded.push(false); - progress.push(0); - } - return {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}}; - }, - render: function() { + return Utils.getPreviewImagePathForFileType(fileType); + } + render() { if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) { return <div/>; } var filename = this.props.filenames[this.state.imgId]; - var fileUrl = utils.getFileUrl(filename); + var fileUrl = Utils.getFileUrl(filename); - var name = decodeURIComponent(utils.getFileName(filename)); + var name = decodeURIComponent(Utils.getFileName(filename)); var content; var bgClass = ''; if (this.state.loaded[this.state.imgId]) { - var fileInfo = utils.splitFileLocation(filename); - var fileType = utils.getFileType(fileInfo.ext); + var fileInfo = Utils.splitFileLocation(filename); + var fileType = Utils.getFileType(fileInfo.ext); if (fileType === 'image') { // image files just show a preview of the file content = ( - <a href={fileUrl} target='_blank'> - <img ref='image' src={this.getPreviewImagePath(filename)}/> + <a + href={fileUrl} + target='_blank' + > + <img + ref='image' + src={this.getPreviewImagePath(filename)} + /> </a> ); } else { // non-image files include a section providing details about the file var infoString = 'File type ' + fileInfo.ext.toUpperCase(); if (this.state.fileSizes[filename] && this.state.fileSizes[filename] >= 0) { - infoString += ', Size ' + utils.fileSizeToString(this.state.fileSizes[filename]); + infoString += ', Size ' + Utils.fileSizeToString(this.state.fileSizes[filename]); } content = ( <div className='file-details__container'> - <a className={'file-details__preview'} href={fileUrl} target='_blank'> + <a + className={'file-details__preview'} + href={fileUrl} + target='_blank' + > <span className='file-details__preview-helper' /> - <img ref='image' src={this.getPreviewImagePath(filename)} /> + <img + ref='image' + src={this.getPreviewImagePath(filename)} + /> </a> <div className='file-details'> <div className='file-details__name'>{name}</div> @@ -228,19 +241,16 @@ module.exports = React.createClass({ // asynchronously request the actual size of this file if (!(filename in this.state.fileSizes)) { - var self = this; - Client.getFileInfo( filename, - function(data) { - if (self.canSetState) { - var fileSizes = self.state.fileSizes; - fileSizes[filename] = parseInt(data["size"], 10); - self.setState(fileSizes); + function success(data) { + if (this.canSetState) { + var fileSizes = this.state.fileSizes; + fileSizes[filename] = parseInt(data.size, 10); + this.setState(fileSizes); } - }, - function(err) { - } + }.bind(this), + function fail() {} ); } } @@ -250,14 +260,22 @@ module.exports = React.createClass({ if (percentage) { content = ( <div> - <img className='loader-image' src='/static/images/load.gif' /> - <span className='loader-percent' >{'Previewing ' + percentage + '%'}</span> + <img + className='loader-image' + src='/static/images/load.gif' + /> + <span className='loader-percent'> + {'Previewing ' + percentage + '%'} + </span> </div> ); } else { content = ( <div> - <img className='loader-image' src='/static/images/load.gif' /> + <img + className='loader-image' + src='/static/images/load.gif' + /> </div> ); } @@ -268,7 +286,14 @@ module.exports = React.createClass({ if (config.AllowPublicLink) { publicLink = ( <div> - <a href='#' className='public-link text' data-title='Public Image' onClick={this.getPublicLink}>Get Public Link</a> + <a + href='#' + className='public-link text' + data-title='Public Image' + onClick={this.getPublicLink} + > + Get Public Link + </a> <span className='text'> | </span> </div> ); @@ -282,7 +307,8 @@ module.exports = React.createClass({ ref='previewArrowLeft' className='modal-prev-bar' href='#' - onClick={this.handlePrev}> + onClick={this.handlePrev} + > <i className='image-control image-prev'/> </a> ); @@ -292,25 +318,51 @@ module.exports = React.createClass({ ref='previewArrowRight' className='modal-next-bar' href='#' - onClick={this.handleNext}> + onClick={this.handleNext} + > <i className='image-control image-next'/> </a> ); } return ( - <div className='modal fade image_modal' ref='modal' id={this.props.modalId} tabIndex='-1' role='dialog' aria-hidden='true'> + <div + className='modal fade image_modal' + ref='modal' + id={this.props.modalId} + tabIndex='-1' + role='dialog' + aria-hidden='true' + > <div className='modal-dialog modal-image'> <div className='modal-content image-content'> - <div ref='imageBody' className='modal-body image-body'> - <div ref='imageWrap' className={'image-wrapper default ' + bgClass}> - <div className='modal-close' data-dismiss='modal'></div> + <div + ref='imageBody' + className='modal-body image-body' + > + <div + ref='imageWrap' + className={'image-wrapper default ' + bgClass} + > + <div + className='modal-close' + data-dismiss='modal' + /> {content} - <div ref='imageFooter' className='modal-button-bar'> - <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span> + <div + ref='imageFooter' + className='modal-button-bar' + > + <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span> <div className='image-links'> {publicLink} - <a href={fileUrl} download={name} className='text'>Download</a> + <a + href={fileUrl} + download={name} + className='text' + > + Download + </a> </div> </div> </div> @@ -322,4 +374,19 @@ module.exports = React.createClass({ </div> ); } -}); +} + +ViewImageModal.defaultProps = { + filenames: [], + modalId: '', + channelId: '', + userId: '', + startId: 0 +}; +ViewImageModal.propTypes = { + filenames: React.PropTypes.array, + modalId: React.PropTypes.string, + channelId: React.PropTypes.string, + userId: React.PropTypes.string, + startId: React.PropTypes.number +}; |