diff options
Diffstat (limited to 'web/react/components')
20 files changed, 510 insertions, 495 deletions
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 8c721348f..6e12c7c14 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -4,6 +4,7 @@ import NavbarSearchBox from './search_bar.jsx'; import MessageWrapper from './message_wrapper.jsx'; import PopoverListMembers from './popover_list_members.jsx'; +import EditChannelHeaderModal from './edit_channel_header_modal.jsx'; import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx'; import ChannelInfoModal from './channel_info_modal.jsx'; import ChannelInviteModal from './channel_invite_modal.jsx'; @@ -167,17 +168,13 @@ export default class ChannelHeader extends React.Component { key='edit_header_direct' role='presentation' > - <a + <ToggleModalButton role='menuitem' - href='#' - data-toggle='modal' - data-target='#edit_channel' - data-header={channel.header} - data-title={channel.display_name} - data-channelid={channel.id} + dialogType={EditChannelHeaderModal} + dialogProps={{channel}} > {'Set Channel Header...'} - </a> + </ToggleModalButton> </li> ); } else { @@ -235,17 +232,13 @@ export default class ChannelHeader extends React.Component { key='set_channel_header' role='presentation' > - <a + <ToggleModalButton role='menuitem' - href='#' - data-toggle='modal' - data-target='#edit_channel' - data-header={channel.header} - data-title={channel.display_name} - data-channelid={channel.id} + dialogType={EditChannelHeaderModal} + dialogProps={{channel}} > - {'Set '}{channelTerm}{' Header...'} - </a> + {`Set ${channelTerm} Header...`} + </ToggleModalButton> </li> ); dropdownContents.push( diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx index ff83d0420..7fc0f79cf 100644 --- a/web/react/components/command_list.jsx +++ b/web/react/components/command_list.jsx @@ -81,7 +81,7 @@ export default class CommandList extends React.Component { <div ref='mentionlist' className='command-box' - style={{height: (this.state.suggestions.length * 56) + 2}} + style={{height: (suggestions.length * 56) + 2}} > {suggestions} </div> diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index fab5b60ea..3c4b17905 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -159,13 +159,4 @@ export default class DeletePostModal extends React.Component { </Modal> ); } - - static show(post, commentCount) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_DELETE_POST_MODAL, - value: true, - post, - commentCount: commentCount || 0 - }); - } } diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx new file mode 100644 index 000000000..5529a419d --- /dev/null +++ b/web/react/components/edit_channel_header_modal.jsx @@ -0,0 +1,126 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Client from '../utils/client.jsx'; +import * as AsyncClient from '../utils/async_client.jsx'; +import * as Utils from '../utils/utils.jsx'; + +const Modal = ReactBootstrap.Modal; + +export default class EditChannelHeaderModal extends React.Component { + constructor(props) { + super(props); + + this.handleEdit = this.handleEdit.bind(this); + + this.onShow = this.onShow.bind(this); + this.onHide = this.onHide.bind(this); + + this.state = { + serverError: '' + }; + } + + componentDidMount() { + if (this.props.show) { + this.onShow(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + + handleEdit() { + var data = {}; + data.channel_id = this.props.channel.id; + + if (data.channel_id.length !== 26) { + return; + } + + data.channel_header = ReactDOM.findDOMNode(this.refs.textarea).value; + + Client.updateChannelHeader(data, + () => { + this.setState({serverError: ''}); + AsyncClient.getChannel(this.props.channel.id); + this.onHide(); + }, + (err) => { + if (err.message === 'Invalid channel_header parameter') { + this.setState({serverError: 'This channel header is too long, please enter a shorter one'}); + } else { + this.setState({serverError: err.message}); + } + } + ); + } + + onShow() { + const textarea = ReactDOM.findDOMNode(this.refs.textarea); + Utils.placeCaretAtEnd(textarea); + } + + onHide() { + this.setState({ + serverError: '' + }); + + this.props.onHide(); + } + + render() { + var serverError = null; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>; + } + + return ( + <Modal + show={this.props.show} + onHide={this.onHide} + > + <Modal.Header closeButton={true}> + {'Edit Header for ' + this.props.channel.display_name} + </Modal.Header> + <Modal.Body> + <p>{'Edit the text appearing next to the channel name in the channel header.'}</p> + <textarea + ref='textarea' + className='form-control no-resize' + rows='6' + id='edit_header' + maxLength='1024' + defaultValue={this.props.channel.header} + /> + {serverError} + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.props.onHide} + > + {'Cancel'} + </button> + <button + type='button' + className='btn btn-primary' + onClick={this.handleEdit} + > + {'Save'} + </button> + </Modal.Footer> + </Modal> + ); + } +} + +EditChannelHeaderModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + channel: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx deleted file mode 100644 index 80dab4a57..000000000 --- a/web/react/components/edit_channel_modal.jsx +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from '../utils/client.jsx'; -import * as AsyncClient from '../utils/async_client.jsx'; - -export default class EditChannelModal extends React.Component { - constructor(props) { - super(props); - - this.handleEdit = this.handleEdit.bind(this); - this.handleUserInput = this.handleUserInput.bind(this); - this.handleClose = this.handleClose.bind(this); - this.onShow = this.onShow.bind(this); - this.handleShown = this.handleShown.bind(this); - - this.state = { - header: '', - title: '', - channelId: '', - serverError: '' - }; - } - handleEdit() { - var data = {}; - data.channel_id = this.state.channelId; - - if (data.channel_id.length !== 26) { - return; - } - - data.channel_header = this.state.header.trim(); - - Client.updateChannelHeader(data, - () => { - this.setState({serverError: ''}); - AsyncClient.getChannel(this.state.channelId); - $(ReactDOM.findDOMNode(this.refs.modal)).modal('hide'); - }, - (err) => { - if (err.message === 'Invalid channel_header parameter') { - this.setState({serverError: 'This channel header is too long, please enter a shorter one'}); - } else { - this.setState({serverError: err.message}); - } - } - ); - } - handleUserInput(e) { - this.setState({header: e.target.value}); - } - handleClose() { - this.setState({header: '', serverError: ''}); - } - onShow(e) { - 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); - } - render() { - var serverError = null; - if (this.state.serverError) { - serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>; - } - - var editTitle = ( - <h4 - className='modal-title' - ref='title' - > - {'Edit Header'} - </h4> - ); - if (this.state.title) { - editTitle = ( - <h4 - className='modal-title' - ref='title' - > - {'Edit Header for '}<span className='name'>{this.state.title}</span> - </h4> - ); - } - - return ( - <div - className='modal fade' - ref='modal' - id='edit_channel' - role='dialog' - tabIndex='-1' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>{'×'}</span> - </button> - {editTitle} - </div> - <div className='modal-body'> - <p>{'Edit the text appearing next to the channel name in the channel header.'}</p> - <textarea - className='form-control no-resize' - rows='6' - id='edit_header' - maxLength='1024' - value={this.state.header} - onChange={this.handleUserInput} - /> - {serverError} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - {'Cancel'} - </button> - <button - type='button' - className='btn btn-primary' - onClick={this.handleEdit} - > - {'Save'} - </button> - </div> - </div> - </div> - </div> - ); - } -} diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index ddbdee8a4..eb58fe721 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -3,7 +3,7 @@ import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import DeletePostModal from './delete_post_modal.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import Textbox from './textbox.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import PostStore from '../stores/post_store.jsx'; @@ -35,7 +35,7 @@ export default class EditPostModal extends React.Component { delete tempState.editText; BrowserStore.setItem('edit_state_transfer', tempState); $('#edit_post').modal('hide'); - DeletePostModal.show(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); + EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); return; } diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx index 2bd2c42d6..df5d6b8e1 100644 --- a/web/react/components/get_link_modal.jsx +++ b/web/react/components/get_link_modal.jsx @@ -1,32 +1,28 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; +const Modal = ReactBootstrap.Modal; export default class GetLinkModal extends React.Component { constructor(props) { super(props); - this.handleClick = this.handleClick.bind(this); - this.onShow = this.onShow.bind(this); this.onHide = this.onHide.bind(this); - this.state = {copiedLink: false}; - } - onShow(e) { - var button = e.relatedTarget; - this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')}); + this.copyLink = this.copyLink.bind(this); + + this.state = { + copiedLink: false + }; } + onHide() { this.setState({copiedLink: false}); + + this.props.onHide(); } - componentDidMount() { - if (this.refs.modal) { - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); - $(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', this.onHide); - } - } - handleClick() { + + copyLink() { var copyTextarea = $(ReactDOM.findDOMNode(this.refs.textarea)); copyTextarea.select(); @@ -41,8 +37,18 @@ export default class GetLinkModal extends React.Component { this.setState({copiedLink: false}); } } + render() { - var currentUser = UserStore.getCurrentUser(); + let helpText = null; + if (this.props.helpText) { + helpText = ( + <p> + {this.props.helpText} + <br /> + <br /> + </p> + ); + } let copyLink = null; if (document.queryCommandSupported('copy')) { @@ -51,75 +57,59 @@ export default class GetLinkModal extends React.Component { data-copy-btn='true' type='button' className='btn btn-primary pull-left' - onClick={this.handleClick} - data-clipboard-text={this.state.value} + onClick={this.copyLink} > - Copy Link + {'Copy Link'} </button> ); } var copyLinkConfirm = null; if (this.state.copiedLink) { - copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> Link copied to clipboard.</p>; + copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i>{' Link copied to clipboard.'}</p>; } - if (currentUser != null) { - return ( - <div - className='modal fade' - ref='modal' - id='get_link' - tabIndex='-1' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>×</span> - </button> - <h4 - className='modal-title' - id='myModalLabel' - > - {this.state.title} Link - </h4> - </div> - <div className='modal-body'> - <p> - Send teammates the link below for them to sign-up to this team site. - <br /><br /> - </p> - <textarea - className='form-control no-resize min-height' - readOnly='true' - ref='textarea' - value={this.state.value} - /> - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - Close - </button> - {copyLink} - {copyLinkConfirm} - </div> - </div> - </div> - </div> - ); - } - return <div/>; + return ( + <Modal + show={this.props.show} + onHide={this.onHide} + > + <Modal.Header closeButton={true}> + {this.props.title} + </Modal.Header> + <Modal.Body> + {helpText} + <textarea + className='form-control no-resize min-height' + readOnly='true' + ref='textarea' + value={this.props.link} + /> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.onHide} + > + {'Close'} + </button> + {copyLink} + {copyLinkConfirm} + </Modal.Footer> + </Modal> + ); } } + +GetLinkModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired, + title: React.PropTypes.string.isRequired, + helpText: React.PropTypes.string, + link: React.PropTypes.string.isRequired +}; + +GetLinkModal.defaultProps = { + helpText: null +}; diff --git a/web/react/components/get_team_invite_link_modal.jsx b/web/react/components/get_team_invite_link_modal.jsx new file mode 100644 index 000000000..a926c4451 --- /dev/null +++ b/web/react/components/get_team_invite_link_modal.jsx @@ -0,0 +1,45 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Constants from '../utils/constants.jsx'; +import GetLinkModal from './get_link_modal.jsx'; +import ModalStore from '../stores/modal_store.jsx'; +import TeamStore from '../stores/team_store.jsx'; + +export default class GetTeamInviteLinkModal extends React.Component { + constructor(props) { + super(props); + + this.handleToggle = this.handleToggle.bind(this); + + this.state = { + show: false + }; + } + + componentDidMount() { + ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle); + } + + componentWillUnmount() { + ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle); + } + + handleToggle(value) { + this.setState({ + show: value + }); + } + + render() { + return ( + <GetLinkModal + show={this.state.show} + onHide={() => this.setState({show: false})} + title='Team Invite Link' + helpText='Send teammates the link below for them to sign-up to this team site.' + link={TeamStore.getCurrentInviteLink()} + /> + ); + } +} diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 7df75252e..76f52faa9 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -4,8 +4,8 @@ import * as utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Client from '../utils/client.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import ModalStore from '../stores/modal_store.jsx'; import UserStore from '../stores/user_store.jsx'; import TeamStore from '../stores/team_store.jsx'; @@ -23,6 +23,7 @@ export default class InviteMemberModal extends React.Component { this.addInviteFields = this.addInviteFields.bind(this); this.clearFields = this.clearFields.bind(this); this.removeInviteFields = this.removeInviteFields.bind(this); + this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this); this.state = { show: false, @@ -188,6 +189,12 @@ export default class InviteMemberModal extends React.Component { this.setState({inviteIds: inviteIds, idCount: count}); } + showGetTeamInviteLinkModal() { + this.handleHide(false); + + EventHelpers.showGetTeamInviteLinkModal(); + } + render() { var currentUser = UserStore.getCurrentUser(); @@ -333,22 +340,18 @@ export default class InviteMemberModal extends React.Component { } else { var teamInviteLink = null; if (currentUser && TeamStore.getCurrent().type === 'O') { - var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id; - var link = - ( - <a - href='#' - data-toggle='modal' - data-target='#get_link' - data-title='Team Invite' - data-value={linkUrl} - onClick={() => this.handleHide(this, false)} - >Team Invite Link</a> + var link = ( + <a + href='#' + onClick={this.showGetTeamInviteLinkModal} + > + {'Team Invite Link'} + </a> ); teamInviteLink = ( <p> - You can also invite people using the {link}. + {'You can also invite people using the '}{link}{'.'} </p> ); } @@ -405,13 +408,6 @@ export default class InviteMemberModal extends React.Component { return null; } - - static show() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, - value: true - }); - } } InviteMemberModal.propTypes = { diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx index 72fdb7be9..f1c31131f 100644 --- a/web/react/components/member_list_team.jsx +++ b/web/react/components/member_list_team.jsx @@ -2,17 +2,56 @@ // See License.txt for license information. import MemberListTeamItem from './member_list_team_item.jsx'; +import UserStore from '../stores/user_store.jsx'; export default class MemberListTeam extends React.Component { + constructor(props) { + super(props); + + this.getUsers = this.getUsers.bind(this); + this.onChange = this.onChange.bind(this); + + this.state = { + users: this.getUsers() + }; + } + + componentDidMount() { + UserStore.addChangeListener(this.onChange); + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onChange); + } + + getUsers() { + const profiles = UserStore.getProfiles(); + const users = []; + + for (const id of Object.keys(profiles)) { + users.push(profiles[id]); + } + + users.sort((a, b) => a.username.localeCompare(b.username)); + + return users; + } + + onChange() { + this.setState({ + users: this.getUsers() + }); + } + render() { - const memberList = this.props.users.map(function makeListItem(user) { + const memberList = this.state.users.map((user) => { return ( <MemberListTeamItem key={user.id} user={user} /> ); - }, this); + }); return ( <table className='table more-table member-list-holder'> @@ -23,7 +62,3 @@ export default class MemberListTeam extends React.Component { ); } } - -MemberListTeam.propTypes = { - users: React.PropTypes.arrayOf(React.PropTypes.object).isRequired -}; diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 6848ee5da..03cc75a08 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import EditChannelHeaderModal from './edit_channel_header_modal.jsx'; import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx'; import MessageWrapper from './message_wrapper.jsx'; import NotifyCounts from './notify_counts.jsx'; @@ -33,11 +34,15 @@ export default class Navbar extends React.Component { this.onChange = this.onChange.bind(this); this.handleLeave = this.handleLeave.bind(this); this.showSearch = this.showSearch.bind(this); + + this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this); + this.createCollapseButtons = this.createCollapseButtons.bind(this); this.createDropdown = this.createDropdown.bind(this); const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; + state.showEditChannelHeaderModal = false; state.showMembersModal = false; state.showInviteModal = false; this.state = state; @@ -110,6 +115,16 @@ export default class Navbar extends React.Component { this.setState(this.getStateFromStores()); $('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true}); } + showEditChannelHeaderModal() { + // this can't be done using a ToggleModalButton because we can't use one inside an OverlayTrigger + if (this.refs.headerOverlay) { + this.refs.headerOverlay.hide(); + } + + this.setState({ + showEditChannelHeaderModal: true + }); + } createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) { if (channel) { var viewInfoOption = ( @@ -129,11 +144,7 @@ export default class Navbar extends React.Component { <a role='menuitem' href='#' - data-toggle='modal' - data-target='#edit_channel' - data-header={channel.header} - data-title={channel.display_name} - data-channelid={channel.id} + onClick={this.showEditChannelHeaderModal} > {'Set Channel Header...'} </a> @@ -239,7 +250,7 @@ export default class Navbar extends React.Component { dialogType={ChannelNotificationsModal} dialogProps={{channel}} > - {'Notification Preferences'} + {'Notification Preferences'} </ToggleModalButton> </li> ); @@ -249,6 +260,7 @@ export default class Navbar extends React.Component { <div className='navbar-brand'> <div className='dropdown'> <OverlayTrigger + ref='headerOverlay' trigger='click' placement='bottom' overlay={popoverContent} @@ -358,6 +370,9 @@ export default class Navbar extends React.Component { var isAdmin = false; var isDirect = false; + var editChannelHeaderModal = null; + var editChannelPurposeModal = null; + if (channel) { popoverContent = ( <Popover @@ -400,11 +415,7 @@ export default class Navbar extends React.Component { <br/> <a href='#' - data-toggle='modal' - data-header={channel.header} - data-title={channel.display_name} - data-channelid={channel.id} - data-target='#edit_channel' + onClick={this.showEditChannelHeaderModal} > {'Click here'} </a> @@ -413,6 +424,22 @@ export default class Navbar extends React.Component { </Popover> ); } + + editChannelHeaderModal = ( + <EditChannelHeaderModal + show={this.state.showEditChannelHeaderModal} + onHide={() => this.setState({showEditChannelHeaderModal: false})} + channel={channel} + /> + ); + + editChannelPurposeModal = ( + <EditChannelPurposeModal + show={this.state.showEditChannelPurposeModal} + onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} + channel={channel} + /> + ); } var collapseButtons = this.createCollapseButtons(currentId); @@ -443,11 +470,8 @@ export default class Navbar extends React.Component { </div> </div> </nav> - <EditChannelPurposeModal - show={this.state.showEditChannelPurposeModal} - onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} - channel={channel} - /> + {editChannelHeaderModal} + {editChannelPurposeModal} <ChannelMembersModal show={this.state.showMembersModal} onModalDismissed={() => this.setState({showMembersModal: false})} diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index c0230fe5f..c286ee6f9 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -5,9 +5,11 @@ import * as Utils from '../utils/utils.jsx'; import * as client from '../utils/client.jsx'; import UserStore from '../stores/user_store.jsx'; import TeamStore from '../stores/team_store.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import AboutBuildModal from './about_build_modal.jsx'; -import InviteMemberModal from './invite_member_modal.jsx'; +import TeamMembersModal from './team_members_modal.jsx'; +import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import Constants from '../utils/constants.jsx'; @@ -93,7 +95,7 @@ export default class NavbarDropdown extends React.Component { <li> <a href='#' - onClick={InviteMemberModal.show} + onClick={EventHelpers.showInviteMemberModal} > {'Invite New Member'} </a> @@ -105,10 +107,7 @@ export default class NavbarDropdown extends React.Component { <li> <a href='#' - data-toggle='modal' - data-target='#get_link' - data-title='Team Invite' - data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id} + onClick={EventHelpers.showGetTeamInviteLinkModal} > {'Get Team Invite Link'} </a> @@ -120,13 +119,9 @@ export default class NavbarDropdown extends React.Component { if (isAdmin) { manageLink = ( <li> - <a - href='#' - data-toggle='modal' - data-target='#team_members' - > + <ToggleModalButton dialogType={TeamMembersModal}> {'Manage Members'} - </a> + </ToggleModalButton> </li> ); diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index bc6e8470d..cedb2b59b 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -1,11 +1,11 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import DeletePostModal from './delete_post_modal.jsx'; import UserStore from '../stores/user_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import * as Utils from '../utils/utils.jsx'; import TimeSince from './time_since.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import Constants from '../utils/constants.jsx'; @@ -74,7 +74,7 @@ export default class PostInfo extends React.Component { <a href='#' role='menuitem' - onClick={() => DeletePostModal.show(post, dataComments)} + onClick={() => EventHelpers.showDeletePostModal(post, dataComments)} > {'Delete'} </a> diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 3e555c85a..7aae5177e 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -8,13 +8,13 @@ import UserStore from '../stores/user_store.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; -import DeletePostModal from './delete_post_modal.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; export default class RhsComment extends React.Component { constructor(props) { @@ -115,7 +115,7 @@ export default class RhsComment extends React.Component { <a href='#' role='menuitem' - onClick={() => DeletePostModal.show(post, 0)} + onClick={() => EventHelpers.showDeletePostModal(post, 0)} > {'Delete'} </a> diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 96f43bdb5..3d3d9e13f 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -6,11 +6,11 @@ import UserProfile from './user_profile.jsx'; import UserStore from '../stores/user_store.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; import * as utils from '../utils/utils.jsx'; -import DeletePostModal from './delete_post_modal.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import twemoji from 'twemoji'; import Constants from '../utils/constants.jsx'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; export default class RhsRootPost extends React.Component { constructor(props) { @@ -38,7 +38,9 @@ export default class RhsRootPost extends React.Component { } render() { var post = this.props.post; - var isOwner = UserStore.getCurrentId() === post.user_id; + var currentUser = UserStore.getCurrentUser(); + var isOwner = currentUser.id === post.user_id; + var isAdmin = utils.isAdmin(currentUser.roles); var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); @@ -61,11 +63,54 @@ export default class RhsRootPost extends React.Component { } } - var ownerOptions; + var dropdownContents = []; + if (isOwner) { - ownerOptions = ( - <div> - <a href='#' + dropdownContents.push( + <li + key='rhs-root-edit' + role='presentation' + > + <a + href='#' + role='menuitem' + data-toggle='modal' + data-target='#edit_post' + data-refocusid='#reply_textbox' + data-title={type} + data-message={post.message} + data-postid={post.id} + data-channelid={post.channel_id} + > + {'Edit'} + </a> + </li> + ); + } + + if (isOwner || isAdmin) { + dropdownContents.push( + <li + key='rhs-root-delete' + role='presentation' + > + <a + href='#' + role='menuitem' + onClick={() => EventHelpers.showDeletePostModal(post, this.props.commentCount)} + > + {'Delete'} + </a> + </li> + ); + } + + var rootOptions = ''; + if (dropdownContents.length > 0) { + rootOptions = ( + <div className='dropdown'> + <a + href='#' className='post__dropdown dropdown-toggle' type='button' data-toggle='dropdown' @@ -75,30 +120,7 @@ export default class RhsRootPost extends React.Component { className='dropdown-menu' role='menu' > - <li role='presentation'> - <a - href='#' - role='menuitem' - data-toggle='modal' - data-target='#edit_post' - data-refocusid='#reply_textbox' - data-title={type} - data-message={post.message} - data-postid={post.id} - data-channelid={post.channel_id} - > - {'Edit'} - </a> - </li> - <li role='presentation'> - <a - href='#' - role='menuitem' - onClick={() => DeletePostModal.show(post, this.props.commentCount)} - > - {'Delete'} - </a> - </li> + {dropdownContents} </ul> </div> ); @@ -166,7 +188,7 @@ export default class RhsRootPost extends React.Component { </li> <li className='col col__reply'> <div className='dropdown'> - {ownerOptions} + {rootOptions} </div> </li> </ul> diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index f6c0c8adb..1f268a2f7 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -1,11 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import InviteMemberModal from './invite_member_modal.jsx'; +import TeamMembersModal from './team_members_modal.jsx'; +import ToggleModalButton from './toggle_modal_button.jsx'; import UserSettingsModal from './user_settings/user_settings_modal.jsx'; import UserStore from '../stores/user_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; import * as client from '../utils/client.jsx'; +import * as EventHelpers from '../dispatcher/event_helpers.jsx'; import * as utils from '../utils/utils.jsx'; export default class SidebarRightMenu extends React.Component { @@ -46,7 +47,7 @@ export default class SidebarRightMenu extends React.Component { <li> <a href='#' - onClick={InviteMemberModal.show} + onClick={EventHelpers.showInviteMemberModal} > <i className='fa fa-user'></i>Invite New Member </a> @@ -56,12 +57,12 @@ export default class SidebarRightMenu extends React.Component { if (this.props.teamType === 'O') { teamLink = ( <li> - <a href='#' - data-toggle='modal' - data-target='#get_link' - data-title='Team Invite' - data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id} - ><i className='fa fa-link'></i>Get Team Invite Link</a> + <a + href='#' + onClick={EventHelpers.showGetTeamInviteLinkModal} + > + <i className='glyphicon glyphicon-link'></i>{'Get Team Invite Link'} + </a> </li> ); } @@ -79,12 +80,9 @@ export default class SidebarRightMenu extends React.Component { ); manageLink = ( <li> - <a - href='#' - data-toggle='modal' - data-target='#team_members' - > - <i className='fa fa-users'></i>Manage Members</a> + <ToggleModalButton dialogType={TeamMembersModal}> + <i className='fa fa-users'></i>{'Manage Members'} + </ToggleModalButton> </li> ); } diff --git a/web/react/components/team_members.jsx b/web/react/components/team_members.jsx deleted file mode 100644 index cd0766012..000000000 --- a/web/react/components/team_members.jsx +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import UserStore from '../stores/user_store.jsx'; -import MemberListTeam from './member_list_team.jsx'; -import * as utils from '../utils/utils.jsx'; - -function getStateFromStores() { - var users = UserStore.getProfiles(); - var memberList = []; - for (var id in users) { - if (users.hasOwnProperty(id)) { - memberList.push(users[id]); - } - } - - memberList.sort(function sort(a, b) { - if (a.username < b.username) { - return -1; - } - - if (a.username > b.username) { - return 1; - } - - return 0; - }); - - return { - member_list: memberList - }; -} - -export default class TeamMembers extends React.Component { - constructor(props) { - super(props); - - this.onChange = this.onChange.bind(this); - - this.state = getStateFromStores(); - } - - componentDidMount() { - UserStore.addChangeListener(this.onChange); - - var self = this; - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function show() { - self.setState({render_members: false}); - }); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function hide() { - self.setState({render_members: true}); - }); - } - - componentWillUnmount() { - UserStore.removeChangeListener(this.onChange); - } - - onChange() { - var newState = getStateFromStores(); - if (!utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } - } - - render() { - var serverError = null; - - if (this.state.server_error) { - serverError = <label className='has-error control-label'>{this.state.server_error}</label>; - } - - var renderMembers = ''; - - if (this.state.render_members) { - renderMembers = <MemberListTeam users={this.state.member_list} />; - } - - return ( - <div - className='modal fade more-modal' - ref='modal' - id='team_members' - tabIndex='-1' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>×</span> - </button> - <h4 - className='modal-title' - id='myModalLabel' - >{this.props.teamDisplayName + ' Members'}</h4> - </div> - <div - ref='modalBody' - className='modal-body' - > - <div className='team-member-list'> - {renderMembers} - </div> - {serverError} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - >Close</button> - </div> - </div> - </div> - </div> - ); - } -} - -TeamMembers.propTypes = { - teamDisplayName: React.PropTypes.string -}; diff --git a/web/react/components/team_members_modal.jsx b/web/react/components/team_members_modal.jsx new file mode 100644 index 000000000..0a30a2202 --- /dev/null +++ b/web/react/components/team_members_modal.jsx @@ -0,0 +1,69 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import MemberListTeam from './member_list_team.jsx'; +import TeamStore from '../stores/team_store.jsx'; + +const Modal = ReactBootstrap.Modal; + +export default class TeamMembersModal extends React.Component { + constructor(props) { + super(props); + + this.onShow = this.onShow.bind(this); + } + + componentDidMount() { + if (this.props.show) { + this.onShow(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.show && !prevProps.show) { + this.onShow(); + } + } + + onShow() { + $(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 300); + if ($(window).width() > 768) { + $(ReactDOM.findDOMNode(this.refs.modalBody)).perfectScrollbar(); + } + } + + render() { + const team = TeamStore.getCurrent(); + + return ( + <Modal + dialogClassName='team-members-modal' + show={this.props.show} + onHide={this.props.onHide} + > + <Modal.Header closeButton={true}> + {team.display_name + ' Members'} + </Modal.Header> + <Modal.Body ref='modalBody'> + <div className='team-member-list'> + <MemberListTeam /> + </div> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.props.onHide} + > + {'Close'} + </button> + </Modal.Footer> + </Modal> + ); + } +} + +TeamMembersModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onHide: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/toggle_modal_button.jsx b/web/react/components/toggle_modal_button.jsx index eae4a024d..ce8ff3f60 100644 --- a/web/react/components/toggle_modal_button.jsx +++ b/web/react/components/toggle_modal_button.jsx @@ -22,7 +22,17 @@ export default class ModalToggleButton extends React.Component { } render() { - const {children, dialogType, dialogProps, ...props} = this.props; //eslint-disable-line no-redeclare + const {children, dialogType, dialogProps, onClick, ...props} = this.props; // eslint-disable-line no-redeclare + + // allow callers to provide an onClick which will be called before the modal is shown + let clickHandler = this.show; + if (onClick) { + clickHandler = () => { + onClick(); + + this.show(); + }; + } // this assumes that all modals will have a show property and an onHide event const dialog = React.createElement(this.props.dialogType, Object.assign({}, dialogProps, { @@ -42,7 +52,7 @@ export default class ModalToggleButton extends React.Component { <a {...props} href='#' - onClick={this.show} + onClick={clickHandler} > {children} {dialog} @@ -54,7 +64,8 @@ export default class ModalToggleButton extends React.Component { ModalToggleButton.propTypes = { children: React.PropTypes.node.isRequired, dialogType: React.PropTypes.func.isRequired, - dialogProps: React.PropTypes.object + dialogProps: React.PropTypes.object, + onClick: React.PropTypes.func }; ModalToggleButton.defaultProps = { diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index b3ec7ddd7..962efd7a2 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -438,12 +438,12 @@ export default class UserSettingsGeneralTab extends React.Component { if (this.props.activeSection === 'email') { const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; - let helpText = 'Email is used for notifications, and requires verification if changed.'; + let helpText = 'Email is used for sign-in, notifications, and password reset. Email requires verification if changed.'; if (!emailEnabled) { helpText = <div className='setting-list__hint text-danger'>{'Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'}</div>; } else if (!emailVerificationEnabled) { - helpText = 'Email is used for notifications.'; + helpText = 'Email is used for sign-in, notifications, and password reset.'; } else if (this.state.emailChangeInProgress) { const newEmail = UserStore.getCurrentUser().email; if (newEmail) { |