diff options
27 files changed, 651 insertions, 468 deletions
diff --git a/api/post.go b/api/post.go index 31a7ab3b5..b52db8752 100644 --- a/api/post.go +++ b/api/post.go @@ -890,7 +890,10 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { channels := []store.StoreChannel{} for _, params := range paramsList { - channels = append(channels, Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, params)) + // don't allow users to search for everything + if params.Terms != "*" { + channels = append(channels, Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, params)) + } } posts := &model.PostList{} diff --git a/api/post_test.go b/api/post_test.go index 3452c9788..0cb437e88 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -450,6 +450,10 @@ func TestSearchPosts(t *testing.T) { if len(r3.Order) != 1 && r3.Order[0] == post3.Id { t.Fatal("wrong serach") } + + if r4 := Client.Must(Client.SearchPosts("*")).Data.(*model.PostList); len(r4.Order) != 0 { + t.Fatal("searching for just * shouldn't return any results") + } } func TestSearchHashtagPosts(t *testing.T) { 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..6a24870f6 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> + <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> + <Modal.Body ref='modalBody'> + {content} + </Modal.Body> + </Modal> ); } } + +ActivityLogModal.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/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/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/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_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/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx index 1a9ac0ad3..24da106d0 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}); @@ -74,7 +75,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..425645c1f 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -20,13 +20,13 @@ export default class UserSettingsAppearance extends React.Component { 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 +34,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 +43,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 +71,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 +86,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(); @@ -103,37 +103,53 @@ export default class UserSettingsAppearance extends React.Component { 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.props.setRequireConfirm(themeChanged); + this.setState({theme}); Utils.applyTheme(theme); } updateCodeTheme(codeTheme) { var theme = this.state.theme; theme.codeTheme = codeTheme; - this.setState({theme}); - Utils.applyTheme(theme); + this.updateTheme(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; @@ -204,7 +220,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 +234,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 +243,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 +273,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..3c12ead23 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; @@ -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..2f872effd 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,15 @@ 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 +80,8 @@ function setupChannelPage(props) { ); ReactDOM.render( - <UserSettingsModal />, - document.getElementById('user_settings_modal') + <InviteMemberModal />, + document.getElementById('invite_member_modal') ); ReactDOM.render( @@ -103,11 +100,6 @@ function setupChannelPage(props) { ); ReactDOM.render( - <MemberInviteModal teamType={props.TeamType} />, - document.getElementById('invite_member_modal') - ); - - ReactDOM.render( <EditChannelModal />, document.getElementById('edit_channel_modal') ); @@ -163,16 +155,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..b8d346ba7 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({ 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/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 96a6cf2ab..3be83ed2f 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -13,6 +13,10 @@ .settings-modal { width:800px; max-width: 100%; + .modal-content { + width:800px; + max-width: 100%; + } .modal-back { width: 40px; height: 56px; |