diff options
Diffstat (limited to 'web/react/components')
25 files changed, 2087 insertions, 1234 deletions
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 1192a72bc..2a83b3c40 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -1,102 +1,104 @@ // 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] = ( @@ -104,33 +106,62 @@ module.exports = React.createClass({ <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 +170,4 @@ module.exports = React.createClass({ </div> ); } -}); +} diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 0254d0e82..87b9cab04 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,139 +1,85 @@ // 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 handleLeaveSuccess() { + const townsquare = ChannelStore.getByName('town-square'); + Utils.switchChannel(townsquare); }, - function(err) { + 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 +95,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 +121,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> - : - <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 + className='dropdown-menu' + role='menu' + aria-labelledby='channel_header_dropdown' + > + {dropdownContents} </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 +366,4 @@ module.exports = React.createClass({ </table> ); } -}); +} 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/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..ce4ebac9e 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -1,33 +1,68 @@ // 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 + }; + } + 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 +71,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 +90,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 +101,26 @@ 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) { + Client.createPost(post, channel, + function handlePostSuccess(data) { this.resizePostHolder(); 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); @@ -95,8 +130,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) { @@ -113,83 +148,83 @@ module.exports = React.createClass({ }.bind(this) ); } - }, - componentDidUpdate: function() { + } + componentDidUpdate() { 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) { + } + handleUserInput(messageText) { this.resizePostHolder(); 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,12 +285,12 @@ 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 @@ -281,13 +300,18 @@ module.exports = React.createClass({ ); } - 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 @@ -311,10 +335,13 @@ module.exports = React.createClass({ {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/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/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/login.jsx b/web/react/components/login.jsx index 28dd64c39..f87e77ff7 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -1,11 +1,11 @@ // 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'); export default class Login extends React.Component { constructor(props) { @@ -17,23 +17,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 +49,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 +73,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 +110,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 +163,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_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/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_body.jsx b/web/react/components/post_body.jsx index e5ab5b624..88fb9aec8 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -1,95 +1,140 @@ // 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'); +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'); -module.exports = React.createClass({ - componentWillReceiveProps: function(nextProps) { - var linkData = utils.extractLinks(nextProps.post.message); +export default class PostBody extends React.Component { + constructor(props) { + super(props); + + const linkData = Utils.extractLinks(this.props.post.message); + this.state = {links: linkData.links, message: linkData.text}; + } + 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); + } + render() { + const post = this.props.post; + const filenames = this.props.post.filenames; + const parentPost = this.props.parentPost; + const inner = Utils.textToJsx(this.state.message); - var comment = ''; - var reply = ''; - var postClass = ''; + 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/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_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/setting_item_min.jsx b/web/react/components/setting_item_min.jsx index 3c87e416e..098729a4f 100644 --- a/web/react/components/setting_item_min.jsx +++ b/web/react/components/setting_item_min.jsx @@ -1,19 +1,23 @@ // 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} + > + Edit + </a> + </li> + ); } + return ( <ul className='section-min'> <li className='col-sm-10 section-title'>{this.props.title}</li> @@ -22,4 +26,11 @@ module.exports = React.createClass({ </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/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/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/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index fd2a22731..2966a8a9a 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -1,11 +1,11 @@ // 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'); export default class GeneralTab extends React.Component { constructor(props) { @@ -21,10 +21,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 +41,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 +84,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 +93,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 +122,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 +135,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_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index 7e65e8cab..668bf76cf 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -1,70 +1,96 @@ // 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: '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_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_url_page.jsx b/web/react/components/team_signup_url_page.jsx index beef725e2..2ea6c3680 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -1,33 +1,37 @@ // 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'); + +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 +40,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) { + for (let 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'}); return; } } - client.findTeamByName(name, + Client.findTeamByName(name, function success(data) { if (!data) { if (config.AllowSignupDomainsWizard) { @@ -65,55 +69,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_with_email.jsx b/web/react/components/team_signup_with_email.jsx index c7204880f..c0bbb7da9 100644 --- a/web/react/components/team_signup_with_email.jsx +++ b/web/react/components/team_signup_with_email.jsx @@ -1,8 +1,8 @@ // 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'); export default class EmailSignUpPage extends React.Component { constructor() { @@ -14,11 +14,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 +30,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 +69,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/textbox.jsx b/web/react/components/textbox.jsx index efd2dd810..0408a262d 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,35 @@ 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); + }.bind(this), 1); + } + handleChange() { + this.props.onUserInput(React.findDOMNode(this.refs.message).value); this.resize(); - }, - handleKeyPress: function(e) { - var text = this.refs.message.getDOMNode().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 +147,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 +158,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 +178,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 +200,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 +210,43 @@ 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); - 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,62 @@ 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(); + } + 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, + createMessage: React.PropTypes.string.isRequired +}; diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings_general.jsx index ddd2fb607..df4d02820 100644 --- a/web/react/components/user_settings_general.jsx +++ b/web/react/components/user_settings_general.jsx @@ -194,7 +194,7 @@ export default class UserSettingsGeneralTab extends React.Component { this.props.updateSection(section); } handleClose() { - $(this.getDOMNode()).find('.form-control').each(function clearForms() { + $(React.findDOMNode(this)).find('.form-control').each(function clearForms() { this.value = ''; }); |