diff options
Diffstat (limited to 'web/react')
24 files changed, 744 insertions, 67 deletions
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx index 9ecd14a1e..6587184ea 100644 --- a/web/react/components/admin_console/team_settings.jsx +++ b/web/react/components/admin_console/team_settings.jsx @@ -32,6 +32,7 @@ export default class TeamSettings extends React.Component { config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked; config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked; config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked; + config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked; var MaxUsersPerTeam = 50; if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) { @@ -243,6 +244,39 @@ export default class TeamSettings extends React.Component { </div> <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableTeamListing' + > + {'Enable Team Directory: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableTeamListing' + value='true' + ref='EnableTeamListing' + defaultChecked={this.props.config.TeamSettings.EnableTeamListing} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableTeamListing' + value='false' + defaultChecked={!this.props.config.TeamSettings.EnableTeamListing} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.'}</p> + </div> + </div> + + <div className='form-group'> <div className='col-sm-12'> {serverError} <button diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 6f3826f75..5b3c74e82 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component { className='modal-title' ref='title' > - Edit Header + {'Edit Header'} </h4> ); if (this.state.title) { @@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component { className='modal-title' ref='title' > - Edit Header for <span className='name'>{this.state.title}</span> + {'Edit Header for '}<span className='name'>{this.state.title}</span> </h4> ); } @@ -105,11 +105,12 @@ export default class EditChannelModal extends React.Component { data-dismiss='modal' aria-label='Close' > - <span aria-hidden='true'>×</span> + <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' @@ -125,14 +126,14 @@ export default class EditChannelModal extends React.Component { className='btn btn-default' data-dismiss='modal' > - Cancel + {'Cancel'} </button> <button type='button' className='btn btn-primary' onClick={this.handleEdit} > - Save + {'Save'} </button> </div> </div> diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx index d8102642e..4d162cfe7 100644 --- a/web/react/components/edit_channel_purpose_modal.jsx +++ b/web/react/components/edit_channel_purpose_modal.jsx @@ -69,6 +69,11 @@ export default class EditChannelPurposeModal extends React.Component { title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>; } + let channelTerm = 'Channel'; + if (this.props.channel.channelType === 'P') { + channelTerm = 'Group'; + } + return ( <Modal className='modal-edit-channel-purpose' @@ -81,6 +86,7 @@ export default class EditChannelPurposeModal extends React.Component { </Modal.Title> </Modal.Header> <Modal.Body> + <p>{`Describe how this ${channelTerm} should be used.`}</p> <textarea ref='purpose' className='form-control no-resize' diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index e5bede026..2abb3f151 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -6,6 +6,10 @@ var AsyncClient = require('../utils/async_client.jsx'); var Textbox = require('./textbox.jsx'); var BrowserStore = require('../stores/browser_store.jsx'); var PostStore = require('../stores/post_store.jsx'); +var PreferenceStore = require('../stores/preference_store.jsx'); + +var Constants = require('../utils/constants.jsx'); +var KeyCodes = Constants.KeyCodes; export default class EditPostModal extends React.Component { constructor() { @@ -16,6 +20,8 @@ export default class EditPostModal extends React.Component { this.handleEditKeyPress = this.handleEditKeyPress.bind(this); this.handleUserInput = this.handleUserInput.bind(this); this.handleEditPostEvent = this.handleEditPostEvent.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''}; } @@ -51,7 +57,7 @@ export default class EditPostModal extends React.Component { this.setState({editText: editMessage}); } handleEditKeyPress(e) { - if (e.which === 13 && !e.shiftKey && !e.altKey) { + if (this.state.ctrlSend === 'false' && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) { e.preventDefault(); ReactDOM.findDOMNode(this.refs.editbox).blur(); this.handleEdit(e); @@ -72,6 +78,16 @@ export default class EditPostModal extends React.Component { $(ReactDOM.findDOMNode(this.refs.modal)).modal('show'); } + handleKeyDown(e) { + if (this.state.ctrlSend === 'true' && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) { + this.handleEdit(e); + } + } + onPreferenceChange() { + this.setState({ + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); + } componentDidMount() { var self = this; @@ -101,9 +117,11 @@ export default class EditPostModal extends React.Component { }); PostStore.addEditPostListener(this.handleEditPostEvent); + PreferenceStore.addChangeListener(this.onPreferenceChange); } componentWillUnmount() { PostStore.removeEditPostListener(this.handleEditPostEvent); + PreferenceStore.removeChangeListener(this.onPreferenceChange); } render() { var error = (<div className='form-group'><br /></div>); @@ -138,6 +156,7 @@ export default class EditPostModal extends React.Component { <Textbox onUserInput={this.handleEditInput} onKeyPress={this.handleEditKeyPress} + onKeyDown={this.handleKeyDown} messageText={this.state.editText} createMessage='Edit the post...' id='edit_textbox' diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 86a4b04cf..bea700725 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -4,6 +4,7 @@ var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); export default class InviteMemberModal extends React.Component { @@ -292,7 +293,7 @@ export default class InviteMemberModal extends React.Component { } else { var teamInviteLink = null; if (currentUser && this.props.teamType === 'O') { - var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id; + var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id; var link = ( <a diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 108735caf..c519959af 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -101,7 +101,7 @@ export default class Login extends React.Component { href={'/' + teamName + '/login/gitlab'} > <span className='icon' /> - <span>with GitLab</span> + <span>{'with GitLab'}</span> </a> ); } @@ -154,7 +154,7 @@ export default class Login extends React.Component { type='submit' className='btn btn-primary' > - Sign in + {'Sign in'} </button> </div> </div> @@ -166,7 +166,7 @@ export default class Login extends React.Component { <div> {loginMessage} <div className='or__container'> - <span>or</span> + <span>{'or'}</span> </div> </div> ); @@ -176,16 +176,48 @@ export default class Login extends React.Component { if (emailSignup) { forgotPassword = ( <div className='form-group'> - <a href={'/' + teamName + '/reset_password'}>I forgot my password</a> + <a href={'/' + teamName + '/reset_password'}>{'I forgot my password'}</a> + </div> + ); + } + + let userSignUp = null; + if (this.props.inviteId) { + userSignUp = ( + <div> + <span>{'Do not have an account? '} + <a + href={'/signup_user_complete/?id=' + this.props.inviteId} + className='signup-team-login' + > + {'Create one now'} + </a> + </span> + </div> + ); + } + + let teamSignUp = null; + if (global.window.mm_config.EnableTeamCreation === 'true') { + teamSignUp = ( + <div className='margin--extra'> + <span>{'Want to create your own team? '} + <a + href='/' + className='signup-team-login' + > + {'Sign up now'} + </a> + </span> </div> ); } return ( <div className='signup-team__container'> - <h5 className='margin--less'>Sign in to:</h5> + <h5 className='margin--less'>{'Sign in to:'}</h5> <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2> + <h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2> <form onSubmit={this.handleSubmit}> {verifiedBox} <div className={'form-group' + errorClass}> @@ -193,20 +225,12 @@ export default class Login extends React.Component { </div> {loginMessage} {emailSignup} + {userSignUp} <div className='form-group margin--extra form-group--small'> <span><a href='/find_team'>{'Find other teams'}</a></span> </div> {forgotPassword} - <div className='margin--extra'> - <span>{'Want to create your own team? '} - <a - href='/' - className='signup-team-login' - > - Sign up now - </a> - </span> - </div> + {teamSignUp} </form> </div> ); @@ -219,5 +243,6 @@ Login.defaultProps = { }; Login.propTypes = { teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + teamDisplayName: React.PropTypes.string, + inviteId: React.PropTypes.string }; diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 2b0f3c40e..dc21fad21 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -112,7 +112,7 @@ export default class NavbarDropdown extends React.Component { data-toggle='modal' data-target='#get_link' data-title='Team Invite' - data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id} + data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id} > {'Get Team Invite Link'} </a> diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index fddc98c9d..9350bbd42 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); @@ -51,7 +52,7 @@ export default class SidebarRightMenu extends React.Component { data-toggle='modal' data-target='#get_link' data-title='Team Invite' - data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id} + data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id} ><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a> </li> ); diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 1858703ef..f926f5cbb 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -12,6 +12,11 @@ export default class TeamSignUp extends React.Component { this.updatePage = this.updatePage.bind(this); + if (global.window.mm_config.EnableTeamListing === 'true') { + this.state = {page: 'team_listing'}; + return; + } + var count = 0; if (global.window.mm_config.EnableSignUpWithEmail === 'true') { @@ -36,6 +41,38 @@ export default class TeamSignUp extends React.Component { } render() { + if (this.state.page === 'team_listing') { + return ( + <div> + <h3>{'Choose a Team'}</h3> + <div className='signup-team-all'> + { + this.props.teams.map((team) => { + return ( + <div + key={'team_' + team.name} + className='signup-team-dir' + > + <a + href={'/' + team.name} + > + <div className='signup-team-dir__group'> + <span className='signup-team-dir__name'>{team.display_name}</span> + <span + className='glyphicon glyphicon-menu-right right signup-team-dir__arrow' + aria-hidden='true' + /> + </div> + </a> + </div> + ); + }) + } + </div> + </div> + ); + } + if (this.state.page === 'choose') { return ( <ChoosePage @@ -51,3 +88,8 @@ export default class TeamSignUp extends React.Component { } } } + +TeamSignUp.propTypes = { + teams: React.PropTypes.array +}; + diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 923180e27..69ba44664 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -6,29 +6,112 @@ const SettingItemMax = require('./setting_item_max.jsx'); const Client = require('../utils/client.jsx'); const Utils = require('../utils/utils.jsx'); +const TeamStore = require('../stores/team_store.jsx'); export default class GeneralTab extends React.Component { constructor(props) { super(props); this.handleNameSubmit = this.handleNameSubmit.bind(this); + this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this); + this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this); + this.handleTeamListingSubmit = this.handleTeamListingSubmit.bind(this); this.handleClose = this.handleClose.bind(this); - this.onUpdateSection = this.onUpdateSection.bind(this); + this.onUpdateNameSection = this.onUpdateNameSection.bind(this); this.updateName = this.updateName.bind(this); + this.onUpdateInviteIdSection = this.onUpdateInviteIdSection.bind(this); + this.updateInviteId = this.updateInviteId.bind(this); + this.onUpdateOpenInviteSection = this.onUpdateOpenInviteSection.bind(this); + this.handleOpenInviteRadio = this.handleOpenInviteRadio.bind(this); + this.onUpdateTeamListingSection = this.onUpdateTeamListingSection.bind(this); + this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this); + this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this); - this.state = {name: this.props.teamDisplayName, serverError: '', clientError: ''}; + this.state = { + name: props.team.display_name, + invite_id: props.team.invite_id, + allow_open_invite: props.team.allow_open_invite, + allow_team_listing: props.team.allow_team_listing, + serverError: '', + clientError: '' + }; } + + handleGenerateInviteId(e) { + e.preventDefault(); + + var newId = ''; + for (var i = 0; i < 32; i++) { + newId += Math.floor(Math.random() * 16).toString(16); + } + + this.setState({invite_id: newId}); + } + + handleOpenInviteRadio(openInvite) { + this.setState({allow_open_invite: openInvite}); + } + + handleTeamListingRadio(listing) { + if (global.window.mm_config.EnableTeamListing !== 'true' && listing) { + ReactDOM.findDOMNode(this.refs.teamListingRadioNo).checked = true; + this.setState({clientError: 'Team directory has been disabled. Please ask a system admin to enable it.'}); + } else { + this.setState({allow_team_listing: listing}); + } + } + + handleOpenInviteSubmit(e) { + e.preventDefault(); + + var state = {serverError: '', clientError: ''}; + + var data = this.props.team; + data.allow_open_invite = this.state.allow_open_invite; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); + this.props.updateSection(''); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + + handleTeamListingSubmit(e) { + e.preventDefault(); + + var state = {serverError: '', clientError: ''}; + + var data = this.props.team; + data.allow_team_listing = this.state.allow_team_listing; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); + this.props.updateSection(''); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + handleNameSubmit(e) { e.preventDefault(); - let state = {serverError: '', clientError: ''}; + var state = {serverError: '', clientError: ''}; let valid = true; const name = this.state.name.trim(); if (!name) { state.clientError = 'This field is required'; valid = false; - } else if (name === this.props.teamDisplayName) { + } else if (name === this.props.team.display_name) { state.clientError = 'Please choose a new name for your team'; valid = false; } else { @@ -41,37 +124,76 @@ export default class GeneralTab extends React.Component { return; } - let data = {}; - data.new_name = name; + var data = this.props.team; + data.display_name = this.state.name; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); + this.props.updateSection(''); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + + handleInviteIdSubmit(e) { + e.preventDefault(); + + var state = {serverError: '', clientError: ''}; + let valid = true; - Client.updateTeamDisplayName(data, - function nameChangeSuccess() { + const inviteId = this.state.invite_id.trim(); + if (inviteId) { + state.clientError = ''; + } else { + state.clientError = 'This field is required'; + valid = false; + } + + this.setState(state); + + if (!valid) { + return; + } + + var data = this.props.team; + data.invite_id = this.state.invite_id; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); this.props.updateSection(''); - $('#team_settings').modal('hide'); - window.location.reload(); - }.bind(this), - function nameChangeFail(err) { + }, + (err) => { state.serverError = err.message; this.setState(state); - }.bind(this) + } ); } + componentWillReceiveProps(newProps) { if (newProps.team && newProps.teamDisplayName) { this.setState({name: newProps.teamDisplayName}); } } + handleClose() { this.setState({clientError: '', serverError: ''}); this.props.updateSection(''); } + componentDidMount() { $('#team_settings').on('hidden.bs.modal', this.handleClose); } + componentWillUnmount() { $('#team_settings').off('hidden.bs.modal', this.handleClose); } - onUpdateSection(e) { + + onUpdateNameSection(e) { e.preventDefault(); if (this.props.activeSection === 'name') { this.props.updateSection(''); @@ -79,10 +201,44 @@ export default class GeneralTab extends React.Component { this.props.updateSection('name'); } } + + onUpdateInviteIdSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'invite_id') { + this.props.updateSection(''); + } else { + this.props.updateSection('invite_id'); + } + } + + onUpdateOpenInviteSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'open_invite') { + this.props.updateSection(''); + } else { + this.props.updateSection('open_invite'); + } + } + + onUpdateTeamListingSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'team_listing') { + this.props.updateSection(''); + } else { + this.props.updateSection('team_listing'); + } + } + updateName(e) { e.preventDefault(); this.setState({name: e.target.value}); } + + updateInviteId(e) { + e.preventDefault(); + this.setState({invite_id: e.target.value}); + } + render() { let clientError = null; let serverError = null; @@ -93,10 +249,180 @@ export default class GeneralTab extends React.Component { serverError = this.state.serverError; } + let teamListingSection; + if (this.props.activeSection === 'team_listing') { + const inputs = [ + <div key='userTeamListingOptions'> + <div className='radio'> + <label> + <input + name='userTeamListingOptions' + type='radio' + defaultChecked={this.state.allow_team_listing} + onChange={this.handleTeamListingRadio.bind(this, true)} + /> + {'Yes'} + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + ref='teamListingRadioNo' + name='userTeamListingOptions' + type='radio' + defaultChecked={!this.state.allow_team_listing} + onChange={this.handleTeamListingRadio.bind(this, false)} + /> + {'No'} + </label> + <br/> + </div> + <div><br/>{'When allowed the team will appear on the main page as part of team directory.'}</div> + </div> + ]; + + teamListingSection = ( + <SettingItemMax + title='Allow in Team Directory' + inputs={inputs} + submit={this.handleTeamListingSubmit} + server_error={serverError} + client_error={clientError} + updateSection={this.onUpdateTeamListingSection} + /> + ); + } else { + let describe = ''; + if (this.state.allow_team_listing === true) { + describe = 'Yes'; + } else { + describe = 'No'; + } + + teamListingSection = ( + <SettingItemMin + title='Allow in Team Directory' + describe={describe} + updateSection={this.onUpdateTeamListingSection} + /> + ); + } + + let openInviteSection; + if (this.props.activeSection === 'open_invite') { + const inputs = [ + <div key='userOpenInviteOptions'> + <div className='radio'> + <label> + <input + name='userOpenInviteOptions' + type='radio' + defaultChecked={this.state.allow_open_invite} + onChange={this.handleOpenInviteRadio.bind(this, true)} + /> + {'Yes'} + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + name='userOpenInviteOptions' + type='radio' + defaultChecked={!this.state.allow_open_invite} + onChange={this.handleOpenInviteRadio.bind(this, false)} + /> + {'No'} + </label> + <br/> + </div> + <div><br/>{'When allowed the team signup link will be included on the login page and anyone can signup to this team.'}</div> + </div> + ]; + + openInviteSection = ( + <SettingItemMax + title='Allow Open Invitations' + inputs={inputs} + submit={this.handleOpenInviteSubmit} + server_error={serverError} + updateSection={this.onUpdateOpenInviteSection} + /> + ); + } else { + let describe = ''; + if (this.state.allow_open_invite === true) { + describe = 'Yes'; + } else { + describe = 'No'; + } + + openInviteSection = ( + <SettingItemMin + title='Allow Open Invitations' + describe={describe} + updateSection={this.onUpdateOpenInviteSection} + /> + ); + } + + let inviteSection; + + if (this.props.activeSection === 'invite_id') { + const inputs = []; + + inputs.push( + <div + key='teamInviteSetting' + className='form-group' + > + <label className='col-sm-5 control-label'>{'Invite Code'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateInviteId} + value={this.state.invite_id} + maxLength='32' + /> + </div> + <div><br/>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div> + <div className='help-text'> + <button + className='btn btn-default' + onClick={this.handleGenerateInviteId} + > + {'Re-Generate'} + </button> + </div> + </div> + ); + + inviteSection = ( + <SettingItemMax + title={`Invite Code`} + inputs={inputs} + submit={this.handleInviteIdSubmit} + server_error={serverError} + client_error={clientError} + updateSection={this.onUpdateInviteIdSection} + /> + ); + } else { + inviteSection = ( + <SettingItemMin + title={`Invite Code`} + describe={`Click 'Edit' to re-generate invite Code.`} + updateSection={this.onUpdateInviteIdSection} + /> + ); + } + let nameSection; if (this.props.activeSection === 'name') { - let inputs = []; + const inputs = []; let teamNameLabel = 'Team Name'; if (Utils.isMobile()) { @@ -127,17 +453,17 @@ export default class GeneralTab extends React.Component { submit={this.handleNameSubmit} server_error={serverError} client_error={clientError} - updateSection={this.onUpdateSection} + updateSection={this.onUpdateNameSection} /> ); } else { - let describe = this.state.name; + var describe = this.state.name; nameSection = ( <SettingItemMin title={`Team Name`} describe={describe} - updateSection={this.onUpdateSection} + updateSection={this.onUpdateNameSection} /> ); } @@ -158,16 +484,19 @@ export default class GeneralTab extends React.Component { ref='title' > <i className='modal-back'></i> - General Settings + {'General Settings'} </h4> </div> <div ref='wrapper' className='user-settings' > - <h3 className='tab-header'>General Settings</h3> + <h3 className='tab-header'>{'General Settings'}</h3> <div className='divider-dark first'/> {nameSection} + {openInviteSection} + {teamListingSection} + {inviteSection} <div className='divider-dark'/> </div> </div> @@ -178,6 +507,5 @@ export default class GeneralTab extends React.Component { GeneralTab.propTypes = { updateSection: React.PropTypes.func.isRequired, team: React.PropTypes.object.isRequired, - activeSection: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + activeSection: React.PropTypes.string.isRequired }; diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index e14da4f04..09674f1ef 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -37,7 +37,6 @@ export default class TeamSettings extends React.Component { team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} - teamDisplayName={this.props.teamDisplayName} /> </div> ); @@ -72,12 +71,11 @@ export default class TeamSettings extends React.Component { TeamSettings.defaultProps = { activeTab: '', - activeSection: '', - teamDisplayName: '' + activeSection: '' }; + TeamSettings.propTypes = { activeTab: React.PropTypes.string.isRequired, activeSection: React.PropTypes.string.isRequired, - updateSection: React.PropTypes.func.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + updateSection: React.PropTypes.func.isRequired }; diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index 5c5995020..17fe31c65 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -82,7 +82,6 @@ export default class TeamSettingsModal extends React.Component { activeTab={this.state.activeTab} activeSection={this.state.activeSection} updateSection={this.updateSection} - teamDisplayName={this.props.teamDisplayName} /> </div> </div> @@ -95,5 +94,4 @@ export default class TeamSettingsModal extends React.Component { } TeamSettingsModal.propTypes = { - teamDisplayName: React.PropTypes.string.isRequired }; diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx new file mode 100644 index 000000000..eef4b24ba --- /dev/null +++ b/web/react/components/user_settings/code_theme_chooser.jsx @@ -0,0 +1,55 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +var Constants = require('../../utils/constants.jsx'); + +export default class CodeThemeChooser extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + render() { + const theme = this.props.theme; + + const premadeThemes = []; + for (const k in Constants.CODE_THEMES) { + if (Constants.CODE_THEMES.hasOwnProperty(k)) { + let activeClass = ''; + if (k === theme.codeTheme) { + activeClass = 'active'; + } + + premadeThemes.push( + <div + className='col-xs-6 col-sm-3 premade-themes' + key={'premade-theme-key' + k} + > + <div + className={activeClass} + onClick={() => this.props.updateTheme(k)} + > + <label> + <img + className='img-responsive' + src={'/static/images/themes/code_themes/' + k + '.png'} + /> + <div className='theme-label'>{Constants.CODE_THEMES[k]}</div> + </label> + </div> + </div> + ); + } + } + + return ( + <div className='row'> + {premadeThemes} + </div> + ); + } +} + +CodeThemeChooser.propTypes = { + theme: React.PropTypes.object.isRequired, + updateTheme: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 44b3f4544..095e5b622 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -40,11 +40,12 @@ export default class CustomThemeChooser extends React.Component { const theme = {type: 'custom'}; let index = 0; Constants.THEME_ELEMENTS.forEach((element) => { - if (index < colors.length) { + if (index < colors.length - 1) { theme[element.id] = colors[index]; } index++; }); + theme.codeTheme = colors[colors.length - 1]; this.props.updateTheme(theme); } @@ -78,6 +79,8 @@ export default class CustomThemeChooser extends React.Component { colors += theme[element.id] + ','; }); + colors += theme.codeTheme; + const pasteBox = ( <div className='col-sm-12'> <label className='custom-label'> diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index 8c62a189d..7b4b54e27 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -7,6 +7,7 @@ var Utils = require('../../utils/utils.jsx'); const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); +const CodeThemeChooser = require('./code_theme_chooser.jsx'); const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -18,12 +19,14 @@ export default class UserSettingsAppearance extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); + this.updateCodeTheme = this.updateCodeTheme.bind(this); this.handleClose = this.handleClose.bind(this); this.handleImportModal = this.handleImportModal.bind(this); this.state = this.getStateFromStores(); this.originalTheme = this.state.theme; + this.originalCodeTheme = this.state.theme.codeTheme; } componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -58,6 +61,10 @@ export default class UserSettingsAppearance extends React.Component { type = 'custom'; } + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; + } + return {theme, type}; } onChange() { @@ -93,6 +100,15 @@ export default class UserSettingsAppearance extends React.Component { ); } updateTheme(theme) { + if (!theme.codeTheme) { + theme.codeTheme = this.state.theme.codeTheme; + } + this.setState({theme}); + Utils.applyTheme(theme); + } + updateCodeTheme(codeTheme) { + var theme = this.state.theme; + theme.codeTheme = codeTheme; this.setState({theme}); Utils.applyTheme(theme); } @@ -102,6 +118,7 @@ export default class UserSettingsAppearance extends React.Component { handleClose() { const state = this.getStateFromStores(); state.serverError = null; + state.theme.codeTheme = this.originalCodeTheme; Utils.applyTheme(state.theme); @@ -170,7 +187,13 @@ export default class UserSettingsAppearance extends React.Component { </div> {custom} <hr /> - {serverError} + <strong className='radio'>{'Code Theme'}</strong> + <CodeThemeChooser + theme={this.state.theme} + updateTheme={this.updateCodeTheme} + /> + <hr /> + {serverError} <a className='btn btn-sm btn-primary' href='#' diff --git a/web/react/package.json b/web/react/package.json index e6a662375..9af6f5880 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -6,6 +6,7 @@ "autolinker": "0.18.1", "babel-runtime": "5.8.24", "flux": "2.1.1", + "highlight.js": "^8.9.1", "keymirror": "0.1.1", "marked": "0.3.5", "object-assign": "3.0.0", diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 03e049db0..7a04c5979 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -90,7 +90,7 @@ function setupChannelPage(props) { ); ReactDOM.render( - <TeamSettingsModal teamDisplayName={props.TeamDisplayName} />, + <TeamSettingsModal />, document.getElementById('team_settings_modal') ); diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx index 430de980c..9865e6fd2 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -8,6 +8,7 @@ function setupLoginPage(props) { <Login teamDisplayName={props.TeamDisplayName} teamName={props.TeamName} + inviteId={props.InviteId} />, document.getElementById('login') ); diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index dc8394a77..caa93b5bf 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -3,9 +3,19 @@ var SignupTeam = require('../components/signup_team.jsx'); -function setupSignupTeamPage() { +function setupSignupTeamPage(props) { + var teams = []; + + for (var prop in props) { + if (props.hasOwnProperty(prop)) { + if (prop !== 'Title') { + teams.push({name: prop, display_name: props[prop]}); + } + } + } + ReactDOM.render( - <SignupTeam />, + <SignupTeam teams={teams} />, document.getElementById('signup-team') ); } diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 4d69a6716..4efeb7c8f 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -165,7 +165,7 @@ function handleNewPostEvent(msg) { } // Send desktop notification - if (UserStore.getCurrentId() !== msg.user_id) { + if (UserStore.getCurrentId() !== msg.user_id || post.props.from_webhook === 'true') { const msgProps = msg.props; let mentions = []; @@ -189,7 +189,9 @@ function handleNewPostEvent(msg) { } let username = 'Someone'; - if (UserStore.hasProfile(msg.user_id)) { + if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { + username = post.props.override_username; + } else if (UserStore.hasProfile(msg.user_id)) { username = UserStore.getProfile(msg.user_id).username; } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index aeb39d8a8..7ce1346f9 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -442,16 +442,16 @@ export function inviteMembers(data, success, error) { track('api', 'api_teams_invite_members'); } -export function updateTeamDisplayName(data, success, error) { +export function updateTeam(team, success, error) { $.ajax({ - url: '/api/v1/teams/update_name', + url: '/api/v1/teams/update', dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify(data), + data: JSON.stringify(team), success, - error: function onError(xhr, status, err) { - var e = handleError('updateTeamDisplayName', xhr, status, err); + error: (xhr, status, err) => { + var e = handleError('updateTeam', xhr, status, err); error(e); } }); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 43d81d322..1593f6706 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -304,6 +304,13 @@ module.exports = { uiName: 'Mention Highlight Link' } ], + CODE_THEMES: { + github: 'GitHub', + solarized_light: 'Solarized light', + monokai: 'Monokai', + solarized_dark: 'Solarized Dark' + }, + DEFAULT_CODE_THEME: 'github', Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', CATEGORY_DISPLAY_SETTINGS: 'display_settings', @@ -318,5 +325,30 @@ module.exports = { ENTER: 13, ESCAPE: 27, SPACE: 32 + }, + HighlightedLanguages: { + diff: 'Diff', + apache: 'Apache', + makefile: 'Makefile', + http: 'HTTP', + json: 'JSON', + markdown: 'Markdown', + javascript: 'JavaScript', + css: 'CSS', + nginx: 'nginx', + objectivec: 'Objective-C', + python: 'Python', + xml: 'XML', + perl: 'Perl', + bash: 'Bash', + php: 'PHP', + coffeescript: 'CoffeeScript', + cs: 'C#', + cpp: 'C++', + sql: 'SQL', + go: 'Go', + ruby: 'Ruby', + java: 'Java', + ini: 'ini' } }; diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index e34f3d00a..179416ea0 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -6,6 +6,34 @@ const Utils = require('./utils.jsx'); const marked = require('marked'); +const highlightJs = require('highlight.js/lib/highlight.js'); +const highlightJsDiff = require('highlight.js/lib/languages/diff.js'); +const highlightJsApache = require('highlight.js/lib/languages/apache.js'); +const highlightJsMakefile = require('highlight.js/lib/languages/makefile.js'); +const highlightJsHttp = require('highlight.js/lib/languages/http.js'); +const highlightJsJson = require('highlight.js/lib/languages/json.js'); +const highlightJsMarkdown = require('highlight.js/lib/languages/markdown.js'); +const highlightJsJavascript = require('highlight.js/lib/languages/javascript.js'); +const highlightJsCss = require('highlight.js/lib/languages/css.js'); +const highlightJsNginx = require('highlight.js/lib/languages/nginx.js'); +const highlightJsObjectivec = require('highlight.js/lib/languages/objectivec.js'); +const highlightJsPython = require('highlight.js/lib/languages/python.js'); +const highlightJsXml = require('highlight.js/lib/languages/xml.js'); +const highlightJsPerl = require('highlight.js/lib/languages/perl.js'); +const highlightJsBash = require('highlight.js/lib/languages/bash.js'); +const highlightJsPhp = require('highlight.js/lib/languages/php.js'); +const highlightJsCoffeescript = require('highlight.js/lib/languages/coffeescript.js'); +const highlightJsCs = require('highlight.js/lib/languages/cs.js'); +const highlightJsCpp = require('highlight.js/lib/languages/cpp.js'); +const highlightJsSql = require('highlight.js/lib/languages/sql.js'); +const highlightJsGo = require('highlight.js/lib/languages/go.js'); +const highlightJsRuby = require('highlight.js/lib/languages/ruby.js'); +const highlightJsJava = require('highlight.js/lib/languages/java.js'); +const highlightJsIni = require('highlight.js/lib/languages/ini.js'); + +const Constants = require('../utils/constants.jsx'); +const HighlightedLanguages = Constants.HighlightedLanguages; + class MattermostInlineLexer extends marked.InlineLexer { constructor(links, options) { super(links, options); @@ -51,6 +79,49 @@ class MattermostMarkdownRenderer extends marked.Renderer { this.text = this.text.bind(this); this.formattingOptions = formattingOptions; + + highlightJs.registerLanguage('diff', highlightJsDiff); + highlightJs.registerLanguage('apache', highlightJsApache); + highlightJs.registerLanguage('makefile', highlightJsMakefile); + highlightJs.registerLanguage('http', highlightJsHttp); + highlightJs.registerLanguage('json', highlightJsJson); + highlightJs.registerLanguage('markdown', highlightJsMarkdown); + highlightJs.registerLanguage('javascript', highlightJsJavascript); + highlightJs.registerLanguage('css', highlightJsCss); + highlightJs.registerLanguage('nginx', highlightJsNginx); + highlightJs.registerLanguage('objectivec', highlightJsObjectivec); + highlightJs.registerLanguage('python', highlightJsPython); + highlightJs.registerLanguage('xml', highlightJsXml); + highlightJs.registerLanguage('perl', highlightJsPerl); + highlightJs.registerLanguage('bash', highlightJsBash); + highlightJs.registerLanguage('php', highlightJsPhp); + highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript); + highlightJs.registerLanguage('cs', highlightJsCs); + highlightJs.registerLanguage('cpp', highlightJsCpp); + highlightJs.registerLanguage('sql', highlightJsSql); + highlightJs.registerLanguage('go', highlightJsGo); + highlightJs.registerLanguage('ruby', highlightJsRuby); + highlightJs.registerLanguage('java', highlightJsJava); + highlightJs.registerLanguage('ini', highlightJsIni); + } + + code(code, language) { + let usedLanguage = language; + + if (String(usedLanguage).toLocaleLowerCase() === 'html') { + usedLanguage = 'xml'; + } + + if (!usedLanguage || highlightJs.listLanguages().indexOf(usedLanguage) < 0) { + let parsed = super.code(code, usedLanguage); + return '<div class="post-body--code"><code class="hljs">' + TextFormatting.sanitizeHtml($(parsed).text()) + '</code></div>'; + } + + let parsed = highlightJs.highlight(usedLanguage, code); + return '<div class="post-body--code">' + + '<span class="post-body--code__language">' + HighlightedLanguages[usedLanguage] + '</span>' + + '<code class="hljs">' + parsed.value + '</code>' + + '</div>'; } br() { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 35ce49ae2..c7c8549b9 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -424,6 +424,11 @@ export function toTitleCase(str) { } export function applyTheme(theme) { + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; + } + updateCodeTheme(theme.codeTheme); + if (theme.sidebarBg) { changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } @@ -612,6 +617,27 @@ export function rgb2hex(rgbIn) { return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); } +export function updateCodeTheme(theme) { + const path = '/static/css/highlight/' + theme + '.css'; + const $link = $('link.code_theme'); + if (path !== $link.attr('href')) { + changeCss('code.hljs', 'visibility: hidden'); + var xmlHTTP = new XMLHttpRequest(); + xmlHTTP.open('GET', path, true); + xmlHTTP.onload = function onLoad() { + $link.attr('href', path); + if (isBrowserFirefox()) { + $link.one('load', () => { + changeCss('code.hljs', 'visibility: visible'); + }); + } else { + changeCss('code.hljs', 'visibility: visible'); + } + }; + xmlHTTP.send(); + } +} + export function placeCaretAtEnd(el) { el.focus(); if (typeof window.getSelection != 'undefined' && typeof document.createRange != 'undefined') { |