diff options
Diffstat (limited to 'web/react')
45 files changed, 1244 insertions, 1030 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index f0a31ce90..27959ec7e 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +var Modal = ReactBootstrap.Modal; var UserStore = require('../stores/user_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); @@ -30,16 +31,23 @@ export default class AccessHistoryModal extends React.Component { } onShow() { AsyncClient.getAudits(); + + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } } onHide() { - $('#user_settings').modal('show'); this.setState({moreInfo: []}); + this.props.onModalDismissed(); } componentDidMount() { UserStore.addAuditsChangeListener(this.onAuditChange); - $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide); + } + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } } componentWillUnmount() { UserStore.removeAuditsChangeListener(this.onAuditChange); @@ -380,43 +388,23 @@ export default class AccessHistoryModal extends React.Component { } return ( - <div> - <div - className='modal fade' - ref='modal' - id='access-history' - tabIndex='-1' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog modal-lg'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>{'×'}</span> - </button> - <h4 - className='modal-title' - id='myModalLabel' - > - {'Access History'} - </h4> - </div> - <div - ref='modalBody' - className='modal-body' - > - {content} - </div> - </div> - </div> - </div> - </div> + <Modal + show={this.props.show} + onHide={this.onHide} + bsSize='large' + > + <Modal.Header closeButton={true}> + <Modal.Title>{'Access History'}</Modal.Title> + </Modal.Header> + <Modal.Body ref='modalBody'> + {content} + </Modal.Body> + </Modal> ); } } + +AccessHistoryModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 2c944913f..ef3077470 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -4,6 +4,7 @@ const UserStore = require('../stores/user_store.jsx'); const Client = require('../utils/client.jsx'); const AsyncClient = require('../utils/async_client.jsx'); +const Modal = ReactBootstrap.Modal; const LoadingScreen = require('./loading_screen.jsx'); const Utils = require('../utils/utils.jsx'); @@ -49,16 +50,23 @@ export default class ActivityLogModal extends React.Component { } onShow() { AsyncClient.getSessions(); + + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } } onHide() { - $('#user_settings').modal('show'); this.setState({moreInfo: []}); + this.props.onModalDismissed(); } componentDidMount() { UserStore.addSessionsChangeListener(this.onListenerChange); - $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.onShow); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide); + } + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } } componentWillUnmount() { UserStore.removeSessionsChangeListener(this.onListenerChange); @@ -151,44 +159,24 @@ export default class ActivityLogModal extends React.Component { } return ( - <div> - <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> - </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' - > - {content} - </div> - </div> - </div> - </div> - </div> + <Modal + show={this.props.show} + onHide={this.onHide} + bsSize='large' + > + <Modal.Header closeButton={true}> + <Modal.Title>{'Active Sessions'}</Modal.Title> + </Modal.Header> + <Modal.Body ref='modalBody'> + <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> + {content} + </Modal.Body> + </Modal> ); } } + +ActivityLogModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index f7e92672d..2badaf0e5 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -212,50 +212,52 @@ export default class UserItem extends React.Component { } return ( - <div className='row member-div'> - <img - className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} - 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} - {makeSystemAdmin} - <li role='presentation'> - <a - role='menuitem' - href='#' - onClick={this.handleResetPassword} - > - {'Reset Password'} - </a> - </li> - </ul> - </div> - {serverError} - </div> + <tr> + <td className='row member-div'> + <img + className='post-profile-img pull-left' + src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} + 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} + {makeSystemAdmin} + <li role='presentation'> + <a + role='menuitem' + href='#' + onClick={this.handleResetPassword} + > + {'Reset Password'} + </a> + </li> + </ul> + </div> + {serverError} + </td> + </tr> ); } } diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx index 242c2c637..3ee40bb86 100644 --- a/web/react/components/center_panel.jsx +++ b/web/react/components/center_panel.jsx @@ -21,7 +21,7 @@ export default class CenterPanel extends React.Component { this.onPreferenceChange = this.onPreferenceChange.bind(this); - const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS}; } componentDidMount() { @@ -31,7 +31,7 @@ export default class CenterPanel extends React.Component { PreferenceStore.removeChangeListener(this.onPreferenceChange); } onPreferenceChange() { - const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS}); } render() { @@ -61,9 +61,7 @@ export default class CenterPanel extends React.Component { <div id='channel-header'> <ChannelHeader /> </div> - <div id='post-list'> - {postsContainer} - </div> + {postsContainer} <div className='post-create__container' id='post-create' diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 20f106f30..895dc5fe4 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,20 +1,23 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -const ChannelStore = require('../stores/channel_store.jsx'); -const UserStore = require('../stores/user_store.jsx'); -const SearchStore = require('../stores/search_store.jsx'); -const PreferenceStore = require('../stores/preference_store.jsx'); const NavbarSearchBox = require('./search_bar.jsx'); -const AsyncClient = require('../utils/async_client.jsx'); -const Client = require('../utils/client.jsx'); -const TextFormatting = require('../utils/text_formatting.jsx'); -const Utils = require('../utils/utils.jsx'); const MessageWrapper = require('./message_wrapper.jsx'); const PopoverListMembers = require('./popover_list_members.jsx'); const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); +const ChannelMembersModal = require('./channel_members_modal.jsx'); + +const ChannelStore = require('../stores/channel_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const SearchStore = require('../stores/search_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Utils = require('../utils/utils.jsx'); +const TextFormatting = require('../utils/text_formatting.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -31,6 +34,8 @@ export default class ChannelHeader extends React.Component { const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; + state.showInviteModal = false; + state.showMembersModal = false; this.state = state; } getStateFromStores() { @@ -86,7 +91,7 @@ export default class ChannelHeader extends React.Component { let terms = ''; if (user.notify_props && user.notify_props.mention_keys) { - let termKeys = UserStore.getCurrentMentionKeys(); + const termKeys = UserStore.getCurrentMentionKeys(); if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) { termKeys.splice(termKeys.indexOf('@all'), 1); } @@ -146,7 +151,7 @@ export default class ChannelHeader extends React.Component { channelTerm = 'Group'; } - let dropdownContents = []; + const dropdownContents = []; if (isDirect) { dropdownContents.push( <li @@ -162,7 +167,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Header... + {'Set Channel Header...'} </a> </li> ); @@ -179,7 +184,7 @@ export default class ChannelHeader extends React.Component { data-channelid={channel.id} href='#' > - View Info + {'View Info'} </a> </li> ); @@ -192,11 +197,10 @@ export default class ChannelHeader extends React.Component { > <a role='menuitem' - data-toggle='modal' - data-target='#channel_invite' href='#' + onClick={() => this.setState({showInviteModal: true})} > - Add Members + {'Add Members'} </a> </li> ); @@ -209,11 +213,10 @@ export default class ChannelHeader extends React.Component { > <a role='menuitem' - data-toggle='modal' - data-target='#channel_members' href='#' + onClick={() => this.setState({showMembersModal: true})} > - Manage Members + {'Manage Members'} </a> </li> ); @@ -234,7 +237,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set {channelTerm} Header... + {'Set '}{channelTerm}{' Header...'} </a> </li> ); @@ -248,7 +251,7 @@ export default class ChannelHeader extends React.Component { href='#' onClick={() => this.setState({showEditChannelPurposeModal: true})} > - Set {channelTerm} Purpose... + {'Set '}{channelTerm}{' Purpose...'} </a> </li> ); @@ -265,7 +268,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Notification Preferences + {'Notification Preferences'} </a> </li> ); @@ -286,7 +289,7 @@ export default class ChannelHeader extends React.Component { data-name={channel.name} data-channelid={channel.id} > - Rename {channelTerm}... + {'Rename '}{channelTerm}{'...'} </a> </li> ); @@ -303,7 +306,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Delete {channelTerm}... + {'Delete '}{channelTerm}{'...'} </a> </li> ); @@ -319,7 +322,7 @@ export default class ChannelHeader extends React.Component { href='#' onClick={this.handleLeave} > - Leave {channelTerm} + {'Leave '}{channelTerm} </a> </li> ); @@ -397,7 +400,7 @@ export default class ChannelHeader extends React.Component { href='#' onClick={this.searchMentions} > - Recent Mentions + {'Recent Mentions'} </a> </li> </ul> @@ -411,6 +414,14 @@ export default class ChannelHeader extends React.Component { onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} channel={channel} /> + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> + <ChannelMembersModal + show={this.state.showMembersModal} + onModalDismissed={() => this.setState({showMembersModal: false})} + /> </div> ); } diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index e90d1a666..7c1032321 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -1,26 +1,25 @@ // Copyright (c) 2015 Mattermost, 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 MemberList = require('./member_list.jsx'); -var LoadingScreen = require('./loading_screen.jsx'); -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); +const MemberList = require('./member_list.jsx'); +const LoadingScreen = require('./loading_screen.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); + +const Utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); + +const Modal = ReactBootstrap.Modal; export default class ChannelInviteModal extends React.Component { constructor() { super(); - this.componentDidMount = this.componentDidMount.bind(this); - this.componentWillUnmount = this.componentWillUnmount.bind(this); - this.onShow = this.onShow.bind(this); - this.onHide = this.onHide.bind(this); this.onListenerChange = this.onListenerChange.bind(this); this.handleInvite = this.handleInvite.bind(this); - this.isShown = false; this.state = this.getStateFromStores(); } getStateFromStores() { @@ -39,7 +38,7 @@ export default class ChannelInviteModal extends React.Component { } } - nonmembers.sort(function sortByUsername(a, b) { + nonmembers.sort((a, b) => { return a.username.localeCompare(b.username); }); @@ -49,35 +48,37 @@ export default class ChannelInviteModal extends React.Component { } return { - nonmembers: nonmembers, - memberIds: memberIds, - channelName: channelName, - loading: loading + nonmembers, + memberIds, + channelName, + loading }; } - componentDidMount() { - $(ReactDOM.findDOMNode(this)).on('hidden.bs.modal', this.onHide); - $(ReactDOM.findDOMNode(this)).on('show.bs.modal', this.onShow); - - ChannelStore.addExtraInfoChangeListener(this.onListenerChange); - ChannelStore.addChangeListener(this.onListenerChange); - UserStore.addChangeListener(this.onListenerChange); - } - componentWillUnmount() { - ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); - ChannelStore.removeChangeListener(this.onListenerChange); - UserStore.removeChangeListener(this.onListenerChange); - } onShow() { - this.isShown = true; - this.onListenerChange(); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } } - onHide() { - this.isShown = false; + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + ChannelStore.addChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); + this.onListenerChange(); + } else if (this.props.show && !nextProps.show) { + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + ChannelStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); + } } onListenerChange() { var newState = this.getStateFromStores(); - if (!utils.areStatesEqual(this.state, newState) && this.isShown) { + if (!Utils.areStatesEqual(this.state, newState)) { this.setState(newState); } } @@ -90,8 +91,8 @@ export default class ChannelInviteModal extends React.Component { var data = {}; data.user_id = userId; - client.addChannelMember(ChannelStore.getCurrentId(), data, - function sucess() { + Client.addChannelMember(ChannelStore.getCurrentId(), data, + () => { var nonmembers = this.state.nonmembers; var memberIds = this.state.memberIds; @@ -103,16 +104,20 @@ export default class ChannelInviteModal extends React.Component { } } - this.setState({inviteError: null, memberIds: memberIds, nonmembers: nonmembers}); + this.setState({inviteError: null, memberIds, nonmembers}); AsyncClient.getChannelExtraInfo(true); - }.bind(this), - - function error(err) { + }, + (err) => { this.setState({inviteError: err.message}); - }.bind(this) + } ); } render() { + var maxHeight = 1000; + if (Utils.windowHeight() <= 1200) { + maxHeight = Utils.windowHeight() - 300; + } + var inviteError = null; if (this.state.inviteError) { inviteError = (<label className='has-error control-label'>{this.state.inviteError}</label>); @@ -121,7 +126,7 @@ export default class ChannelInviteModal extends React.Component { var currentMember = ChannelStore.getCurrentMember(); var isAdmin = false; if (currentMember) { - isAdmin = utils.isAdmin(currentMember.roles) || utils.isAdmin(UserStore.getCurrentUser().roles); + isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles); } var content; @@ -138,43 +143,36 @@ export default class ChannelInviteModal extends React.Component { } return ( - <div - className='modal fade more-modal' - id='channel_invite' - tabIndex='-1' - role='dialog' - aria-hidden='true' + <Modal + dialogClassName='more-modal' + show={this.props.show} + onHide={this.props.onModalDismissed} > - <div - className='modal-dialog' - role='document' - > - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>×</span> - </button> - <h4 className='modal-title'>Add New Members to <span className='name'>{this.state.channelName}</span></h4> - </div> - <div className='modal-body'> + <Modal.Header closeButton={true}> + <Modal.Title>{'Add New Members to '}<span className='name'>{this.state.channelName}</span></Modal.Title> + </Modal.Header> + <Modal.Body + ref='modalBody' + style={{maxHeight}} + > {inviteError} {content} - </div> - <div className='modal-footer'> + </Modal.Body> + <Modal.Footer> <button type='button' className='btn btn-default' - data-dismiss='modal' - >Close</button> - </div> - </div> - </div> - </div> + onClick={this.props.onModalDismissed} + > + {'Close'} + </button> + </Modal.Footer> + </Modal> ); } } + +ChannelInviteModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx deleted file mode 100644 index d0ea7278b..000000000 --- a/web/react/components/channel_members.jsx +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -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.onHide = this.onHide.bind(this); - this.onShow = this.onShow.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]); - } - } - } - - function compareByUsername(a, b) { - if (a.username < b.username) { - return -1; - } else if (a.username > b.username) { - return 1; - } - - return 0; - } - - memberList.sort(compareByUsername); - nonmemberList.sort(compareByUsername); - - const channel = ChannelStore.getCurrent(); - let channelName = ''; - if (channel) { - channelName = channel.display_name; - } - - return { - nonmemberList: nonmemberList, - memberList: memberList, - channelName: channelName - }; - } - onHide() { - this.setState({renderMembers: false}); - } - onShow() { - this.setState({renderMembers: true}); - } - componentDidMount() { - ChannelStore.addExtraInfoChangeListener(this.onChange); - ChannelStore.addChangeListener(this.onChange); - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); - } - componentWillUnmount() { - ChannelStore.removeExtraInfoChangeListener(this.onChange); - ChannelStore.removeChangeListener(this.onChange); - } - onChange() { - const newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(this.state, newState)) { - this.setState(newState); - } - } - handleRemove(userId) { - // Make sure the user is a member of the channel - 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; - } - - let data = {}; - data.user_id = userId; - - 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; - } - } - - let nonmemberList = this.state.nonmemberList; - if (oldMember) { - nonmemberList.push(oldMember); - } - - this.setState({memberList: memberList, nonmemberList: nonmemberList}); - AsyncClient.getChannelExtraInfo(true); - }.bind(this), - function handleRemoveError(err) { - this.setState({inviteError: err.message}); - }.bind(this) - ); - } - render() { - const currentMember = ChannelStore.getCurrentMember(); - let isAdmin = false; - if (currentMember) { - isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles); - } - - var memberList = null; - if (this.state.renderMembers) { - memberList = ( - <MemberList - memberList={this.state.memberList} - isAdmin={isAdmin} - handleRemove={this.handleRemove} - /> - ); - } - - return ( - <div - className='modal fade more-modal' - 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' - > - {memberList} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - Close - </button> - </div> - </div> - </div> - </div> - ); - } -} diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx new file mode 100644 index 000000000..2fa7ae8ff --- /dev/null +++ b/web/react/components/channel_members_modal.jsx @@ -0,0 +1,210 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const MemberList = require('./member_list.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); + +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); +const Utils = require('../utils/utils.jsx'); + +const Modal = ReactBootstrap.Modal; + +export default class ChannelMembersModal 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); + + const state = this.getStateFromStores(); + state.showInviteModal = false; + this.state = state; + } + getStateFromStores() { + const users = UserStore.getActiveOnlyProfiles(); + const memberList = ChannelStore.getCurrentExtraInfo().members; + + const nonmemberList = []; + for (const 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]); + } + } + } + + function compareByUsername(a, b) { + if (a.username < b.username) { + return -1; + } else if (a.username > b.username) { + return 1; + } + + return 0; + } + + memberList.sort(compareByUsername); + nonmemberList.sort(compareByUsername); + + const channel = ChannelStore.getCurrent(); + let channelName = ''; + if (channel) { + channelName = channel.display_name; + } + + return { + nonmemberList, + memberList, + channelName + }; + } + onShow() { + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } + } + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + ChannelStore.addExtraInfoChangeListener(this.onChange); + ChannelStore.addChangeListener(this.onChange); + } else if (this.props.show && !nextProps.show) { + ChannelStore.removeExtraInfoChangeListener(this.onChange); + ChannelStore.removeChangeListener(this.onChange); + } + } + onChange() { + const newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(this.state, newState)) { + this.setState(newState); + } + } + handleRemove(userId) { + // Make sure the user is a member of the channel + const 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; + } + + const data = {}; + data.user_id = userId; + + Client.removeChannelMember(ChannelStore.getCurrentId(), data, + () => { + let oldMember; + for (let i = 0; i < memberList.length; i++) { + if (userId === memberList[i].id) { + oldMember = memberList[i]; + memberList.splice(i, 1); + break; + } + } + + const nonmemberList = this.state.nonmemberList; + if (oldMember) { + nonmemberList.push(oldMember); + } + + this.setState({memberList, nonmemberList}); + AsyncClient.getChannelExtraInfo(true); + }, + (err) => { + this.setState({inviteError: err.message}); + } + ); + } + render() { + var maxHeight = 1000; + if (Utils.windowHeight() <= 1200) { + maxHeight = Utils.windowHeight() - 300; + } + + const currentMember = ChannelStore.getCurrentMember(); + let isAdmin = false; + if (currentMember) { + isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles); + } + + return ( + <div> + <Modal + dialogClassName='more-modal' + show={this.props.show} + onHide={this.props.onModalDismissed} + > + <Modal.Header closeButton={true}> + <Modal.Title><span className='name'>{this.state.channelName}</span>{' Members'}</Modal.Title> + <a + className='btn btn-md btn-primary' + href='#' + onClick={() => { + this.setState({showInviteModal: true}); + this.props.onModalDismissed(); + }} + > + <i className='glyphicon glyphicon-envelope'/>{' Add New Members'} + </a> + </Modal.Header> + <Modal.Body + ref='modalBody' + style={{maxHeight}} + > + <div className='team-member-list'> + <MemberList + memberList={this.state.memberList} + isAdmin={isAdmin} + handleRemove={this.handleRemove} + /> + </div> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.props.onModalDismissed} + > + {'Close'} + </button> + </Modal.Footer> + </Modal> + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> + </div> + ); + } +} + +ChannelMembersModal.defaultProps = { + show: false +}; + +ChannelMembersModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/confirm_modal.jsx b/web/react/components/confirm_modal.jsx index 12002f33f..cdef1c1ea 100644 --- a/web/react/components/confirm_modal.jsx +++ b/web/react/components/confirm_modal.jsx @@ -1,70 +1,63 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +const Modal = ReactBootstrap.Modal; + export default class ConfirmModal extends React.Component { constructor(props) { super(props); this.handleConfirm = this.handleConfirm.bind(this); - - this.state = {}; } + handleConfirm() { - $('#' + this.props.parent_id).attr('data-confirm', 'true'); - $('#' + this.props.parent_id).modal('hide'); - $('#' + this.props.id).modal('hide'); + this.props.onConfirm(); } + render() { return ( - <div - className='modal fade' - id={this.props.id} - tabIndex='-1' - role='dialog' - aria-hidden='true' + <Modal + className='modal-confirm' + show={this.props.show} + onHide={this.props.onCancel} > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <h4 className='modal-title'>{this.props.title}</h4> - </div> - <div className='modal-body'> - {this.props.message} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - Cancel - </button> - <button - onClick={this.handleConfirm} - type='button' - className='btn btn-primary' - > - {this.props.confirm_button} - </button> - </div> - </div> - </div> - </div> + <Modal.Header closeButton={false}> + <Modal.Title>{this.props.title}</Modal.Title> + </Modal.Header> + <Modal.Body> + {this.props.message} + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.props.onCancel} + > + {'Cancel'} + </button> + <button + type='button' + className='btn btn-primary' + onClick={this.props.onConfirm} + > + {this.props.confirm_button} + </button> + </Modal.Footer> + </Modal> ); } } ConfirmModal.defaultProps = { - parent_id: '', - id: '', title: '', message: '', confirm_button: '' }; ConfirmModal.propTypes = { - parent_id: React.PropTypes.string, - id: React.PropTypes.string, + show: React.PropTypes.bool.isRequired, title: React.PropTypes.string, message: React.PropTypes.string, - confirm_button: React.PropTypes.string + confirm_button: React.PropTypes.string, + onConfirm: React.PropTypes.func.isRequired, + onCancel: React.PropTypes.func.isRequired }; diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 1545cdfaa..5a69c9bfb 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -50,7 +50,7 @@ export default class CreatePost extends React.Component { PostStore.clearDraftUploads(); const draft = this.getCurrentDraft(); - const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); this.state = { channelId: ChannelStore.getCurrentId(), @@ -236,8 +236,6 @@ export default class CreatePost extends React.Component { PostStore.storeCurrentDraft(draft); } resizePostHolder() { - const height = this.state.windowHeight - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; - $('.post-list-holder-by-time').css('height', `${height}px`); if (this.state.windowWidth > 960) { $('#post_textbox').focus(); } @@ -338,7 +336,7 @@ export default class CreatePost extends React.Component { } } onPreferenceChange() { - const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); this.setState({ showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER, ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 5b3c74e82..2557a55ca 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -12,6 +12,7 @@ export default class EditChannelModal extends React.Component { this.handleUserInput = this.handleUserInput.bind(this); this.handleClose = this.handleClose.bind(this); this.onShow = this.onShow.bind(this); + this.handleShown = this.handleShown.bind(this); this.state = { header: '', @@ -55,9 +56,13 @@ export default class EditChannelModal extends React.Component { const button = e.relatedTarget; this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); } + handleShown() { + $('#edit_channel #edit_header').focus(); + } componentDidMount() { $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown); } componentWillUnmount() { $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); @@ -114,6 +119,7 @@ export default class EditChannelModal extends React.Component { <textarea className='form-control no-resize' rows='6' + id='edit_header' maxLength='1024' value={this.state.header} onChange={this.handleUserInput} diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx index 4d162cfe7..4cb96a3ff 100644 --- a/web/react/components/edit_channel_purpose_modal.jsx +++ b/web/react/components/edit_channel_purpose_modal.jsx @@ -15,6 +15,12 @@ export default class EditChannelPurposeModal extends React.Component { this.state = {serverError: ''}; } + componentDidUpdate() { + if (this.props.show) { + $(ReactDOM.findDOMNode(this.refs.purpose)).focus(); + } + } + handleHide() { this.setState({serverError: ''}); @@ -77,6 +83,7 @@ export default class EditChannelPurposeModal extends React.Component { return ( <Modal className='modal-edit-channel-purpose' + ref='modal' show={this.props.show} onHide={this.handleHide} > diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index bea700725..c09477a69 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -2,55 +2,50 @@ // See License.txt for license information. var utils = require('../utils/utils.jsx'); +var ActionTypes = require('../utils/constants.jsx').ActionTypes; +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Client = require('../utils/client.jsx'); +var ModalStore = require('../stores/modal_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); +const Modal = ReactBootstrap.Modal; + export default class InviteMemberModal extends React.Component { constructor(props) { super(props); + this.handleToggle = this.handleToggle.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.handleHide = this.handleHide.bind(this); this.addInviteFields = this.addInviteFields.bind(this); this.clearFields = this.clearFields.bind(this); this.removeInviteFields = this.removeInviteFields.bind(this); this.state = { + show: false, inviteIds: [0], idCount: 0, emailErrors: {}, firstNameErrors: {}, lastNameErrors: {}, - emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' + emailEnabled: global.window.mm_config.SendEmailNotifications === 'true', + showConfirmModal: false }; } componentDidMount() { - var self = this; - $('#invite_member').on('hide.bs.modal', function hide(e) { - if ($('#invite_member').attr('data-confirm') === 'true') { - $('#invite_member').attr('data-confirm', 'false'); - return; - } - - var notEmpty = false; - for (var i = 0; i < self.state.inviteIds.length; i++) { - var index = self.state.inviteIds[i]; - if (ReactDOM.findDOMNode(self.refs['email' + index]).value.trim() !== '') { - notEmpty = true; - break; - } - } + ModalStore.addModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle); + } - if (notEmpty) { - $('#confirm_invite_modal').modal('show'); - e.preventDefault(); - } - }); + componentWillUnmount() { + ModalStore.removeModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle); + } - $('#invite_member').on('hidden.bs.modal', function show() { - self.clearFields(); + handleToggle(value) { + this.setState({ + show: value }); } @@ -94,25 +89,57 @@ export default class InviteMemberModal extends React.Component { var data = {}; data.invites = invites; - Client.inviteMembers(data, - function success() { - $(ReactDOM.findDOMNode(this.refs.modal)).attr('data-confirm', 'true'); - $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); - }.bind(this), - function fail(err) { + Client.inviteMembers( + data, + () => { + this.handleHide(false); + }, + (err) => { if (err.message === 'This person is already on your team') { emailErrors[err.detailed_error] = err.message; this.setState({emailErrors: emailErrors}); } else { this.setState({serverError: err.message}); } - }.bind(this) + } ); } - componentDidUpdate() { - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200); - $(ReactDOM.findDOMNode(this.refs.modalBody)).css('overflow-y', 'scroll'); + handleHide(requireConfirm) { + if (requireConfirm) { + var notEmpty = false; + for (var i = 0; i < this.state.inviteIds.length; i++) { + var index = this.state.inviteIds[i]; + if (ReactDOM.findDOMNode(this.refs['email' + index]).value.trim() !== '') { + notEmpty = true; + break; + } + } + + if (notEmpty) { + this.setState({ + showConfirmModal: true + }); + + return; + } + } + + this.clearFields(); + + this.setState({ + show: false, + showConfirmModal: false + }); + } + + componentDidUpdate(prevProps, prevState) { + if (!prevState.show && this.state.show) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } + } } addInviteFields() { @@ -292,7 +319,7 @@ export default class InviteMemberModal extends React.Component { ); } else { var teamInviteLink = null; - if (currentUser && this.props.teamType === 'O') { + if (currentUser && TeamStore.getCurrent().type === 'O') { var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id; var link = ( @@ -302,11 +329,7 @@ export default class InviteMemberModal extends React.Component { data-target='#get_link' data-title='Team Invite' data-value={linkUrl} - onClick={ - function click() { - $('#invite_member').modal('hide'); - } - } + onClick={() => this.handleHide(this, false)} >Team Invite Link</a> ); @@ -327,64 +350,54 @@ export default class InviteMemberModal extends React.Component { return ( <div> - <div - className='modal fade' - ref='modal' - id='invite_member' - tabIndex='-1' - role='dialog' - aria-hidden='true' + <Modal + className='modal-invite-member' + show={this.state.show} + onHide={this.handleHide.bind(this, true)} + enforceFocus={!this.state.showConfirmModal} > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> + <Modal.Header closeButton={true}> + <Modal.Title>{'Invite New Member'}</Modal.Title> + </Modal.Header> + <Modal.Body ref='modalBody'> + <form role='form'> + {inviteSections} + </form> + {content} + </Modal.Body> + <Modal.Footer> <button type='button' - className='close' - data-dismiss='modal' - aria-label='Close' + className='btn btn-default' + onClick={this.handleHide.bind(this, true)} > - <span aria-hidden='true'>×</span> + {'Cancel'} </button> - <h4 - className='modal-title' - id='myModalLabel' - >Invite New Member</h4> - </div> - <div - ref='modalBody' - className='modal-body' - > - <form role='form'> - {inviteSections} - </form> - {content} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - >Cancel</button> - {sendButton} - </div> - </div> - </div> - </div> + {sendButton} + </Modal.Footer> + </Modal> <ConfirmModal - id='confirm_invite_modal' - parent_id='invite_member' title='Discard Invitations?' message='You have unsent invitations, are you sure you want to discard them?' confirm_button='Yes, Discard' + show={this.state.showConfirmModal} + onConfirm={this.handleHide.bind(this, false)} + onCancel={() => this.setState({showConfirmModal: false})} /> </div> ); } - return <div/>; + + return null; + } + + static show() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, + value: true + }); } } InviteMemberModal.propTypes = { - teamType: React.PropTypes.string }; diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx index 70eb0a500..0238c7920 100644 --- a/web/react/components/member_list.jsx +++ b/web/react/components/member_list.jsx @@ -15,28 +15,28 @@ export default class MemberList extends React.Component { members = this.props.memberList; } - var message = ''; + var message = null; if (members.length === 0) { - message = <span>No users to add.</span>; + message = <tr><td>No users to add.</td></tr>; } return ( <table className='table more-table member-list-holder'> <tbody> - {members.map(function mymembers(member) { - return ( - <MemberListItem - key={member.id} - member={member} - isAdmin={this.props.isAdmin} - handleInvite={this.props.handleInvite} - handleRemove={this.props.handleRemove} - handleMakeAdmin={this.props.handleMakeAdmin} - /> - ); - }, this)} + {members.map(function mymembers(member) { + return ( + <MemberListItem + key={member.id} + member={member} + isAdmin={this.props.isAdmin} + handleInvite={this.props.handleInvite} + handleRemove={this.props.handleRemove} + handleMakeAdmin={this.props.handleMakeAdmin} + /> + ); + }, this)} + {message} </tbody> - {message} </table> ); } diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 14db05cdb..1fa369068 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -166,40 +166,42 @@ export default class MemberListTeamItem extends React.Component { } return ( - <div className='row member-div'> - <img - className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`} - 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> + <tr> + <td className='row member-div'> + <img + className='post-profile-img pull-left' + src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`} + 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} + </td> + </tr> ); } } diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index f7778f25f..ff53816c7 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -1,22 +1,26 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); -var MessageWrapper = require('./message_wrapper.jsx'); -var NotifyCounts = require('./notify_counts.jsx'); const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); +const MessageWrapper = require('./message_wrapper.jsx'); +const NotifyCounts = require('./notify_counts.jsx'); +const ChannelMembersModal = require('./channel_members_modal.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); + +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); const Utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Popover = ReactBootstrap.Popover; -var OverlayTrigger = ReactBootstrap.OverlayTrigger; +const Popover = ReactBootstrap.Popover; +const OverlayTrigger = ReactBootstrap.OverlayTrigger; export default class Navbar extends React.Component { constructor(props) { @@ -29,6 +33,8 @@ export default class Navbar extends React.Component { const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; + state.showMembersModal = false; + state.showInviteModal = false; this.state = state; } getStateFromStores() { @@ -45,17 +51,18 @@ export default class Navbar extends React.Component { } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); + ChannelStore.removeExtraInfoChangeListener(this.onChange); } handleSubmit(e) { e.preventDefault(); } handleLeave() { Client.leaveChannel(this.state.channel.id, - function success() { + () => { AsyncClient.getChannels(true); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; }, - function error(err) { + (err) => { AsyncClient.dispatchError(err, 'handleLeave'); } ); @@ -104,7 +111,7 @@ export default class Navbar extends React.Component { data-channelid={channel.id} href='#' > - View Info + {'View Info'} </a> </li> ); @@ -120,7 +127,7 @@ export default class Navbar extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Header... + {'Set Channel Header...'} </a> </li> ); @@ -145,11 +152,10 @@ export default class Navbar extends React.Component { <li role='presentation'> <a role='menuitem' - data-toggle='modal' - data-target='#channel_invite' href='#' + onClick={() => this.setState({showInviteModal: false})} > - Add Members + {'Add Members'} </a> </li> ); @@ -161,7 +167,7 @@ export default class Navbar extends React.Component { href='#' onClick={this.handleLeave} > - Leave Channel + {'Leave Channel'} </a> </li> ); @@ -175,11 +181,10 @@ export default class Navbar extends React.Component { <li role='presentation'> <a role='menuitem' - data-toggle='modal' - data-target='#channel_members' href='#' + onClick={() => this.setState({showMembersModal: true})} > - Manage Members + {'Manage Members'} </a> </li> ); @@ -195,7 +200,7 @@ export default class Navbar extends React.Component { data-name={channel.name} data-channelid={channel.id} > - Rename Channel... + {'Rename Channel...'} </a> </li> ); @@ -210,7 +215,7 @@ export default class Navbar extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Delete Channel... + {'Delete Channel...'} </a> </li> ); @@ -228,7 +233,7 @@ export default class Navbar extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Notification Preferences + {'Notification Preferences'} </a> </li> ); @@ -299,7 +304,7 @@ export default class Navbar extends React.Component { data-toggle='collapse' data-target='#navbar-collapse-1' > - <span className='sr-only'>Toggle sidebar</span> + <span className='sr-only'>{'Toggle sidebar'}</span> <span className='icon-bar'></span> <span className='icon-bar'></span> <span className='icon-bar'></span> @@ -315,7 +320,7 @@ export default class Navbar extends React.Component { data-target='#sidebar-nav' onClick={this.toggleLeftSidebar} > - <span className='sr-only'>Toggle sidebar</span> + <span className='sr-only'>{'Toggle sidebar'}</span> <span className='icon-bar'></span> <span className='icon-bar'></span> <span className='icon-bar'></span> @@ -426,6 +431,14 @@ export default class Navbar extends React.Component { onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} channel={channel} /> + <ChannelMembersModal + show={this.state.showMembersModal} + onModalDismissed={() => this.setState({showMembersModal: false})} + /> + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> </div> ); } diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 029b9c137..0b755f377 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -7,6 +7,8 @@ var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var AboutBuildModal = require('./about_build_modal.jsx'); +var InviteMemberModal = require('./invite_member_modal.jsx'); +var UserSettingsModal = require('./user_settings/user_settings_modal.jsx'); var Constants = require('../utils/constants.jsx'); @@ -33,7 +35,10 @@ export default class NavbarDropdown extends React.Component { this.onListenerChange = this.onListenerChange.bind(this); this.aboutModalDismissed = this.aboutModalDismissed.bind(this); - this.state = getStateFromStores(); + const state = getStateFromStores(); + state.showUserSettingsModal = false; + state.showAboutModal = false; + this.state = state; } handleLogoutClick(e) { e.preventDefault(); @@ -88,8 +93,7 @@ export default class NavbarDropdown extends React.Component { <li> <a href='#' - data-toggle='modal' - data-target='#invite_member' + onClick={InviteMemberModal.show} > {'Invite New Member'} </a> @@ -210,8 +214,7 @@ export default class NavbarDropdown extends React.Component { <li> <a href='#' - data-toggle='modal' - data-target='#user_settings' + onClick={() => this.setState({showUserSettingsModal: true})} > {'Account Settings'} </a> @@ -256,6 +259,10 @@ export default class NavbarDropdown extends React.Component { {'About Mattermost'} </a> </li> + <UserSettingsModal + show={this.state.showUserSettingsModal} + onModalDismissed={() => this.setState({showUserSettingsModal: false})} + /> <AboutBuildModal show={this.state.showAboutModal} onModalDismissed={this.aboutModalDismissed} diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 7671ca01d..761664602 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -81,7 +81,7 @@ export default class PostsViewContainer extends React.Component { } } onChannelChange() { - const postLists = Object.assign({}, this.state.postLists); + const postLists = this.state.postLists.slice(); const channels = this.state.channels.slice(); const channelId = ChannelStore.getCurrentId(); @@ -112,7 +112,7 @@ export default class PostsViewContainer extends React.Component { postLists}); } onChannelLeave(id) { - const postLists = Object.assign({}, this.state.postLists); + const postLists = this.state.postLists.slice(); const channels = this.state.channels.slice(); const index = channels.indexOf(id); if (index !== -1) { @@ -123,7 +123,7 @@ export default class PostsViewContainer extends React.Component { } onPostsChange() { const channels = this.state.channels; - const postLists = Object.assign({}, this.state.postLists); + const postLists = this.state.postLists.slice(); const newPostsView = this.getChannelPosts(channels[this.state.currentChannelIndex]); postLists[this.state.currentChannelIndex] = newPostsView; @@ -261,7 +261,7 @@ export default class PostsViewContainer extends React.Component { } return ( - <div>{postListCtls}</div> + <div id='post-list'>{postListCtls}</div> ); } } diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index 80f0956f2..9fb3af035 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -16,6 +16,7 @@ export default class RenameChannelModal extends React.Component { this.displayNameKeyUp = this.displayNameKeyUp.bind(this); this.handleClose = this.handleClose.bind(this); this.handleShow = this.handleShow.bind(this); + this.handleShown = this.handleShown.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.state = { @@ -118,9 +119,13 @@ export default class RenameChannelModal extends React.Component { const button = $(e.relatedTarget); this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')}); } + handleShown() { + $('#rename_channel #display_name').focus(); + } componentDidMount() { $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.handleShow); $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown); } componentWillUnmount() { $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); @@ -176,6 +181,7 @@ export default class RenameChannelModal extends React.Component { onChange={this.onDisplayNameChange} type='text' ref='displayName' + id='display_name' className='form-control' placeholder='Enter display name' value={this.state.displayName} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index aab9919a4..f5ce5c10e 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -20,6 +20,7 @@ const Utils = require('../utils/utils.jsx'); const Constants = require('../utils/constants.jsx'); const Preferences = Constants.Preferences; const TutorialSteps = Constants.TutorialSteps; +const NotificationPrefs = Constants.NotificationPrefs; const Tooltip = ReactBootstrap.Tooltip; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -76,6 +77,8 @@ export default class Sidebar extends React.Component { if (ch.type === 'D') { chMentionCount = chUnreadCount; chUnreadCount = 0; + } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) { + chUnreadCount = 0; } channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount}; @@ -143,7 +146,7 @@ export default class Sidebar extends React.Component { visibleDirectChannels.sort(this.sortChannelsByDisplayName); - const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); return { activeId: currentChannelId, @@ -362,7 +365,7 @@ export default class Sidebar extends React.Component { var unread = false; if (channelMember) { msgCount = unreadCount.msgs + unreadCount.mentions; - unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0; + unread = msgCount > 0 || channelMember.mention_count > 0; } if (unread) { diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 46730e1e6..bc7f6ba50 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -31,7 +31,7 @@ export default class SidebarHeader extends React.Component { PreferenceStore.removeChangeListener(this.onPreferenceChange); } getStateFromStores() { - const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); return {showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.MENU_POPOVER}; } diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index 9350bbd42..2135e3ef3 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +var InviteMemberModal = require('./invite_member_modal.jsx'); +var UserSettingsModal = require('./user_settings/user_settings_modal.jsx'); var UserStore = require('../stores/user_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var client = require('../utils/client.jsx'); @@ -15,6 +17,10 @@ export default class SidebarRightMenu extends React.Component { super(props); this.handleLogoutClick = this.handleLogoutClick.bind(this); + + this.state = { + showUserSettingsModal: false + }; } handleLogoutClick(e) { @@ -38,10 +44,12 @@ export default class SidebarRightMenu extends React.Component { inviteLink = ( <li> - <a href='#' - data-toggle='modal' - data-target='#invite_member' - ><i className='glyphicon glyphicon-user'></i>Invite New Member</a> + <a + href='#' + onClick={InviteMemberModal.show} + > + <i className='glyphicon glyphicon-user'></i>Invite New Member + </a> </li> ); @@ -115,9 +123,11 @@ export default class SidebarRightMenu extends React.Component { <li> <a href='#' - data-toggle='modal' - data-target='#user_settings' - ><i className='glyphicon glyphicon-cog'></i>Account Settings</a></li> + onClick={() => this.setState({showUserSettingsModal: true})} + > + <i className='glyphicon glyphicon-cog'></i>Account Settings + </a> + </li> {teamSettingsLink} {inviteLink} {teamLink} @@ -141,6 +151,10 @@ export default class SidebarRightMenu extends React.Component { ><i className='glyphicon glyphicon-earphone'></i>Report a Problem</a></li> </ul> </div> + <UserSettingsModal + show={this.state.showUserSettingsModal} + onModalDismissed={() => this.setState({showUserSettingsModal: false})} + /> </div> ); } diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx index 33590c89a..ac1ebf52d 100644 --- a/web/react/components/team_members.jsx +++ b/web/react/components/team_members.jsx @@ -79,7 +79,7 @@ export default class TeamMembers extends React.Component { return ( <div - className='modal fade' + className='modal fade more-modal' ref='modal' id='team_members' tabIndex='-1' @@ -106,12 +106,10 @@ export default class TeamMembers extends React.Component { ref='modalBody' className='modal-body' > - <div className='channel-settings'> - <div className='team-member-list'> - {renderMembers} - </div> - {serverError} + <div className='team-member-list'> + {renderMembers} </div> + {serverError} </div> <div className='modal-footer'> <button diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index 17fe31c65..4d47db2a8 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -17,11 +17,13 @@ export default class TeamSettingsModal extends React.Component { }; } componentDidMount() { - $('body').on('click', '.modal-back', function handleBackClick() { + const modal = $(ReactDOM.findDOMNode(this.refs.modal)); + + modal.on('click', '.modal-back', function handleBackClick() { $(this).closest('.modal-dialog').removeClass('display--content'); $(this).closest('.modal-dialog').find('.settings-table .nav li.active').removeClass('active'); }); - $('body').on('click', '.modal-header .close', () => { + modal.on('click', '.modal-header .close', () => { setTimeout(() => { $('.modal-dialog.display--content').removeClass('display--content'); }, 500); diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx index c85acb346..3094b2f4c 100644 --- a/web/react/components/tutorial/tutorial_tip.jsx +++ b/web/react/components/tutorial/tutorial_tip.jsx @@ -98,7 +98,7 @@ export default class TutorialTip extends React.Component { <div className='tutorial__circles'>{dots}</div> <div className='text-right'> <button - className='btn btn-default' + className='btn btn-primary' onClick={this.handleNext} > {buttonText} diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx deleted file mode 100644 index eef4b24ba..000000000 --- a/web/react/components/user_settings/code_theme_chooser.jsx +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -var Constants = require('../../utils/constants.jsx'); - -export default class CodeThemeChooser extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - render() { - const theme = this.props.theme; - - const premadeThemes = []; - for (const k in Constants.CODE_THEMES) { - if (Constants.CODE_THEMES.hasOwnProperty(k)) { - let activeClass = ''; - if (k === theme.codeTheme) { - activeClass = 'active'; - } - - premadeThemes.push( - <div - className='col-xs-6 col-sm-3 premade-themes' - key={'premade-theme-key' + k} - > - <div - className={activeClass} - onClick={() => this.props.updateTheme(k)} - > - <label> - <img - className='img-responsive' - src={'/static/images/themes/code_themes/' + k + '.png'} - /> - <div className='theme-label'>{Constants.CODE_THEMES[k]}</div> - </label> - </div> - </div> - ); - } - } - - return ( - <div className='row'> - {premadeThemes} - </div> - ); - } -} - -CodeThemeChooser.propTypes = { - theme: React.PropTypes.object.isRequired, - updateTheme: React.PropTypes.func.isRequired -}; diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 095e5b622..895d0c500 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -55,28 +55,70 @@ export default class CustomThemeChooser extends React.Component { const elements = []; let colors = ''; Constants.THEME_ELEMENTS.forEach((element, index) => { - elements.push( - <div - className='col-sm-4 form-group' - key={'custom-theme-key' + index} - > - <label className='custom-label'>{element.uiName}</label> + if (element.id === 'codeTheme') { + const codeThemeOptions = []; + + element.themes.forEach((codeTheme, codeThemeIndex) => { + codeThemeOptions.push( + <option + key={'code-theme-key' + codeThemeIndex} + value={codeTheme.id} + > + {codeTheme.uiName} + </option> + ); + }); + + elements.push( <div - className='input-group color-picker' - id={element.id} + className='col-sm-4 form-group' + key={'custom-theme-key' + index} > - <input - className='form-control' - type='text' - defaultValue={theme[element.id]} - onChange={this.onInputChange} - /> - <span className='input-group-addon'><i></i></span> + <label className='custom-label'>{element.uiName}</label> + <div + className='input-group theme-group dropdown' + id={element.id} + > + <select + className='form-control' + type='text' + defaultValue={theme[element.id]} + onChange={this.onInputChange} + > + {codeThemeOptions} + </select> + <span className='input-group-addon'> + <img + src={'/static/images/themes/code_themes/' + theme[element.id] + '.png'} + /> + </span> + </div> </div> - </div> - ); + ); + } else { + elements.push( + <div + className='col-sm-4 form-group' + key={'custom-theme-key' + index} + > + <label className='custom-label'>{element.uiName}</label> + <div + className='input-group color-picker' + id={element.id} + > + <input + className='form-control' + type='text' + defaultValue={theme[element.id]} + onChange={this.onInputChange} + /> + <span className='input-group-addon'><i></i></span> + </div> + </div> + ); - colors += theme[element.id] + ','; + colors += theme[element.id] + ','; + } }); colors += theme.codeTheme; @@ -87,6 +129,7 @@ export default class CustomThemeChooser extends React.Component { {'Copy and paste to share theme colors:'} </label> <input + readOnly='true' type='text' className='form-control' value={colors} diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx index 1a9ac0ad3..4d594bb1b 100644 --- a/web/react/components/user_settings/import_theme_modal.jsx +++ b/web/react/components/user_settings/import_theme_modal.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +const ModalStore = require('../../stores/modal_store.jsx'); const UserStore = require('../../stores/user_store.jsx'); const Utils = require('../../utils/utils.jsx'); const Client = require('../../utils/client.jsx'); @@ -24,10 +25,10 @@ export default class ImportThemeModal extends React.Component { }; } componentDidMount() { - UserStore.addImportModalListener(this.updateShow); + ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow); } componentWillUnmount() { - UserStore.removeImportModalListener(this.updateShow); + ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow); } updateShow(show) { this.setState({show}); @@ -49,7 +50,7 @@ export default class ImportThemeModal extends React.Component { theme.sidebarText = colors[5]; theme.sidebarUnreadText = colors[5]; theme.sidebarTextHoverBg = colors[4]; - theme.sidebarTextActiveBg = colors[2]; + theme.sidebarTextActiveBorder = colors[2]; theme.sidebarTextActiveColor = colors[3]; theme.sidebarHeaderBg = colors[1]; theme.sidebarHeaderTextColor = colors[5]; @@ -58,9 +59,13 @@ export default class ImportThemeModal extends React.Component { theme.mentionColor = '#ffffff'; theme.centerChannelBg = '#ffffff'; theme.centerChannelColor = '#333333'; + theme.newMessageSeparator = '#F80'; theme.linkColor = '#2389d7'; theme.buttonBg = '#26a970'; theme.buttonColor = '#ffffff'; + theme.mentionHighlightBg = '#fff2bb'; + theme.mentionHighlightLink = '#2f81b7'; + theme.codeTheme = 'github'; let user = UserStore.getCurrentUser(); user.theme_props = theme; @@ -74,7 +79,6 @@ export default class ImportThemeModal extends React.Component { this.setState({show: false}); Utils.applyTheme(theme); - $('#user_settings').modal('show'); }, (err) => { var state = this.getStateFromStores(); diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx index 546e26ca3..e089ce973 100644 --- a/web/react/components/user_settings/user_settings.jsx +++ b/web/react/components/user_settings/user_settings.jsx @@ -16,6 +16,7 @@ export default class UserSettings extends React.Component { constructor(props) { super(props); + this.getActiveTab = this.getActiveTab.bind(this); this.onListenerChange = this.onListenerChange.bind(this); this.state = {user: UserStore.getCurrentUser()}; @@ -29,10 +30,14 @@ export default class UserSettings extends React.Component { UserStore.removeChangeListener(this.onListenerChange); } + getActiveTab() { + return this.refs.activeTab; + } + onListenerChange() { var user = UserStore.getCurrentUser(); if (!utils.areStatesEqual(this.state.user, user)) { - this.setState({user: user}); + this.setState({user}); } } @@ -41,10 +46,13 @@ export default class UserSettings extends React.Component { return ( <div> <GeneralTab + ref='activeTab' user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} /> </div> ); @@ -52,10 +60,14 @@ export default class UserSettings extends React.Component { return ( <div> <SecurityTab + ref='activeTab' user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} + setEnforceFocus={this.props.setEnforceFocus} /> </div> ); @@ -63,10 +75,13 @@ export default class UserSettings extends React.Component { return ( <div> <NotificationsTab + ref='activeTab' user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} /> </div> ); @@ -74,9 +89,14 @@ export default class UserSettings extends React.Component { return ( <div> <AppearanceTab + ref='activeTab' activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} + setEnforceFocus={this.props.setEnforceFocus} + setRequireConfirm={this.props.setRequireConfirm} /> </div> ); @@ -84,8 +104,11 @@ export default class UserSettings extends React.Component { return ( <div> <DeveloperTab + ref='activeTab' activeSection={this.props.activeSection} updateSection={this.props.updateSection} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} /> </div> ); @@ -93,10 +116,13 @@ export default class UserSettings extends React.Component { return ( <div> <IntegrationsTab + ref='activeTab' user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} /> </div> ); @@ -104,10 +130,13 @@ export default class UserSettings extends React.Component { return ( <div> <DisplayTab + ref='activeTab' user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} /> </div> ); @@ -115,10 +144,13 @@ export default class UserSettings extends React.Component { return ( <div> <AdvancedTab + ref='activeTab' user={this.state.user} activeSection={this.props.activeSection} updateSection={this.props.updateSection} updateTab={this.props.updateTab} + closeModal={this.props.closeModal} + collapseModal={this.props.collapseModal} /> </div> ); @@ -132,5 +164,9 @@ UserSettings.propTypes = { activeTab: React.PropTypes.string, activeSection: React.PropTypes.string, updateSection: React.PropTypes.func, - updateTab: React.PropTypes.func + updateTab: React.PropTypes.func, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired, + setEnforceFocus: React.PropTypes.func.isRequired, + setRequireConfirm: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_advanced.jsx b/web/react/components/user_settings/user_settings_advanced.jsx index 910444735..2616981ba 100644 --- a/web/react/components/user_settings/user_settings_advanced.jsx +++ b/web/react/components/user_settings/user_settings_advanced.jsx @@ -13,7 +13,6 @@ export default class AdvancedSettingsDisplay extends React.Component { this.updateSection = this.updateSection.bind(this); this.updateSetting = this.updateSetting.bind(this); - this.handleClose = this.handleClose.bind(this); this.setupInitialState = this.setupInitialState.bind(this); this.state = this.setupInitialState(); @@ -59,18 +58,6 @@ export default class AdvancedSettingsDisplay extends React.Component { this.props.updateSection(section); } - handleClose() { - this.updateSection(''); - } - - componentDidMount() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - } - - componentWillUnmount() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - } - render() { const serverError = this.state.serverError || null; let ctrlSendSection; @@ -139,6 +126,7 @@ export default class AdvancedSettingsDisplay extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > <span aria-hidden='true'>{'×'}</span> </button> @@ -146,7 +134,10 @@ export default class AdvancedSettingsDisplay extends React.Component { className='modal-title' ref='title' > - <i className='modal-back'></i> + <i + className='modal-back' + onClick={this.props.collapseModal} + /> {'Advanced Settings'} </h4> </div> @@ -165,5 +156,7 @@ AdvancedSettingsDisplay.propTypes = { user: React.PropTypes.object, updateSection: React.PropTypes.func, updateTab: React.PropTypes.func, - activeSection: React.PropTypes.string + activeSection: React.PropTypes.string, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index 7b4b54e27..d73b5f476 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -7,7 +7,6 @@ var Utils = require('../../utils/utils.jsx'); const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); -const CodeThemeChooser = require('./code_theme_chooser.jsx'); const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -19,14 +18,13 @@ export default class UserSettingsAppearance extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); - this.updateCodeTheme = this.updateCodeTheme.bind(this); - this.handleClose = this.handleClose.bind(this); + this.deactivate = this.deactivate.bind(this); + this.resetFields = this.resetFields.bind(this); this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); - this.originalTheme = this.state.theme; - this.originalCodeTheme = this.state.theme.codeTheme; + this.originalTheme = Object.assign({}, this.state.theme); } componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -34,7 +32,6 @@ export default class UserSettingsAppearance extends React.Component { if (this.props.activeSection === 'theme') { $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } - $('#user_settings').on('hidden.bs.modal', this.handleClose); } componentDidUpdate() { if (this.props.activeSection === 'theme') { @@ -44,14 +41,13 @@ export default class UserSettingsAppearance extends React.Component { } componentWillUnmount() { UserStore.removeChangeListener(this.onChange); - $('#user_settings').off('hidden.bs.modal', this.handleClose); } getStateFromStores() { const user = UserStore.getCurrentUser(); let theme = null; if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { - theme = user.theme_props; + theme = Object.assign({}, user.theme_props); } else { theme = $.extend(true, {}, Constants.THEMES.default); } @@ -73,6 +69,8 @@ export default class UserSettingsAppearance extends React.Component { if (!Utils.areStatesEqual(this.state, newState)) { this.setState(newState); } + + this.props.setEnforceFocus(true); } submitTheme(e) { e.preventDefault(); @@ -86,11 +84,11 @@ export default class UserSettingsAppearance extends React.Component { me: data }); - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateTab('general'); + this.props.setRequireConfirm(false); + this.originalTheme = Object.assign({}, this.state.theme); + $('.ps-container.modal-body').scrollTop(0); $('.ps-container.modal-body').perfectScrollbar('update'); - $('#user_settings').modal('hide'); }, (err) => { var state = this.getStateFromStores(); @@ -100,40 +98,47 @@ export default class UserSettingsAppearance extends React.Component { ); } updateTheme(theme) { - if (!theme.codeTheme) { - theme.codeTheme = this.state.theme.codeTheme; + let themeChanged = this.state.theme.length === theme.length; + if (!themeChanged) { + for (const field in theme) { + if (theme.hasOwnProperty(field)) { + if (this.state.theme[field] !== theme[field]) { + themeChanged = true; + break; + } + } + } } - this.setState({theme}); - Utils.applyTheme(theme); - } - updateCodeTheme(codeTheme) { - var theme = this.state.theme; - theme.codeTheme = codeTheme; + + this.props.setRequireConfirm(themeChanged); + this.setState({theme}); Utils.applyTheme(theme); } updateType(type) { this.setState({type}); } - handleClose() { + deactivate() { const state = this.getStateFromStores(); - state.serverError = null; - state.theme.codeTheme = this.originalCodeTheme; Utils.applyTheme(state.theme); - + } + resetFields() { + const state = this.getStateFromStores(); + state.serverError = null; this.setState(state); - $('.ps-container.modal-body').scrollTop(0); - $('.ps-container.modal-body').perfectScrollbar('update'); - $('#user_settings').modal('hide'); + Utils.applyTheme(state.theme); + + this.props.setRequireConfirm(false); } handleImportModal() { - $('#user_settings').modal('hide'); AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL, value: true }); + + this.props.setEnforceFocus(false); } render() { var serverError; @@ -187,12 +192,6 @@ export default class UserSettingsAppearance extends React.Component { </div> {custom} <hr /> - <strong className='radio'>{'Code Theme'}</strong> - <CodeThemeChooser - theme={this.state.theme} - updateTheme={this.updateCodeTheme} - /> - <hr /> {serverError} <a className='btn btn-sm btn-primary' @@ -204,7 +203,7 @@ export default class UserSettingsAppearance extends React.Component { <a className='btn btn-sm theme' href='#' - onClick={this.handleClose} + onClick={this.resetFields} > {'Cancel'} </a> @@ -218,8 +217,8 @@ export default class UserSettingsAppearance extends React.Component { <button type='button' className='close' - data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > <span aria-hidden='true'>{'×'}</span> </button> @@ -227,7 +226,11 @@ export default class UserSettingsAppearance extends React.Component { className='modal-title' ref='title' > - <i className='modal-back'></i>{'Appearance Settings'} + <i + className='modal-back' + onClick={this.props.collapseModal} + /> + {'Appearance Settings'} </h4> </div> <div className='user-settings'> @@ -253,5 +256,9 @@ UserSettingsAppearance.defaultProps = { }; UserSettingsAppearance.propTypes = { activeSection: React.PropTypes.string, - updateTab: React.PropTypes.func + updateTab: React.PropTypes.func, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired, + setRequireConfirm: React.PropTypes.func.isRequired, + setEnforceFocus: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index c2d7a9710..e6adba1d4 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -63,6 +63,7 @@ export default class DeveloperTab extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > <span aria-hidden='true'>{'×'}</span> </button> @@ -70,7 +71,11 @@ export default class DeveloperTab extends React.Component { className='modal-title' ref='title' > - <i className='modal-back'></i>{'Developer Settings'} + <i + className='modal-back' + onClick={this.props.collapseModal} + /> + {'Developer Settings'} </h4> </div> <div className='user-settings'> @@ -89,5 +94,7 @@ DeveloperTab.defaultProps = { }; DeveloperTab.propTypes = { activeSection: React.PropTypes.string, - updateSection: React.PropTypes.func + updateSection: React.PropTypes.func, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index d086c78a9..43c8d33d1 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -25,7 +25,6 @@ export default class UserSettingsDisplay extends React.Component { this.handleClockRadio = this.handleClockRadio.bind(this); this.handleNameRadio = this.handleNameRadio.bind(this); this.updateSection = this.updateSection.bind(this); - this.handleClose = this.handleClose.bind(this); this.state = getDisplayStateFromStores(); } @@ -53,15 +52,6 @@ export default class UserSettingsDisplay extends React.Component { this.setState(getDisplayStateFromStores()); this.props.updateSection(section); } - handleClose() { - this.updateSection(''); - } - componentDidMount() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - } - componentWillUnmount() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - } render() { const serverError = this.state.serverError || null; let clockSection; @@ -182,13 +172,13 @@ export default class UserSettingsDisplay extends React.Component { </label> <br/> </div> - <div><br/>{'How should other users be shown in Direct Messages list?'}</div> + <div><br/>{'Set what name to display in the Direct Messages list.'}</div> </div> ]; nameFormatSection = ( <SettingItemMax - title='Show real names, nick names or usernames?' + title='Teammate Name Display' inputs={inputs} submit={this.handleSubmit} server_error={serverError} @@ -210,7 +200,7 @@ export default class UserSettingsDisplay extends React.Component { nameFormatSection = ( <SettingItemMin - title='Show real names, nick names or usernames?' + title='Teammate Name Display' describe={describe} updateSection={() => { this.props.updateSection('name_format'); @@ -227,6 +217,7 @@ export default class UserSettingsDisplay extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > <span aria-hidden='true'>{'×'}</span> </button> @@ -234,7 +225,10 @@ export default class UserSettingsDisplay extends React.Component { className='modal-title' ref='title' > - <i className='modal-back'></i> + <i + className='modal-back' + onClick={this.props.collapseModal} + /> {'Display Settings'} </h4> </div> @@ -255,5 +249,7 @@ UserSettingsDisplay.propTypes = { user: React.PropTypes.object, updateSection: React.PropTypes.func, updateTab: React.PropTypes.func, - activeSection: React.PropTypes.string + activeSection: React.PropTypes.string, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 3adac197a..9f0c16194 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -32,7 +32,6 @@ export default class UserSettingsGeneralTab extends React.Component { this.updatePicture = this.updatePicture.bind(this); this.updateSection = this.updateSection.bind(this); - this.handleClose = this.handleClose.bind(this); this.setupInitialState = this.setupInitialState.bind(this); this.state = this.setupInitialState(props); @@ -210,20 +209,6 @@ export default class UserSettingsGeneralTab extends React.Component { this.submitActive = false; this.props.updateSection(section); } - handleClose() { - $(ReactDOM.findDOMNode(this)).find('.form-control').each(function clearForms() { - this.value = ''; - }); - - this.setState(assign({}, this.setupInitialState(this.props), {clientError: null, serverError: null, emailError: null})); - this.props.updateSection(''); - } - componentDidMount() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - } - componentWillUnmount() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - } setupInitialState(props) { var user = props.user; @@ -579,6 +564,7 @@ export default class UserSettingsGeneralTab extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > <span aria-hidden='true'>{'×'}</span> </button> @@ -586,7 +572,10 @@ export default class UserSettingsGeneralTab extends React.Component { className='modal-title' ref='title' > - <i className='modal-back'></i> + <i + className='modal-back' + onClick={this.props.collapseModal} + /> {'General Settings'} </h4> </div> @@ -613,5 +602,7 @@ UserSettingsGeneralTab.propTypes = { user: React.PropTypes.object, updateSection: React.PropTypes.func, updateTab: React.PropTypes.func, - activeSection: React.PropTypes.string + activeSection: React.PropTypes.string, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 4a9915a1f..744a6beea 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -11,24 +11,12 @@ export default class UserSettingsIntegrationsTab extends React.Component { super(props); this.updateSection = this.updateSection.bind(this); - this.handleClose = this.handleClose.bind(this); this.state = {}; } updateSection(section) { this.props.updateSection(section); } - handleClose() { - this.updateSection(''); - $('.ps-container.modal-body').scrollTop(0); - $('.ps-container.modal-body').perfectScrollbar('update'); - } - componentDidMount() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - } - componentWillUnmount() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - } render() { let incomingHooksSection; let outgoingHooksSection; @@ -104,6 +92,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > <span aria-hidden='true'>{'×'}</span> </button> @@ -111,7 +100,10 @@ export default class UserSettingsIntegrationsTab extends React.Component { className='modal-title' ref='title' > - <i className='modal-back'></i> + <i + className='modal-back' + onClick={this.props.collapseModal} + /> {'Integration Settings'} </h4> </div> @@ -132,5 +124,7 @@ UserSettingsIntegrationsTab.propTypes = { user: React.PropTypes.object, updateSection: React.PropTypes.func, updateTab: React.PropTypes.func, - activeSection: React.PropTypes.string + activeSection: React.PropTypes.string, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 18dd490e7..4dcf32cb9 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -1,34 +1,161 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var SettingsSidebar = require('../settings_sidebar.jsx'); -var UserSettings = require('./user_settings.jsx'); +const ConfirmModal = require('../confirm_modal.jsx'); +const Modal = ReactBootstrap.Modal; +const SettingsSidebar = require('../settings_sidebar.jsx'); +const UserSettings = require('./user_settings.jsx'); export default class UserSettingsModal extends React.Component { constructor(props) { super(props); + this.handleHide = this.handleHide.bind(this); + this.handleHidden = this.handleHidden.bind(this); + this.handleCollapse = this.handleCollapse.bind(this); + this.handleConfirm = this.handleConfirm.bind(this); + this.handleCancelConfirmation = this.handleCancelConfirmation.bind(this); + + this.deactivateTab = this.deactivateTab.bind(this); + this.closeModal = this.closeModal.bind(this); + this.collapseModal = this.collapseModal.bind(this); + this.updateTab = this.updateTab.bind(this); this.updateSection = this.updateSection.bind(this); - this.state = {active_tab: 'general', active_section: ''}; + this.state = { + active_tab: 'general', + active_section: '', + showConfirmModal: false, + enforceFocus: true + }; + + this.requireConfirm = false; + } + + componentDidUpdate(prevProps) { + if (!prevProps.show && this.props.show) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } + } + } + + // Called when the close button is pressed on the main modal + handleHide() { + if (this.requireConfirm) { + this.afterConfirm = () => this.handleHide(); + this.showConfirmModal(); + + return false; + } + + this.deactivateTab(); + this.props.onModalDismissed(); } - componentDidMount() { - $('body').on('click', '.modal-back', function changeDisplay() { - $(this).closest('.modal-dialog').removeClass('display--content'); + + // called after the dialog is fully hidden and faded out + handleHidden() { + this.setState({ + active_tab: 'general', + active_section: '' }); - $('body').on('click', '.modal-header .close', () => { - setTimeout(() => { - $('.modal-dialog.display--content').removeClass('display--content'); - }, 500); + } + + // Called to hide the settings pane when on mobile + handleCollapse() { + $(ReactDOM.findDOMNode(this.refs.modalBody)).closest('.modal-dialog').removeClass('display--content'); + + this.deactivateTab(); + + this.setState({ + active_tab: '', + active_section: '' }); } - updateTab(tab) { - this.setState({active_tab: tab}); + + handleConfirm() { + this.setState({ + showConfirmModal: false, + enforceFocus: true + }); + + this.requireConfirm = false; + + if (this.afterConfirm) { + this.afterConfirm(); + this.afterConfirm = null; + } } - updateSection(section) { - this.setState({active_section: section}); + + handleCancelConfirmation() { + this.setState({ + showConfirmModal: false, + enforceFocus: true + }); + + this.afterConfirm = null; } + + showConfirmModal(afterConfirm) { + this.setState({ + showConfirmModal: true, + enforceFocus: false + }); + + if (afterConfirm) { + this.afterConfirm = afterConfirm; + } + } + + // Called to let settings tab perform cleanup before being closed + deactivateTab() { + const activeTab = this.refs.userSettings.getActiveTab(); + if (activeTab && activeTab.deactivate) { + activeTab.deactivate(); + } + } + + // Called by settings tabs when their close button is pressed + closeModal() { + if (this.requireConfirm) { + this.showConfirmModal(this.closeModal); + } else { + this.handleHide(); + } + } + + // Called by settings tabs when their back button is pressed + collapseModal() { + if (this.requireConfirm) { + this.showConfirmModal(this.collapseModal); + } else { + this.handleCollapse(); + } + } + + updateTab(tab, skipConfirm) { + if (!skipConfirm && this.requireConfirm) { + this.showConfirmModal(() => this.updateTab(tab, true)); + } else { + this.deactivateTab(); + + this.setState({ + active_tab: tab, + active_section: '' + }); + } + } + + updateSection(section, skipConfirm) { + if (!skipConfirm && this.requireConfirm) { + this.showConfirmModal(() => this.updateSection(section, true)); + } else { + this.setState({active_section: section}); + } + } + render() { var tabs = []; tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'}); @@ -46,33 +173,17 @@ export default class UserSettingsModal extends React.Component { tabs.push({name: 'advanced', uiName: 'Advanced', icon: 'glyphicon glyphicon-list-alt'}); return ( - <div - className='modal fade' - ref='modal' - id='user_settings' - role='dialog' - tabIndex='-1' - aria-hidden='true' + <Modal + dialogClassName='settings-modal' + show={this.props.show} + onHide={this.handleHide} + onExited={this.handleHidden} + enforceFocus={this.state.enforceFocus} > - <div className='modal-dialog settings-modal'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>{'×'}</span> - </button> - <h4 - className='modal-title' - ref='title' - > - {'Account Settings'} - </h4> - </div> - <div className='modal-body'> + <Modal.Header closeButton={true}> + <Modal.Title>{'Account Settings'}</Modal.Title> + </Modal.Header> + <Modal.Body ref='modalBody'> <div className='settings-table'> <div className='settings-links'> <SettingsSidebar @@ -83,17 +194,33 @@ export default class UserSettingsModal extends React.Component { </div> <div className='settings-content minimize-settings'> <UserSettings + ref='userSettings' activeTab={this.state.active_tab} activeSection={this.state.active_section} updateSection={this.updateSection} updateTab={this.updateTab} + closeModal={this.closeModal} + collapseModal={this.collapseModal} + setEnforceFocus={(enforceFocus) => this.setState({enforceFocus})} + setRequireConfirm={(requireConfirm) => this.requireConfirm = requireConfirm} /> </div> </div> - </div> - </div> - </div> - </div> + </Modal.Body> + <ConfirmModal + title='Discard Changes?' + message='You have unsaved changes, are you sure you want to discard them?' + confirm_button='Yes, Discard' + show={this.state.showConfirmModal} + onConfirm={this.handleConfirm} + onCancel={this.handleCancelConfirmation} + /> + </Modal> ); } } + +UserSettingsModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index 2b904763c..c6f47804f 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -7,7 +7,6 @@ var SettingItemMax = require('../setting_item_max.jsx'); var client = require('../../utils/client.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var utils = require('../../utils/utils.jsx'); -var assign = require('object-assign'); function getNotificationsStateFromStores() { var user = UserStore.getCurrentUser(); @@ -77,7 +76,6 @@ export default class NotificationsTab extends React.Component { super(props); this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); this.updateSection = this.updateSection.bind(this); this.onListenerChange = this.onListenerChange.bind(this); this.handleNotifyRadio = this.handleNotifyRadio.bind(this); @@ -128,27 +126,15 @@ export default class NotificationsTab extends React.Component { }.bind(this) ); } - handleClose() { - $(ReactDOM.findDOMNode(this)).find('.form-control').each(function clearField() { - this.value = ''; - }); - - this.setState(assign({}, getNotificationsStateFromStores(), {serverError: null})); - - this.props.updateTab('general'); - } updateSection(section) { this.setState(getNotificationsStateFromStores()); this.props.updateSection(section); } componentDidMount() { UserStore.addChangeListener(this.onListenerChange); - $('#user_settings').on('hidden.bs.modal', this.handleClose); } componentWillUnmount() { UserStore.removeChangeListener(this.onListenerChange); - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); } onListenerChange() { var newState = getNotificationsStateFromStores(); @@ -644,15 +630,19 @@ export default class NotificationsTab extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > - <span aria-hidden='true'>×</span> + <span aria-hidden='true'>{'×'}</span> </button> <h4 className='modal-title' ref='title' > - <i className='modal-back'></i> - Notifications + <i + className='modal-back' + onClick={this.props.collapseModal} + /> + {'Notification Settings'} </h4> </div> <div @@ -686,5 +676,7 @@ NotificationsTab.propTypes = { updateSection: React.PropTypes.func, updateTab: React.PropTypes.func, activeSection: React.PropTypes.string, - activeTab: React.PropTypes.string + activeTab: React.PropTypes.string, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired }; diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index 983a10df0..61d13ed8b 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -3,6 +3,8 @@ var SettingItemMin = require('../setting_item_min.jsx'); var SettingItemMax = require('../setting_item_max.jsx'); +var AccessHistoryModal = require('../access_history_modal.jsx'); +var ActivityLogModal = require('../activity_log_modal.jsx'); var Client = require('../../utils/client.jsx'); var AsyncClient = require('../../utils/async_client.jsx'); var Constants = require('../../utils/constants.jsx'); @@ -11,14 +13,34 @@ export default class SecurityTab extends React.Component { constructor(props) { super(props); + this.showAccessHistoryModal = this.showAccessHistoryModal.bind(this); + this.showActivityLogModal = this.showActivityLogModal.bind(this); + this.hideModals = this.hideModals.bind(this); this.submitPassword = this.submitPassword.bind(this); this.updateCurrentPassword = this.updateCurrentPassword.bind(this); this.updateNewPassword = this.updateNewPassword.bind(this); this.updateConfirmPassword = this.updateConfirmPassword.bind(this); - this.handleClose = this.handleClose.bind(this); this.setupInitialState = this.setupInitialState.bind(this); - this.state = this.setupInitialState(); + const state = this.setupInitialState(); + state.showAccessHistoryModal = false; + state.showActivityLogModal = false; + this.state = state; + } + showAccessHistoryModal() { + this.props.setEnforceFocus(false); + this.setState({showAccessHistoryModal: true}); + } + showActivityLogModal() { + this.props.setEnforceFocus(false); + this.setState({showActivityLogModal: true}); + } + hideModals() { + this.props.setEnforceFocus(true); + this.setState({ + showAccessHistoryModal: false, + showActivityLogModal: false + }); } submitPassword(e) { e.preventDefault(); @@ -75,30 +97,9 @@ export default class SecurityTab extends React.Component { updateConfirmPassword(e) { this.setState({confirmPassword: e.target.value}); } - handleHistoryOpen() { - $('#user_settings').modal('hide'); - } - handleDevicesOpen() { - $('#user_settings').modal('hide'); - } - handleClose() { - $(ReactDOM.findDOMNode(this)).find('.form-control').each(function resetValue() { - this.value = ''; - }); - this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); - - this.props.updateTab('general'); - } setupInitialState() { return {currentPassword: '', newPassword: '', confirmPassword: ''}; } - componentDidMount() { - $('#user_settings').on('hidden.bs.modal', this.handleClose); - } - componentWillUnmount() { - $('#user_settings').off('hidden.bs.modal', this.handleClose); - this.props.updateSection(''); - } render() { var serverError; if (this.state.serverError) { @@ -236,14 +237,19 @@ export default class SecurityTab extends React.Component { className='close' data-dismiss='modal' aria-label='Close' + onClick={this.props.closeModal} > - <span aria-hidden='true'>×</span> + <span aria-hidden='true'>{'×'}</span> </button> <h4 className='modal-title' ref='title' > - <i className='modal-back'></i>Security Settings + <i + className='modal-back' + onClick={this.props.collapseModal} + /> + {'Security Settings'} </h4> </div> <div className='user-settings'> @@ -253,25 +259,29 @@ export default class SecurityTab extends React.Component { <div className='divider-dark'/> <br></br> <a - data-toggle='modal' className='security-links theme' - data-target='#access-history' href='#' - onClick={this.handleHistoryOpen} + onClick={this.showAccessHistoryModal} > <i className='fa fa-clock-o'></i>View Access History </a> <b> </b> <a - data-toggle='modal' className='security-links theme' - data-target='#activity-log' href='#' - onClick={this.handleDevicesOpen} + onClick={this.showActivityLogModal} > <i className='fa fa-globe'></i>View and Logout of Active Sessions </a> </div> + <AccessHistoryModal + show={this.state.showAccessHistoryModal} + onModalDismissed={this.hideModals} + /> + <ActivityLogModal + show={this.state.showActivityLogModal} + onModalDismissed={this.hideModals} + /> </div> ); } @@ -285,5 +295,8 @@ SecurityTab.propTypes = { user: React.PropTypes.object, activeSection: React.PropTypes.string, updateSection: React.PropTypes.func, - updateTab: React.PropTypes.func + updateTab: React.PropTypes.func, + closeModal: React.PropTypes.func.isRequired, + collapseModal: React.PropTypes.func.isRequired, + setEnforceFocus: React.PropTypes.func.isRequired }; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 067dcde50..8781d52a5 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -9,7 +9,6 @@ var ErrorStore = require('../stores/error_store.jsx'); var MentionList = require('../components/mention_list.jsx'); var GetLinkModal = require('../components/get_link_modal.jsx'); -var MemberInviteModal = require('../components/invite_member_modal.jsx'); var EditChannelModal = require('../components/edit_channel_modal.jsx'); var DeleteChannelModal = require('../components/delete_channel_modal.jsx'); var RenameChannelModal = require('../components/rename_channel_modal.jsx'); @@ -18,17 +17,13 @@ var DeletePostModal = require('../components/delete_post_modal.jsx'); var MoreChannelsModal = require('../components/more_channels.jsx'); var PostDeletedModal = require('../components/post_deleted_modal.jsx'); var ChannelNotificationsModal = require('../components/channel_notifications.jsx'); -var UserSettingsModal = require('../components/user_settings/user_settings_modal.jsx'); var TeamSettingsModal = require('../components/team_settings_modal.jsx'); -var ChannelMembersModal = require('../components/channel_members.jsx'); -var ChannelInviteModal = require('../components/channel_invite_modal.jsx'); var TeamMembersModal = require('../components/team_members.jsx'); var ChannelInfoModal = require('../components/channel_info_modal.jsx'); -var AccessHistoryModal = require('../components/access_history_modal.jsx'); -var ActivityLogModal = require('../components/activity_log_modal.jsx'); var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx'); var RegisterAppModal = require('../components/register_app_modal.jsx'); var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx'); +var InviteMemberModal = require('../components/invite_member_modal.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); @@ -83,8 +78,8 @@ function setupChannelPage(props) { ); ReactDOM.render( - <UserSettingsModal />, - document.getElementById('user_settings_modal') + <InviteMemberModal />, + document.getElementById('invite_member_modal') ); ReactDOM.render( @@ -103,11 +98,6 @@ function setupChannelPage(props) { ); ReactDOM.render( - <MemberInviteModal teamType={props.TeamType} />, - document.getElementById('invite_member_modal') - ); - - ReactDOM.render( <EditChannelModal />, document.getElementById('edit_channel_modal') ); @@ -128,16 +118,6 @@ function setupChannelPage(props) { ); ReactDOM.render( - <ChannelMembersModal />, - document.getElementById('channel_members_modal') - ); - - ReactDOM.render( - <ChannelInviteModal />, - document.getElementById('channel_invite_modal') - ); - - ReactDOM.render( <ChannelInfoModal />, document.getElementById('channel_info_modal') ); @@ -163,16 +143,6 @@ function setupChannelPage(props) { ); ReactDOM.render( - <AccessHistoryModal />, - document.getElementById('access_history_modal') - ); - - ReactDOM.render( - <ActivityLogModal />, - document.getElementById('activity_log_modal') - ); - - ReactDOM.render( <RemovedFromChannelModal />, document.getElementById('removed_from_channel_modal') ); diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx new file mode 100644 index 000000000..dc65d48da --- /dev/null +++ b/web/react/stores/modal_store.jsx @@ -0,0 +1,42 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const EventEmitter = require('events').EventEmitter; + +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; + +class ModalStoreClass extends EventEmitter { + constructor() { + super(); + + this.addModalListener = this.addModalListener.bind(this); + this.removeModalListener = this.removeModalListener.bind(this); + + this.handleEventPayload = this.handleEventPayload.bind(this); + this.dispatchToken = AppDispatcher.register(this.handleEventPayload); + } + + addModalListener(action, callback) { + this.on(action, callback); + } + + removeModalListener(action, callback) { + this.removeListener(action, callback); + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.TOGGLE_IMPORT_THEME_MODAL: + case ActionTypes.TOGGLE_INVITE_MEMBER_MODAL: + this.emit(action.type, action.value); + break; + } + } +} + +const ModalStore = new ModalStoreClass(); +export default ModalStore; diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index aedb3dc09..4fa7224b7 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -13,7 +13,6 @@ var CHANGE_EVENT_SESSIONS = 'change_sessions'; var CHANGE_EVENT_AUDITS = 'change_audits'; var CHANGE_EVENT_TEAMS = 'change_teams'; var CHANGE_EVENT_STATUSES = 'change_statuses'; -var TOGGLE_IMPORT_MODAL_EVENT = 'toggle_import_modal'; class UserStoreClass extends EventEmitter { constructor() { @@ -34,9 +33,6 @@ class UserStoreClass extends EventEmitter { this.emitStatusesChange = this.emitStatusesChange.bind(this); this.addStatusesChangeListener = this.addStatusesChangeListener.bind(this); this.removeStatusesChangeListener = this.removeStatusesChangeListener.bind(this); - this.emitToggleImportModal = this.emitToggleImportModal.bind(this); - this.addImportModalListener = this.addImportModalListener.bind(this); - this.removeImportModalListener = this.removeImportModalListener.bind(this); this.getCurrentId = this.getCurrentId.bind(this); this.getCurrentUser = this.getCurrentUser.bind(this); this.setCurrentUser = this.setCurrentUser.bind(this); @@ -124,18 +120,6 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_STATUSES, callback); } - emitToggleImportModal(value) { - this.emit(TOGGLE_IMPORT_MODAL_EVENT, value); - } - - addImportModalListener(callback) { - this.on(TOGGLE_IMPORT_MODAL_EVENT, callback); - } - - removeImportModalListener(callback) { - this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback); - } - getCurrentUser() { if (this.getProfiles()[global.window.mm_user.id] == null) { this.saveProfile(global.window.mm_user); @@ -353,10 +337,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); break; - case ActionTypes.TOGGLE_IMPORT_THEME_MODAL: - UserStore.emitToggleImportModal(action.value); - break; - default: } }); diff --git a/web/react/utils/channel_intro_mssages.jsx b/web/react/utils/channel_intro_mssages.jsx index b3f868456..161c79761 100644 --- a/web/react/utils/channel_intro_mssages.jsx +++ b/web/react/utils/channel_intro_mssages.jsx @@ -3,6 +3,7 @@ // See License.txt for license information. const Utils = require('./utils.jsx'); +const InviteMemberModal = require('../components/invite_member_modal.jsx'); const UserProfile = require('../components/user_profile.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const Constants = require('../utils/constants.jsx'); @@ -109,8 +110,7 @@ export function createDefaultIntroMessage(channel) { <a className='intro-links' href='#' - data-toggle='modal' - data-target='#invite_member' + onClick={InviteMemberModal.show} > <i className='fa fa-user-plus'></i>{'Invite others to this team'} </a> @@ -213,6 +213,7 @@ export function createStandardIntroMessage(channel) { > <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType} </a> + </div> ); } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 39be577df..58ee8e2d2 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -39,7 +39,8 @@ module.exports = { RECIEVED_LOGS: null, RECIEVED_ALL_TEAMS: null, - TOGGLE_IMPORT_THEME_MODAL: null + TOGGLE_IMPORT_THEME_MODAL: null, + TOGGLE_INVITE_MEMBER_MODAL: null }), PayloadSources: keyMirror({ @@ -158,7 +159,8 @@ module.exports = { buttonBg: '#2389d7', buttonColor: '#FFFFFF', mentionHighlightBg: '#fff2bb', - mentionHighlightLink: '#2f81b7' + mentionHighlightLink: '#2f81b7', + codeTheme: 'github' }, organization: { type: 'Organization', @@ -180,7 +182,8 @@ module.exports = { buttonBg: '#1dacfc', buttonColor: '#FFFFFF', mentionHighlightBg: '#fff2bb', - mentionHighlightLink: '#2f81b7' + mentionHighlightLink: '#2f81b7', + codeTheme: 'github' }, mattermostDark: { type: 'Mattermost Dark', @@ -202,7 +205,8 @@ module.exports = { buttonBg: '#4CBBA4', buttonColor: '#FFFFFF', mentionHighlightBg: '#984063', - mentionHighlightLink: '#A4FFEB' + mentionHighlightLink: '#A4FFEB', + codeTheme: 'solarized_dark' }, windows10: { type: 'Windows Dark', @@ -224,7 +228,8 @@ module.exports = { buttonBg: '#0177e7', buttonColor: '#FFFFFF', mentionHighlightBg: '#784098', - mentionHighlightLink: '#A4FFEB' + mentionHighlightLink: '#A4FFEB', + codeTheme: 'monokai' } }, THEME_ELEMENTS: [ @@ -303,14 +308,30 @@ module.exports = { { id: 'mentionHighlightLink', uiName: 'Mention Highlight Link' + }, + { + id: 'codeTheme', + uiName: 'Code Theme', + themes: [ + { + id: 'solarized_dark', + uiName: 'Solarized Dark' + }, + { + id: 'solarized_light', + uiName: 'Solarized Light' + }, + { + id: 'github', + uiName: 'GitHub' + }, + { + id: 'monokai', + uiName: 'Monokai' + } + ] } ], - CODE_THEMES: { - github: 'GitHub', - solarized_light: 'Solarized light', - monokai: 'Monokai', - solarized_dark: 'Solarized Dark' - }, DEFAULT_CODE_THEME: 'github', Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', @@ -363,5 +384,8 @@ module.exports = { BOTTOM: 1, POST: 2, SIDEBAR_OPEN: 3 + }, + NotificationPrefs: { + MENTION: 'mention' } }; diff --git a/web/react/utils/text_formatting.jsx b/web/react/utils/text_formatting.jsx index 2de858a17..ac26107cc 100644 --- a/web/react/utils/text_formatting.jsx +++ b/web/react/utils/text_formatting.jsx @@ -64,22 +64,6 @@ export function doFormatText(text, options) { return output; } -export function doFormatEmoticons(text) { - const tokens = new Map(); - - let output = Emoticons.handleEmoticons(text, tokens); - output = replaceTokens(output, tokens); - - return output; -} - -export function doFormatMentions(text) { - const tokens = new Map(); - let output = autolinkAtMentions(text, tokens); - output = replaceTokens(output, tokens); - return output; -} - export function sanitizeHtml(text) { let output = text; @@ -182,11 +166,15 @@ function autolinkAtMentions(text, tokens) { } let output = text; - output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken); + output = output.replace(/(^|[^a-z0-9])(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken); return output; } +function escapeRegex(text) { + return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + function highlightCurrentMentions(text, tokens) { let output = text; @@ -226,7 +214,7 @@ function highlightCurrentMentions(text, tokens) { } for (const mention of UserStore.getCurrentMentionKeys()) { - output = output.replace(new RegExp(`(^|\\W)(${mention})\\b`, 'gi'), replaceCurrentMentionWithToken); + output = output.replace(new RegExp(`(^|\\W)(${escapeRegex(mention)})\\b`, 'gi'), replaceCurrentMentionWithToken); } return output; @@ -306,7 +294,7 @@ function highlightSearchTerm(text, tokens, searchTerm) { return prefix + alias; } - return output.replace(new RegExp(`()(${searchTerm})`, 'gi'), replaceSearchTermWithToken); + return output.replace(new RegExp(`()(${escapeRegex(searchTerm)})`, 'gi'), replaceSearchTermWithToken); } function replaceTokens(text, tokens) { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index e8d34dccd..fff9c460b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -151,10 +151,14 @@ export function notifyMe(title, body, channel) { } } +var canDing = true; + export function ding() { - if (!isBrowserFirefox()) { + if (!isBrowserFirefox() && canDing) { var audio = new Audio('/static/images/ding.mp3'); audio.play(); + canDing = false; + setTimeout(() => canDing = true, 3000); } } @@ -438,11 +442,6 @@ export function toTitleCase(str) { } export function applyTheme(theme) { - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; - } - updateCodeTheme(theme.codeTheme); - if (theme.sidebarBg) { changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } @@ -533,7 +532,7 @@ export function applyTheme(theme) { changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3); changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2); changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1); - changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle, .tip-overlay .tutorial__circles .circle.active', 'background:' + theme.centerChannelColor, 1); + changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle', 'background:' + theme.centerChannelColor, 1); changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); @@ -598,6 +597,11 @@ export function applyTheme(theme) { if (theme.mentionHighlightLink) { changeCss('.mention-highlight .mention-link', 'color:' + theme.mentionHighlightLink, 1); } + + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; + } + updateCodeTheme(theme.codeTheme); } export function changeCss(className, classValue, classRepeat) { // we need invisible container to store additional css definitions |