diff options
Diffstat (limited to 'web')
34 files changed, 635 insertions, 204 deletions
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 4f144b0dd..e587c4f84 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -18,6 +18,7 @@ import GitLabSettingsTab from './gitlab_settings.jsx'; import SqlSettingsTab from './sql_settings.jsx'; import TeamSettingsTab from './team_settings.jsx'; import ServiceSettingsTab from './service_settings.jsx'; +import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx'; import TeamUsersTab from './team_users.jsx'; import TeamAnalyticsTab from './team_analytics.jsx'; @@ -148,6 +149,8 @@ export default class AdminController extends React.Component { tab = <TeamSettingsTab config={this.state.config} />; } else if (this.state.selected === 'service_settings') { tab = <ServiceSettingsTab config={this.state.config} />; + } else if (this.state.selected === 'legal_and_support_settings') { + tab = <LegalAndSupportSettingsTab config={this.state.config} />; } else if (this.state.selected === 'team_users') { if (this.state.teams) { tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index cc98c495e..da445da37 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -108,7 +108,7 @@ export default class AdminSidebar extends React.Component { <a href='#' onClick={this.handleClick.bind(this, 'team_users', team.id)} - className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)} + className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id) + ' ' + this.isSelected('team_analytics', team.id)} > {team.name} <OverlayTrigger @@ -252,6 +252,15 @@ export default class AdminSidebar extends React.Component { {'GitLab Settings'} </a> </li> + <li> + <a + href='#' + className={this.isSelected('legal_and_support_settings')} + onClick={this.handleClick.bind(this, 'legal_and_support_settings', null)} + > + {'Legal and Support Settings'} + </a> + </li> </ul> <ul className='nav nav__sub-menu'> <li> diff --git a/web/react/components/admin_console/legal_and_support_settings.jsx b/web/react/components/admin_console/legal_and_support_settings.jsx new file mode 100644 index 000000000..b00e4b6bd --- /dev/null +++ b/web/react/components/admin_console/legal_and_support_settings.jsx @@ -0,0 +1,222 @@ +// 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 LegalAndSupportSettings extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.state = { + saveNeeded: false, + serverError: null + }; + } + + handleChange() { + var s = {saveNeeded: true, serverError: this.state.serverError}; + this.setState(s); + } + + handleSubmit(e) { + e.preventDefault(); + $('#save-button').button('loading'); + + var config = this.props.config; + + config.SupportSettings.TermsOfServiceLink = ReactDOM.findDOMNode(this.refs.TermsOfServiceLink).value.trim(); + config.SupportSettings.PrivacyPolicyLink = ReactDOM.findDOMNode(this.refs.PrivacyPolicyLink).value.trim(); + config.SupportSettings.AboutLink = ReactDOM.findDOMNode(this.refs.AboutLink).value.trim(); + config.SupportSettings.HelpLink = ReactDOM.findDOMNode(this.refs.HelpLink).value.trim(); + config.SupportSettings.ReportAProblemLink = ReactDOM.findDOMNode(this.refs.ReportAProblemLink).value.trim(); + config.SupportSettings.SupportEmail = ReactDOM.findDOMNode(this.refs.SupportEmail).value.trim(); + + Client.saveConfig( + config, + () => { + AsyncClient.getConfig(); + this.setState({ + serverError: null, + saveNeeded: false + }); + $('#save-button').button('reset'); + }, + (err) => { + this.setState({ + serverError: err.message, + saveNeeded: true + }); + $('#save-button').button('reset'); + } + ); + } + + render() { + var serverError = ''; + if (this.state.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; + } + + var saveClass = 'btn'; + if (this.state.saveNeeded) { + saveClass = 'btn btn-primary'; + } + + return ( + <div className='wrapper--fixed'> + + <h3>{'Legal and Support Settings'}</h3> + <form + className='form-horizontal' + role='form' + > + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='TermsOfServiceLink' + > + {'Terms of Service link:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='TermsOfServiceLink' + ref='TermsOfServiceLink' + defaultValue={this.props.config.SupportSettings.TermsOfServiceLink} + onChange={this.handleChange} + /> + <p className='help-text'>{'Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='PrivacyPolicyLink' + > + {'Privacy Policy link:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='PrivacyPolicyLink' + ref='PrivacyPolicyLink' + defaultValue={this.props.config.SupportSettings.PrivacyPolicyLink} + onChange={this.handleChange} + /> + <p className='help-text'>{'Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='AboutLink' + > + {'About link:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='AboutLink' + ref='AboutLink' + defaultValue={this.props.config.SupportSettings.AboutLink} + onChange={this.handleChange} + /> + <p className='help-text'>{'Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='HelpLink' + > + {'Help link:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='HelpLink' + ref='HelpLink' + defaultValue={this.props.config.SupportSettings.HelpLink} + onChange={this.handleChange} + /> + <p className='help-text'>{'Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='ReportAProblemLink' + > + {'Report a Problem link:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='ReportAProblemLink' + ref='ReportAProblemLink' + defaultValue={this.props.config.SupportSettings.ReportAProblemLink} + onChange={this.handleChange} + /> + <p className='help-text'>{'Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='SupportEmail' + > + {'Support email:'} + </label> + <div className='col-sm-8'> + <input + type='text' + className='form-control' + id='SupportEmail' + ref='SupportEmail' + defaultValue={this.props.config.SupportSettings.SupportEmail} + onChange={this.handleChange} + /> + <p className='help-text'>{'Email shown during tutorial for end users to ask support questions.'}</p> + </div> + </div> + + <div className='form-group'> + <div className='col-sm-12'> + {serverError} + <button + disabled={!this.state.saveNeeded} + type='submit' + className={saveClass} + onClick={this.handleSubmit} + id='save-button' + data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'} + > + {'Save'} + </button> + </div> + </div> + + </form> + </div> + ); + } +} + +LegalAndSupportSettings.propTypes = { + config: React.PropTypes.object +}; diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index 908eb709a..1f5faf1d4 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -36,7 +36,7 @@ export default class ServiceSettings extends React.Component { config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim(); config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim(); config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked; - config.ServiceSettings.EnableOutgoingWebhooks = React.findDOMNode(this.refs.EnableOutgoingWebhooks).checked; + config.ServiceSettings.EnableOutgoingWebhooks = ReactDOM.findDOMNode(this.refs.EnableOutgoingWebhooks).checked; config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked; config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked; config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; diff --git a/web/react/components/edit_channel_header_modal.jsx b/web/react/components/edit_channel_header_modal.jsx index 209e30fcc..e4817f6e4 100644 --- a/web/react/components/edit_channel_header_modal.jsx +++ b/web/react/components/edit_channel_header_modal.jsx @@ -1,8 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import * as Client from '../utils/client.jsx'; -import * as AsyncClient from '../utils/async_client.jsx'; +import Constants from '../utils/constants.jsx'; import * as Utils from '../utils/utils.jsx'; const Modal = ReactBootstrap.Modal; @@ -11,12 +12,14 @@ export default class EditChannelHeaderModal extends React.Component { constructor(props) { super(props); - this.handleEdit = this.handleEdit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); this.onShow = this.onShow.bind(this); this.onHide = this.onHide.bind(this); this.state = { + header: props.channel.header, serverError: '' }; } @@ -27,27 +30,38 @@ export default class EditChannelHeaderModal extends React.Component { } } + componentWillReceiveProps(nextProps) { + if (this.props.channel.header !== nextProps.channel.header) { + this.setState({ + header: nextProps.channel.header + }); + } + } + 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; + handleChange(e) { + this.setState({ + header: e.target.value + }); + } - Client.updateChannelHeader(data, - () => { + handleSubmit() { + Client.updateChannelHeader( + this.props.channel.id, + this.state.header, + (channel) => { this.setState({serverError: ''}); - AsyncClient.getChannel(this.props.channel.id); this.onHide(); + + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECIEVED_CHANNEL, + channel + }); }, (err) => { if (err.message === 'Invalid channel_header parameter') { @@ -66,7 +80,8 @@ export default class EditChannelHeaderModal extends React.Component { onHide() { this.setState({ - serverError: '' + serverError: '', + header: this.props.channel.header }); this.props.onHide(); @@ -94,7 +109,8 @@ export default class EditChannelHeaderModal extends React.Component { rows='6' id='edit_header' maxLength='1024' - defaultValue={this.props.channel.header} + value={this.state.header} + onChange={this.handleChange} /> {serverError} </Modal.Body> @@ -102,14 +118,14 @@ export default class EditChannelHeaderModal extends React.Component { <button type='button' className='btn btn-default' - onClick={this.props.onHide} + onClick={this.onHide} > {'Cancel'} </button> <button type='button' className='btn btn-primary' - onClick={this.handleEdit} + onClick={this.handleSubmit} > {'Save'} </button> diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 649ec7321..56bc00a7e 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -33,6 +33,7 @@ export default class InviteMemberModal extends React.Component { firstNameErrors: {}, lastNameErrors: {}, emailEnabled: global.window.mm_config.SendEmailNotifications === 'true', + userCreationEnabled: global.window.mm_config.EnableUserCreation === 'true', showConfirmModal: false, isSendingEmails: false }; @@ -252,7 +253,7 @@ export default class InviteMemberModal extends React.Component { ref={'first_name' + index} placeholder='First name' maxLength='64' - disabled={!this.state.emailEnabled} + disabled={!this.state.emailEnabled || !this.state.userCreationEnabled} spellCheck='false' /> {firstNameError} @@ -266,7 +267,7 @@ export default class InviteMemberModal extends React.Component { ref={'last_name' + index} placeholder='Last name' maxLength='64' - disabled={!this.state.emailEnabled} + disabled={!this.state.emailEnabled || !this.state.userCreationEnabled} spellCheck='false' /> {lastNameError} @@ -285,7 +286,7 @@ export default class InviteMemberModal extends React.Component { className='form-control' placeholder='email@domain.com' maxLength='64' - disabled={!this.state.emailEnabled} + disabled={!this.state.emailEnabled || !this.state.userCreationEnabled} spellCheck='false' /> {emailError} @@ -303,7 +304,7 @@ export default class InviteMemberModal extends React.Component { var content = null; var sendButton = null; - if (this.state.emailEnabled) { + if (this.state.emailEnabled && this.state.userCreationEnabled) { content = ( <div> {serverError} @@ -337,7 +338,7 @@ export default class InviteMemberModal extends React.Component { {sendButtonLabel} </button> ); - } else { + } else if (this.state.userCreationEnabled) { var teamInviteLink = null; if (currentUser && TeamStore.getCurrent().type === 'O') { var link = ( @@ -358,10 +359,16 @@ export default class InviteMemberModal extends React.Component { content = ( <div> - <p>Email is currently disabled for your team, and email invitations cannot be sent. Contact your system administrator to enable email and email invitations.</p> + <p>{'Email is currently disabled for your team, and email invitations cannot be sent. Contact your system administrator to enable email and email invitations.'}</p> {teamInviteLink} </div> ); + } else { + content = ( + <div> + <p>{'User creation has been disabled for your team. Please ask your team administrator for details.'}</p> + </div> + ); } return ( diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index cf40af6ae..3661b19e6 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -47,6 +47,21 @@ export default class MoreDirectChannels extends React.Component { UserStore.addChangeListener(this.handleUserChange); } + componentDidUpdate(prevProps) { + if (!prevProps.show && this.props.show) { + this.onShow(); + } + } + + onShow() { + if (Utils.isMobile()) { + $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 250); + } else { + $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar(); + $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 300); + } + } + handleFilterChange() { const filter = ReactDOM.findDOMNode(this.refs.filter).value; @@ -164,15 +179,6 @@ export default class MoreDirectChannels extends React.Component { ); } - componentDidUpdate(prevProps) { - if (!prevProps.show && this.props.show) { - $(ReactDOM.findDOMNode(this.refs.userList)).css('max-height', $(window).height() - 50); - if ($(window).width() > 768) { - $(ReactDOM.findDOMNode(this.refs.userList)).perfectScrollbar(); - } - } - } - render() { if (!this.props.show) { return null; @@ -217,8 +223,8 @@ export default class MoreDirectChannels extends React.Component { <Modal.Header closeButton={true}> <Modal.Title>{'Direct Messages'}</Modal.Title> </Modal.Header> - <Modal.Body> - <div className='row filter-row'> + <Modal.Body ref='modalBody'> + <div className='filter-row'> <div className='col-sm-6'> <input ref='filter' diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index c286ee6f9..d4ec5a5f5 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -184,6 +184,34 @@ export default class NavbarDropdown extends React.Component { ); } + let helpLink = null; + if (global.window.mm_config.HelpLink) { + helpLink = ( + <li> + <a + target='_blank' + href={global.window.mm_config.HelpLink} + > + {'Help'} + </a> + </li> + ); + } + + let reportLink = null; + if (global.window.mm_config.ReportAProblemLink) { + reportLink = ( + <li> + <a + target='_blank' + href={global.window.mm_config.ReportAProblemLink} + > + {'Report a Problem'} + </a> + </li> + ); + } + return ( <ul className='nav navbar-nav navbar-right'> <li @@ -230,22 +258,8 @@ export default class NavbarDropdown extends React.Component { {sysAdminLink} {teams} <li className='divider'></li> - <li> - <a - target='_blank' - href='/static/help/help.html' - > - {'Help'} - </a> - </li> - <li> - <a - target='_blank' - href='/static/help/report_problem.html' - > - {'Report a Problem'} - </a> - </li> + {helpLink} + {reportLink} <li> <a href='#' diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 3d7f449d1..8393440cb 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -71,49 +71,47 @@ export default class Sidebar extends React.Component { getStateFromStores() { const members = ChannelStore.getAllMembers(); const currentChannelId = ChannelStore.getCurrentId(); + const currentUserId = UserStore.getCurrentId(); const channels = Object.assign([], ChannelStore.getAll()); channels.sort((a, b) => a.display_name.localeCompare(b.display_name)); const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL); const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); - const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL); const preferences = PreferenceStore.getPreferences(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW); - var visibleDirectChannels = []; - for (var i = 0; i < directChannels.length; i++) { - const dm = directChannels[i]; - const teammate = Utils.getDirectTeammate(dm.id); - if (!teammate) { + const directChannels = []; + for (const preference of preferences) { + if (preference.value !== 'true') { continue; } - const member = members[dm.id]; - const msgCount = dm.total_msg_count - member.msg_count; + const teammateId = preference.name; - // always show a channel if either it is the current one or if it is unread, but it is not currently being left - const forceShow = (currentChannelId === dm.id || msgCount > 0) && !this.isLeaving.get(dm.id); - const preferenceShow = preferences.some((preference) => (preference.name === teammate.id && preference.value !== 'false')); + let directChannel = channels.find(Utils.isDirectChannelForUser.bind(null, teammateId)); - if (preferenceShow || forceShow) { - dm.display_name = Utils.displayUsername(teammate.id); - dm.teammate_id = teammate.id; - dm.status = UserStore.getStatus(teammate.id); + // a direct channel doesn't exist yet so create a fake one + if (!directChannel) { + directChannel = { + name: Utils.getDirectChannelName(currentUserId, teammateId), + last_post_at: 0, + total_msg_count: 0, + type: Constants.DM_CHANNEL, + fake: true + }; + } - visibleDirectChannels.push(dm); + directChannel.display_name = Utils.displayUsername(teammateId); + directChannel.teammate_id = teammateId; + directChannel.status = UserStore.getStatus(teammateId); - if (forceShow && !preferenceShow) { - // make sure that unread direct channels are visible - const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true'); - AsyncClient.savePreferences([preference]); - } - } + directChannels.push(directChannel); } - const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - visibleDirectChannels.length; + directChannels.sort(this.sortChannelsByDisplayName); - visibleDirectChannels.sort(this.sortChannelsByDisplayName); + const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - directChannels.length; const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '999'}); @@ -122,7 +120,7 @@ export default class Sidebar extends React.Component { members, publicChannels, privateChannels, - visibleDirectChannels, + directChannels, hiddenDirectChannelCount, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER @@ -484,7 +482,7 @@ export default class Sidebar extends React.Component { const privateChannelItems = this.state.privateChannels.map(this.createChannelElement); - const directMessageItems = this.state.visibleDirectChannels.map((channel, index, arr) => { + const directMessageItems = this.state.directChannels.map((channel, index, arr) => { return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel); }); diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index d93d146d8..20c2bf696 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -49,7 +49,7 @@ export default class SidebarRightMenu extends React.Component { href='#' onClick={EventHelpers.showInviteMemberModal} > - <i className='fa fa-user'></i>Invite New Member + <i className='fa fa-user'></i>{'Invite New Member'} </a> </li> ); @@ -75,7 +75,7 @@ export default class SidebarRightMenu extends React.Component { href='#' data-toggle='modal' data-target='#team_settings' - ><i className='fa fa-globe'></i>Team Settings</a> + ><i className='fa fa-globe'></i>{'Team Settings'}</a> </li> ); manageLink = ( @@ -93,7 +93,7 @@ export default class SidebarRightMenu extends React.Component { <a href={'/admin_console?' + utils.getSessionIndex()} > - <i className='fa fa-wrench'></i>System Console</a> + <i className='fa fa-wrench'></i>{'System Console'}</a> </li> ); } @@ -107,6 +107,27 @@ export default class SidebarRightMenu extends React.Component { teamDisplayName = this.props.teamDisplayName; } + let helpLink = null; + if (global.window.mm_config.HelpLink) { + helpLink = ( + <li> + <a + target='_blank' + href={global.window.mm_config.HelpLink} + ><i className='fa fa-question'></i>{'Help'}</a></li> + ); + } + + let reportLink = null; + if (global.window.mm_config.ReportAProblemLink) { + reportLink = ( + <li> + <a + target='_blank' + href={global.window.mm_config.ReportAProblemLink} + ><i className='fa fa-phone'></i>{'Report a Problem'}</a></li> + ); + } return ( <div> <div className='team__header theme'> @@ -123,7 +144,7 @@ export default class SidebarRightMenu extends React.Component { href='#' onClick={() => this.setState({showUserSettingsModal: true})} > - <i className='fa fa-cog'></i>Account Settings + <i className='fa fa-cog'></i>{'Account Settings'} </a> </li> {teamSettingsLink} @@ -135,18 +156,10 @@ export default class SidebarRightMenu extends React.Component { <a href='#' onClick={this.handleLogoutClick} - ><i className='fa fa-sign-out'></i>Logout</a></li> + ><i className='fa fa-sign-out'></i>{'Logout'}</a></li> <li className='divider'></li> - <li> - <a - target='_blank' - href='/static/help/help.html' - ><i className='fa fa-question'></i>Help</a></li> - <li> - <a - target='_blank' - href='/static/help/report_problem.html' - ><i className='fa fa-phone'></i>Report a Problem</a></li> + {helpLink} + {reportLink} </ul> </div> <UserSettingsModal diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 03715d585..dc615f2e8 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -12,6 +12,7 @@ export default class GeneralTab extends React.Component { constructor(props) { super(props); + this.updateSection = this.updateSection.bind(this); this.handleNameSubmit = this.handleNameSubmit.bind(this); this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this); this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this); @@ -27,11 +28,22 @@ export default class GeneralTab extends React.Component { this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this); this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this); - 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, + this.state = this.setupInitialState(props); + } + + updateSection(section) { + this.setState(this.setupInitialState(this.props)); + this.props.updateSection(section); + } + + setupInitialState(props) { + const team = props.team; + + return { + name: team.display_name, + invite_id: team.invite_id, + allow_open_invite: team.allow_open_invite, + allow_team_listing: team.allow_team_listing, serverError: '', clientError: '' }; @@ -71,7 +83,7 @@ export default class GeneralTab extends React.Component { (team) => { TeamStore.saveTeam(team); TeamStore.emitChange(); - this.props.updateSection(''); + this.updateSection(''); }, (err) => { state.serverError = err.message; @@ -91,7 +103,7 @@ export default class GeneralTab extends React.Component { (team) => { TeamStore.saveTeam(team); TeamStore.emitChange(); - this.props.updateSection(''); + this.updateSection(''); }, (err) => { state.serverError = err.message; @@ -129,7 +141,7 @@ export default class GeneralTab extends React.Component { (team) => { TeamStore.saveTeam(team); TeamStore.emitChange(); - this.props.updateSection(''); + this.updateSection(''); }, (err) => { state.serverError = err.message; @@ -164,7 +176,7 @@ export default class GeneralTab extends React.Component { (team) => { TeamStore.saveTeam(team); TeamStore.emitChange(); - this.props.updateSection(''); + this.updateSection(''); }, (err) => { state.serverError = err.message; @@ -180,8 +192,7 @@ export default class GeneralTab extends React.Component { } handleClose() { - this.setState({clientError: '', serverError: ''}); - this.props.updateSection(''); + this.updateSection(''); } componentDidMount() { @@ -195,36 +206,36 @@ export default class GeneralTab extends React.Component { onUpdateNameSection(e) { e.preventDefault(); if (this.props.activeSection === 'name') { - this.props.updateSection(''); + this.updateSection(''); } else { - this.props.updateSection('name'); + this.updateSection('name'); } } onUpdateInviteIdSection(e) { e.preventDefault(); if (this.props.activeSection === 'invite_id') { - this.props.updateSection(''); + this.updateSection(''); } else { - this.props.updateSection('invite_id'); + this.updateSection('invite_id'); } } onUpdateOpenInviteSection(e) { e.preventDefault(); if (this.props.activeSection === 'open_invite') { - this.props.updateSection(''); + this.updateSection(''); } else { - this.props.updateSection('open_invite'); + this.updateSection('open_invite'); } } onUpdateTeamListingSection(e) { e.preventDefault(); if (this.props.activeSection === 'team_listing') { - this.props.updateSection(''); + this.updateSection(''); } else { - this.props.updateSection('team_listing'); + this.updateSection('team_listing'); } } @@ -248,44 +259,59 @@ export default class GeneralTab extends React.Component { serverError = this.state.serverError; } + const enableTeamListing = global.window.mm_config.EnableTeamListing === 'true'; + 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/> + const inputs = []; + let submitHandle = null; + + if (enableTeamListing) { + submitHandle = this.handleTeamListingSubmit; + + inputs.push( + <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/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div> </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/> + ); + } else { + inputs.push( + <div key='userTeamListingOptions'> + <div><br/>{'Contact your system administrator to turn on the team directory on the system home page.'}</div> </div> - <div><br/>{'Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.'}</div> - </div> - ]; + ); + } teamListingSection = ( <SettingItemMax title='Include this team in the Team Directory' inputs={inputs} - submit={this.handleTeamListingSubmit} + submit={submitHandle} server_error={serverError} client_error={clientError} updateSection={this.onUpdateTeamListingSection} @@ -293,10 +319,15 @@ export default class GeneralTab extends React.Component { ); } else { let describe = ''; - if (this.state.allow_team_listing === true) { - describe = 'Yes'; + + if (enableTeamListing) { + if (this.state.allow_team_listing === true) { + describe = 'Yes'; + } else { + describe = 'No'; + } } else { - describe = 'No'; + describe = 'Team directory is turned off for this system.'; } teamListingSection = ( diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx index 9360d31f8..7ab1e5512 100644 --- a/web/react/components/tutorial/tutorial_intro_screens.jsx +++ b/web/react/components/tutorial/tutorial_intro_screens.jsx @@ -112,23 +112,30 @@ export default class TutorialIntroScreens extends React.Component { const circles = this.createCircles(); - return ( - <div> - <h3>{'You’re all set'}</h3> - <p> - {inviteModalLink} - {' when you’re ready.'} - </p> + let supportInfo = null; + if (global.window.mm_config.SupportEmail) { + supportInfo = ( <p> {'Need anything, just email us at '} <a - href='mailto:feedback@mattermost.com' + href={'mailto:' + global.window.mm_config.SupportEmail} target='_blank' > - {'feedback@mattermost.com'} + {global.window.mm_config.SupportEmail} </a> {'.'} </p> + ); + } + + return ( + <div> + <h3>{'You’re all set'}</h3> + <p> + {inviteModalLink} + {' when you’re ready.'} + </p> + {supportInfo} {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.'} {circles} </div> diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 35f836adb..778c74c23 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -3,6 +3,9 @@ import Constants from '../../utils/constants.jsx'; +const OverlayTrigger = ReactBootstrap.OverlayTrigger; +const Popover = ReactBootstrap.Popover; + export default class CustomThemeChooser extends React.Component { constructor(props) { super(props); @@ -72,6 +75,19 @@ export default class CustomThemeChooser extends React.Component { ); }); + var popoverContent = ( + <Popover + bsStyle='info' + id='code-popover' + className='code-popover' + > + <img + width='200' + src={'/static/images/themes/code_themes/' + theme[element.id] + 'Large.png'} + /> + </Popover> + ); + elements.push( <div className='col-sm-4 form-group' @@ -90,11 +106,17 @@ export default class CustomThemeChooser extends React.Component { > {codeThemeOptions} </select> + <OverlayTrigger + placement='top' + overlay={popoverContent} + ref='headerOverlay' + > <span className='input-group-addon'> <img src={'/static/images/themes/code_themes/' + theme[element.id] + '.png'} /> </span> + </OverlayTrigger> </div> </div> ); diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index 9c0fe3709..ede639691 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -1,4 +1,4 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import LoadingScreen from '../loading_screen.jsx'; @@ -36,7 +36,7 @@ export default class ManageOutgoingHooks extends React.Component { if (this.state.triggerWords.length !== 0) { hook.trigger_words = this.state.triggerWords.trim().split(','); } - hook.callback_urls = this.state.callbackURLs.split('\n'); + hook.callback_urls = this.state.callbackURLs.split('\n').map((url) => url.trim()); Client.addOutgoingHook( hook, diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 962efd7a2..7c1a1297f 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -51,7 +51,7 @@ export default class UserSettingsGeneralTab extends React.Component { } if (user.username === username) { - this.setState({clientError: 'You must submit a new username.', emailError: '', serverError: ''}); + this.updateSection(''); return; } @@ -66,7 +66,7 @@ export default class UserSettingsGeneralTab extends React.Component { const nickname = this.state.nickname.trim(); if (user.nickname === nickname) { - this.setState({clientError: 'You must submit a new nickname.', emailError: '', serverError: ''}); + this.updateSection(''); return; } @@ -82,7 +82,7 @@ export default class UserSettingsGeneralTab extends React.Component { const lastName = this.state.lastName.trim(); if (user.first_name === firstName && user.last_name === lastName) { - this.setState({clientError: 'You must submit a new first or last name.', emailError: '', serverError: ''}); + this.updateSection(''); return; } @@ -98,10 +98,6 @@ export default class UserSettingsGeneralTab extends React.Component { const email = this.state.email.trim().toLowerCase(); const confirmEmail = this.state.confirmEmail.trim().toLowerCase(); - if (user.email === email) { - return; - } - if (email === '' || !Utils.isEmail(email)) { this.setState({emailError: 'Please enter a valid email address.', clientError: '', serverError: ''}); return; @@ -112,6 +108,11 @@ export default class UserSettingsGeneralTab extends React.Component { return; } + if (user.email === email) { + this.updateSection(''); + return; + } + user.email = email; this.submitUser(user, true); } diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/dispatcher/event_helpers.jsx index fa4d1d64f..a03923c1a 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/dispatcher/event_helpers.jsx @@ -172,3 +172,10 @@ export function emitClearSuggestions(suggestionId) { id: suggestionId }); } + +export function emitPreferenceChangedEvent(preference) { + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECIEVED_PREFERENCE, + preference + }); +} diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index 0bfde77b4..afc960fcf 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -317,7 +317,9 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { case ActionTypes.RECIEVED_CHANNEL: ChannelStore.pStoreChannel(action.channel); - ChannelStore.pStoreChannelMember(action.member); + if (action.member) { + ChannelStore.pStoreChannelMember(action.member); + } currentId = ChannelStore.getCurrentId(); if (currentId) { ChannelStore.resetCounts(currentId); diff --git a/web/react/stores/preference_store.jsx b/web/react/stores/preference_store.jsx index 068bc29c2..543129aca 100644 --- a/web/react/stores/preference_store.jsx +++ b/web/react/stores/preference_store.jsx @@ -1,4 +1,4 @@ -// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import Constants from '../utils/constants.jsx'; @@ -90,8 +90,8 @@ class PreferenceStoreClass extends EventEmitter { return preference; } - emitChange(preferences) { - this.emit(CHANGE_EVENT, preferences); + emitChange() { + this.emit(CHANGE_EVENT); } addChangeListener(callback) { @@ -106,6 +106,12 @@ class PreferenceStoreClass extends EventEmitter { const action = payload.action; switch (action.type) { + case ActionTypes.RECIEVED_PREFERENCE: { + const preference = action.preference; + this.setPreference(preference.category, preference.name, preference.value); + this.emitChange(); + break; + } case ActionTypes.RECIEVED_PREFERENCES: { const preferences = this.getAllPreferences(); @@ -114,7 +120,7 @@ class PreferenceStoreClass extends EventEmitter { } this.setAllPreferences(preferences); - this.emitChange(preferences); + this.emitChange(); break; } } diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index d5aed40cf..24fa79ca6 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -136,6 +136,10 @@ class SocketStoreClass extends EventEmitter { handleChannelViewedEvent(msg); break; + case SocketEvents.PREFERENCE_CHANGED: + handlePreferenceChangedEvent(msg); + break; + default: } } @@ -281,6 +285,11 @@ function handleChannelViewedEvent(msg) { } } +function handlePreferenceChangedEvent(msg) { + const preference = JSON.parse(msg.props.preference); + EventHelpers.emitPreferenceChangedEvent(preference); +} + var SocketStore = new SocketStoreClass(); /*SocketStore.dispatchToken = AppDispatcher.register((payload) => { diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 09e962161..5d02a8c88 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -590,7 +590,12 @@ export function updateChannel(channel, success, error) { track('api', 'api_channels_update'); } -export function updateChannelHeader(data, success, error) { +export function updateChannelHeader(channelId, header, success, error) { + const data = { + channel_id: channelId, + channel_header: header + }; + $.ajax({ url: '/api/v1/channels/update_header', dataType: 'json', diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 891bc4624..5f027a409 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -35,6 +35,7 @@ export default { RECIEVED_AUDITS: null, RECIEVED_TEAMS: null, RECIEVED_STATUSES: null, + RECIEVED_PREFERENCE: null, RECIEVED_PREFERENCES: null, RECIEVED_MSG: null, @@ -74,7 +75,8 @@ export default { NEW_USER: 'new_user', USER_ADDED: 'user_added', USER_REMOVED: 'user_removed', - TYPING: 'typing' + TYPING: 'typing', + PREFERENCE_CHANGED: 'preference_changed' }, //SPECIAL_MENTIONS: ['all', 'channel'], @@ -164,29 +166,6 @@ export default { UPDATE_TYPING_MS: 5000, THEMES: { default: { - type: 'Mattermost', - sidebarBg: '#fafafa', - sidebarText: '#333333', - sidebarUnreadText: '#333333', - sidebarTextHoverBg: '#e6f2fa', - sidebarTextActiveBorder: '#378FD2', - sidebarTextActiveColor: '#111111', - sidebarHeaderBg: '#2389d7', - sidebarHeaderTextColor: '#ffffff', - onlineIndicator: '#7DBE00', - mentionBj: '#2389d7', - mentionColor: '#ffffff', - centerChannelBg: '#ffffff', - centerChannelColor: '#333333', - newMessageSeparator: '#FF8800', - linkColor: '#2389d7', - buttonBg: '#2389d7', - buttonColor: '#FFFFFF', - mentionHighlightBg: '#fff2bb', - mentionHighlightLink: '#2f81b7', - codeTheme: 'github' - }, - organization: { type: 'Organization', sidebarBg: '#2071a7', sidebarText: '#fff', @@ -209,6 +188,29 @@ export default { mentionHighlightLink: '#2f81b7', codeTheme: 'github' }, + mattermost: { + type: 'Mattermost', + sidebarBg: '#fafafa', + sidebarText: '#333333', + sidebarUnreadText: '#333333', + sidebarTextHoverBg: '#e6f2fa', + sidebarTextActiveBorder: '#378FD2', + sidebarTextActiveColor: '#111111', + sidebarHeaderBg: '#2389d7', + sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#7DBE00', + mentionBj: '#2389d7', + mentionColor: '#ffffff', + centerChannelBg: '#ffffff', + centerChannelColor: '#333333', + newMessageSeparator: '#FF8800', + linkColor: '#2389d7', + buttonBg: '#2389d7', + buttonColor: '#FFFFFF', + mentionHighlightBg: '#fff2bb', + mentionHighlightLink: '#2f81b7', + codeTheme: 'github' + }, mattermostDark: { type: 'Mattermost Dark', sidebarBg: '#1B2C3E', diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index c2e4276b0..fb8b89252 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -1138,6 +1138,11 @@ export function getUserIdFromChannelName(channel) { return otherUserId; } +// Returns true if the given channel is a direct channel between the current user and the given one +export function isDirectChannelForUser(otherUserId, channel) { + return channel.type === Constants.DM_CHANNEL && getUserIdFromChannelName(channel) === otherUserId; +} + export function importSlack(file, success, error) { var formData = new FormData(); formData.append('file', file, file.name); diff --git a/web/sass-files/sass/partials/_docs.scss b/web/sass-files/sass/partials/_docs.scss new file mode 100644 index 000000000..f4e7cc314 --- /dev/null +++ b/web/sass-files/sass/partials/_docs.scss @@ -0,0 +1,19 @@ +@charset "UTF-8"; + +.docs__page { + line-height: 1.7; + padding-bottom: 20px; + + > div { + width: 1170px; + margin: 0 auto; + padding: 0 15px; + max-width: 100%; + } + + h1.markdown__heading { + border-bottom: 1px solid #ddd; + padding-bottom: 1rem; + margin: 1em 0 1em; + } +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index a082ffb22..6f70e502f 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -381,9 +381,9 @@ .more-modal { .user-list { + overflow-y: auto; + overflow-x: hidden; margin-top: 10px; - overflow: auto; - -webkit-overflow-scrolling: touch; max-height: 500px; position: relative; } @@ -394,11 +394,12 @@ .modal-body { padding: 10px 0 20px; + overflow-x: hidden; } .filter-row { margin-top: 10px; - padding: 0 15px; + @include clearfix; } .member-count { diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss index bc55b7ff7..1ae07fe5b 100644 --- a/web/sass-files/sass/partials/_popover.scss +++ b/web/sass-files/sass/partials/_popover.scss @@ -10,6 +10,10 @@ display: inline-block; } +.code-popover .popover-content { + padding: 5px; +} + .user-popover__image { margin: 0 0 10px; @include border-radius(128px); diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index c93372175..7bf3574d2 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -49,6 +49,9 @@ // Responsive Css @import "partials/responsive"; +// Docs Css +@import "partials/docs"; + // Standalone Css @import "partials/oauth"; diff --git a/web/static/images/themes/code_themes/githubLarge.png b/web/static/images/themes/code_themes/githubLarge.png Binary files differnew file mode 100644 index 000000000..cffc6e012 --- /dev/null +++ b/web/static/images/themes/code_themes/githubLarge.png diff --git a/web/static/images/themes/code_themes/monokaiLarge.png b/web/static/images/themes/code_themes/monokaiLarge.png Binary files differnew file mode 100644 index 000000000..7224950af --- /dev/null +++ b/web/static/images/themes/code_themes/monokaiLarge.png diff --git a/web/static/images/themes/code_themes/solarized_darkLarge.png b/web/static/images/themes/code_themes/solarized_darkLarge.png Binary files differnew file mode 100644 index 000000000..582df48f9 --- /dev/null +++ b/web/static/images/themes/code_themes/solarized_darkLarge.png diff --git a/web/static/images/themes/code_themes/solarized_lightLarge.png b/web/static/images/themes/code_themes/solarized_lightLarge.png Binary files differnew file mode 100644 index 000000000..d2c2702fb --- /dev/null +++ b/web/static/images/themes/code_themes/solarized_lightLarge.png diff --git a/web/templates/docs.html b/web/templates/docs.html index 21659e810..0e0f51648 100644 --- a/web/templates/docs.html +++ b/web/templates/docs.html @@ -7,7 +7,7 @@ <div class="inner__wrap"> <div class="row content"> <div class="col-sm-12"> - <div id="docs"></div> + <div class="docs__page" id="docs"></div> </div> <div class="footer-push"></div> </div> diff --git a/web/templates/footer.html b/web/templates/footer.html index dc1a7c9d0..60dd5a40e 100644 --- a/web/templates/footer.html +++ b/web/templates/footer.html @@ -12,9 +12,28 @@ </div> </div> <script> - document.getElementById("help_link").setAttribute("href", '/static/help/help.html'); - document.getElementById("terms_link").setAttribute("href", '/static/help/terms.html'); - document.getElementById("privacy_link").setAttribute("href", '/static/help/privacy.html'); - document.getElementById("about_link").setAttribute("href", '/static/help/about.html'); + if (window.mm_config.HelpLink) { + document.getElementById("help_link").setAttribute("href", window.mm_config.HelpLink); + } else { + $("#help_link").remove(); + } + + if (window.mm_config.TermsOfServiceLink) { + document.getElementById("terms_link").setAttribute("href", window.mm_config.TermsOfServiceLink); + } else { + $("#terms_link").remove(); + } + + if (window.mm_config.PrivacyPolicyLink) { + document.getElementById("privacy_link").setAttribute("href", window.mm_config.PrivacyPolicyLink); + } else { + $("#privacy_link").remove(); + } + + if (window.mm_config.AboutLink) { + document.getElementById("about_link").setAttribute("href", window.mm_config.AboutLink); + } else { + $("#about_link").remove(); + } </script> {{end}} diff --git a/web/web.go b/web/web.go index a72bff2bf..63544229b 100644 --- a/web/web.go +++ b/web/web.go @@ -564,7 +564,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { return } else { c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?verified=true&email="+email, http.StatusTemporaryRedirect) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?verified=true&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) return } } diff --git a/web/web_test.go b/web/web_test.go index 4e15037ad..8d40810b5 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -60,7 +60,7 @@ func TestGetAccessToken(t *testing.T) { team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, _ := ApiClient.CreateTeam(&team) - user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"} + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey+test@test.com", Password: "pwd"} ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User) store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id)) @@ -189,7 +189,7 @@ func TestIncomingWebhook(t *testing.T) { team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team = ApiClient.Must(ApiClient.CreateTeam(team)).Data.(*model.Team) - user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey+test@test.com", Nickname: "Corey Hulen", Password: "pwd"} user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User) store.Must(api.Srv.Store.User().VerifyEmail(user.Id)) |