summaryrefslogtreecommitdiffstats
path: root/web/react/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/react/components')
-rw-r--r--web/react/components/activity_log_modal.jsx7
-rw-r--r--web/react/components/admin_console/admin_controller.jsx105
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx222
-rw-r--r--web/react/components/admin_console/email_settings.jsx617
-rw-r--r--web/react/components/admin_console/gitlab_settings.jsx277
-rw-r--r--web/react/components/admin_console/image_settings.jsx496
-rw-r--r--web/react/components/admin_console/log_settings.jsx74
-rw-r--r--web/react/components/admin_console/privacy_settings.jsx163
-rw-r--r--web/react/components/admin_console/rate_settings.jsx272
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx132
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx193
-rw-r--r--web/react/components/admin_console/service_settings.jsx296
-rw-r--r--web/react/components/admin_console/sql_settings.jsx283
-rw-r--r--web/react/components/admin_console/team_settings.jsx235
-rw-r--r--web/react/components/admin_console/team_users.jsx178
-rw-r--r--web/react/components/admin_console/user_item.jsx266
-rw-r--r--web/react/components/channel_header.jsx2
-rw-r--r--web/react/components/channel_loader.jsx44
-rw-r--r--web/react/components/channel_notifications.jsx16
-rw-r--r--web/react/components/create_comment.jsx13
-rw-r--r--web/react/components/create_post.jsx103
-rw-r--r--web/react/components/delete_channel_modal.jsx3
-rw-r--r--web/react/components/email_verify.jsx14
-rw-r--r--web/react/components/error_bar.jsx85
-rw-r--r--web/react/components/file_attachment.jsx10
-rw-r--r--web/react/components/file_attachment_list.jsx16
-rw-r--r--web/react/components/file_upload.jsx3
-rw-r--r--web/react/components/invite_member_modal.jsx2
-rw-r--r--web/react/components/login.jsx13
-rw-r--r--web/react/components/more_direct_channels.jsx2
-rw-r--r--web/react/components/navbar.jsx2
-rw-r--r--web/react/components/new_channel_modal.jsx3
-rw-r--r--web/react/components/password_reset_send_link.jsx7
-rw-r--r--web/react/components/popover_list_members.jsx2
-rw-r--r--web/react/components/post.jsx51
-rw-r--r--web/react/components/post_body.jsx2
-rw-r--r--web/react/components/post_info.jsx16
-rw-r--r--web/react/components/post_list.jsx22
-rw-r--r--web/react/components/post_list_container.jsx1
-rw-r--r--web/react/components/register_app_modal.jsx2
-rw-r--r--web/react/components/rhs_comment.jsx16
-rw-r--r--web/react/components/rhs_header_post.jsx1
-rw-r--r--web/react/components/rhs_root_post.jsx9
-rw-r--r--web/react/components/search_results_header.jsx1
-rw-r--r--web/react/components/search_results_item.jsx2
-rw-r--r--web/react/components/settings_sidebar.jsx3
-rw-r--r--web/react/components/sidebar.jsx2
-rw-r--r--web/react/components/sidebar_right_menu.jsx4
-rw-r--r--web/react/components/signup_team.jsx56
-rw-r--r--web/react/components/signup_user_complete.jsx35
-rw-r--r--web/react/components/team_feature_tab.jsx190
-rw-r--r--web/react/components/team_import_tab.jsx2
-rw-r--r--web/react/components/team_settings.jsx14
-rw-r--r--web/react/components/team_settings_modal.jsx13
-rw-r--r--web/react/components/team_signup_choose_auth.jsx20
-rw-r--r--web/react/components/team_signup_password_page.jsx2
-rw-r--r--web/react/components/team_signup_send_invites_page.jsx2
-rw-r--r--web/react/components/team_signup_with_sso.jsx2
-rw-r--r--web/react/components/textbox.jsx53
-rw-r--r--web/react/components/user_profile.jsx2
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx111
-rw-r--r--web/react/components/user_settings/import_theme_modal.jsx179
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx174
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx58
-rw-r--r--web/react/components/user_settings/user_settings.jsx (renamed from web/react/components/user_settings.jsx)16
-rw-r--r--web/react/components/user_settings/user_settings_appearance.jsx236
-rw-r--r--web/react/components/user_settings/user_settings_developer.jsx (renamed from web/react/components/user_settings_developer.jsx)6
-rw-r--r--web/react/components/user_settings/user_settings_general.jsx (renamed from web/react/components/user_settings_general.jsx)16
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx95
-rw-r--r--web/react/components/user_settings/user_settings_modal.jsx (renamed from web/react/components/user_settings_modal.jsx)7
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx (renamed from web/react/components/user_settings_notifications.jsx)24
-rw-r--r--web/react/components/user_settings/user_settings_security.jsx (renamed from web/react/components/user_settings_security.jsx)10
-rw-r--r--web/react/components/user_settings_appearance.jsx181
-rw-r--r--web/react/components/view_image.jsx250
-rw-r--r--web/react/components/view_image_popover_bar.jsx66
75 files changed, 4917 insertions, 1191 deletions
diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx
index 7cbd4021e..fe40385a0 100644
--- a/web/react/components/activity_log_modal.jsx
+++ b/web/react/components/activity_log_modal.jsx
@@ -25,7 +25,8 @@ export default class ActivityLogModal extends React.Component {
clientError: null
};
}
- submitRevoke(altId) {
+ submitRevoke(altId, e) {
+ e.preventDefault();
Client.revokeSession(altId,
function handleRevokeSuccess() {
AsyncClient.getSessions();
@@ -86,7 +87,7 @@ export default class ActivityLogModal extends React.Component {
<div>{`First time active: ${firstAccessTime.toDateString()}, ${lastAccessTime.toLocaleTimeString()}`}</div>
<div>{`OS: ${currentSession.props.os}`}</div>
<div>{`Browser: ${currentSession.props.browser}`}</div>
- <div>{`Session ID: ${currentSession.alt_id}`}</div>
+ <div>{`Session ID: ${currentSession.id}`}</div>
</div>
);
} else {
@@ -115,7 +116,7 @@ export default class ActivityLogModal extends React.Component {
</div>
<div className='activity-log__action'>
<button
- onClick={this.submitRevoke.bind(this, currentSession.alt_id)}
+ onClick={this.submitRevoke.bind(this, currentSession.id)}
className='btn btn-primary'
>
Logout
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index e82fe1b76..92f0bbdce 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -3,44 +3,118 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
+var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
var LogsTab = require('./logs.jsx');
+var FileSettingsTab = require('./image_settings.jsx');
+var PrivacySettingsTab = require('./privacy_settings.jsx');
+var RateSettingsTab = require('./rate_settings.jsx');
+var GitLabSettingsTab = require('./gitlab_settings.jsx');
+var SqlSettingsTab = require('./sql_settings.jsx');
+var TeamSettingsTab = require('./team_settings.jsx');
+var ServiceSettingsTab = require('./service_settings.jsx');
+var TeamUsersTab = require('./team_users.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
this.state = {
- config: null,
- selected: 'email_settings'
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: 'service_settings',
+ selectedTeam: null
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
- selected: this.state.selected
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
});
}
- selectTab(tab) {
- this.setState({selected: tab});
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
}
render() {
@@ -53,6 +127,22 @@ export default class AdminController extends React.Component {
tab = <LogSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
+ } else if (this.state.selected === 'image_settings') {
+ tab = <FileSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'privacy_settings') {
+ tab = <PrivacySettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'rate_settings') {
+ tab = <RateSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'gitlab_settings') {
+ tab = <GitLabSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'sql_settings') {
+ tab = <SqlSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_settings') {
+ tab = <TeamSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'service_settings') {
+ tab = <ServiceSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_users') {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
}
@@ -64,7 +154,12 @@ export default class AdminController extends React.Component {
/>
<AdminSidebar
selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
/>
<div className='inner__wrap channel__wrap'>
<div className='row header'>
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index a6e689490..4b9ff3cb8 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
+var SelectTeamModal = require('./select_team_modal.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component {
this.isSelected = this.isSelected.bind(this);
this.handleClick = this.handleClick.bind(this);
+ this.removeTeam = this.removeTeam.bind(this);
+
+ this.showTeamSelect = this.showTeamSelect.bind(this);
+ this.teamSelectedModal = this.teamSelectedModal.bind(this);
+ this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
this.state = {
+ showSelectModal: false
};
}
- handleClick(name, e) {
+ handleClick(name, teamId, e) {
e.preventDefault();
- this.props.selectTab(name);
+ this.props.selectTab(name, teamId);
}
- isSelected(name) {
+ isSelected(name, teamId) {
if (this.props.selected === name) {
- return 'active';
+ if (name === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ return 'active';
+ }
+ } else {
+ return 'active';
+ }
}
return '';
}
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
componentDidMount() {
}
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.props.selectedTeams[teamId] = 'true';
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ teamSelectedModalDismissed() {
+ this.setState({showSelectModal: false});
+ }
+
render() {
+ var count = '*';
+ var teams = 'Loading';
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ if (team != null) {
+ teams.push(
+ <ul
+ key={'team_' + team.id}
+ className='nav nav__sub-menu'
+ >
+ <li>
+ <a
+ href='#'
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
+ >
+ {team.name}
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.removeTeam.bind(this, team.id)}
+ style={{cursor: 'pointer'}}
+ >
+ {'x'}
+ </span>
+ </a>
+ </li>
+ <li>
+ <ul className='nav nav__inner-menu'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_users', team.id)}
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ >
+ {'- Users'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+ }
+ }
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -39,10 +133,45 @@ export default class AdminSidebar extends React.Component {
<li>
<ul className='nav nav__sub-menu'>
<li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'SETTINGS'}</span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('service_settings')}
+ onClick={this.handleClick.bind(this, 'service_settings', null)}
+ >
+ {'Service Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
+ >
+ {'Team Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
+ >
+ {'SQL Settings'}
+ </a>
+ </li>
+ <li>
<a
href='#'
className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
>
{'Email Settings'}
</a>
@@ -50,8 +179,17 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
+ className={this.isSelected('image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
+ >
+ {'File Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
>
{'Log Settings'}
</a>
@@ -59,8 +197,66 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
+ className={this.isSelected('rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
+ >
+ {'Rate Limit Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
+ >
+ {'Privacy Settings'}
+ </a>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
+ >
+ {'GitLab Settings'}
+ </a>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'TEAMS (' + count + ')'}</span>
+ <span className='menu-icon--right'>
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i className='fa fa-plus'></i>
+ </a>
+ </span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ {teams}
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu'>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'OTHER'}</span>
+ </h4>
+ </li>
+ </ul>
+ <ul className='nav nav__sub-menu padded'>
+ <li>
+ <a
+ href='#'
className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs')}
+ onClick={this.handleClick.bind(this, 'logs', null)}
>
{'Logs'}
</a>
@@ -69,12 +265,24 @@ export default class AdminSidebar extends React.Component {
</li>
</ul>
</div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
</div>
);
}
}
AdminSidebar.propTypes = {
+ teams: React.PropTypes.object,
+ selectedTeams: React.PropTypes.object,
+ removeSelectedTeam: React.PropTypes.func,
+ addSelectedTeam: React.PropTypes.func,
selected: React.PropTypes.string,
+ selectedTeam: React.PropTypes.string,
selectTab: React.PropTypes.func
}; \ No newline at end of file
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index e8fb25858..3b5ad2a1a 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -1,15 +1,179 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
export default class EmailSettings extends React.Component {
constructor(props) {
super(props);
+ this.handleChange = this.handleChange.bind(this);
+ this.handleTestConnection = this.handleTestConnection.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.buildConfig = this.buildConfig.bind(this);
+ this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
+ this.handleGenerateReset = this.handleGenerateReset.bind(this);
+
this.state = {
+ sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
+ saveNeeded: false,
+ serverError: null,
+ emailSuccess: null,
+ emailFail: null
};
}
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'sendEmailNotifications_true') {
+ s.sendEmailNotifications = true;
+ }
+
+ if (action === 'sendEmailNotifications_false') {
+ s.sendEmailNotifications = false;
+ }
+
+ this.setState(s);
+ }
+
+ buildConfig() {
+ var config = this.props.config;
+ config.EmailSettings.EnableSignUpWithEmail = React.findDOMNode(this.refs.allowSignUpWithEmail).checked;
+ config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.RequireEmailVerification = React.findDOMNode(this.refs.requireEmailVerification).checked;
+ config.EmailSettings.SendEmailNotifications = React.findDOMNode(this.refs.sendEmailNotifications).checked;
+ config.EmailSettings.FeedbackName = React.findDOMNode(this.refs.feedbackName).value.trim();
+ config.EmailSettings.FeedbackEmail = React.findDOMNode(this.refs.feedbackEmail).value.trim();
+ config.EmailSettings.SMTPServer = React.findDOMNode(this.refs.SMTPServer).value.trim();
+ config.EmailSettings.SMTPPort = React.findDOMNode(this.refs.SMTPPort).value.trim();
+ config.EmailSettings.SMTPUsername = React.findDOMNode(this.refs.SMTPUsername).value.trim();
+ config.EmailSettings.SMTPPassword = React.findDOMNode(this.refs.SMTPPassword).value.trim();
+ config.EmailSettings.ConnectionSecurity = React.findDOMNode(this.refs.ConnectionSecurity).value.trim();
+
+ config.EmailSettings.InviteSalt = React.findDOMNode(this.refs.InviteSalt).value.trim();
+ if (config.EmailSettings.InviteSalt === '') {
+ config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
+ }
+
+ config.EmailSettings.PasswordResetSalt = React.findDOMNode(this.refs.PasswordResetSalt).value.trim();
+ if (config.EmailSettings.PasswordResetSalt === '') {
+ config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
+ }
+
+ return config;
+ }
+
+ handleGenerateInvite(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleGenerateReset(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ handleTestConnection(e) {
+ e.preventDefault();
+ $('#connection-button').button('loading');
+
+ var config = this.buildConfig();
+
+ Client.testEmail(
+ config,
+ () => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: true,
+ emailSuccess: true,
+ emailFail: null
+ });
+ $('#connection-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: true,
+ emailSuccess: null,
+ emailFail: err.message + ' - ' + err.detailed_error
+ });
+ $('#connection-button').button('reset');
+ }
+ );
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.buildConfig();
+
+ Client.saveConfig(
+ config,
+ () => {
+ AsyncClient.getConfig();
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: null,
+ saveNeeded: false,
+ emailSuccess: null,
+ emailFail: null
+ });
+ $('#save-button').button('reset');
+ },
+ (err) => {
+ this.setState({
+ sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
+ serverError: err.message,
+ saveNeeded: true,
+ emailSuccess: null,
+ emailFail: null
+ });
+ $('#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';
+ }
+
+ var emailSuccess = '';
+ if (this.state.emailSuccess) {
+ emailSuccess = (
+ <div className='alert alert-success'>
+ <i className='fa fa-check'></i>{'No errors were reported while sending an email. Please check your inbox to make sure.'}
+ </div>
+ );
+ }
+
+ var emailFail = '';
+ if (this.state.emailFail) {
+ emailSuccess = (
+ <div className='alert alert-warning'>
+ <i className='fa fa-warning'></i>{'Connection unsuccessful: ' + this.state.emailFail}
+ </div>
+ );
+ }
+
return (
<div className='wrapper--fixed'>
<h3>{'Email Settings'}</h3>
@@ -17,295 +181,370 @@ export default class EmailSettings extends React.Component {
className='form-horizontal'
role='form'
>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='email'
+ htmlFor='allowSignUpWithEmail'
>
- {'Bypass Email: '}
- <a
- href='#'
- data-trigger='hover click'
- data-toggle='popover'
- data-position='bottom'
- data-content={'Here\'s some more help text inside a popover for the Bypass Email field just to show how popovers look.'}
- >
- {'(?)'}
- </a>
+ {'Allow Sign Up With Email: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
- name='byPassEmail'
- value='option1'
+ name='allowSignUpWithEmail'
+ value='true'
+ ref='allowSignUpWithEmail'
+ defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
/>
- {'True'}
+ {'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
- name='byPassEmail'
- value='option2'
+ name='allowSignUpWithEmail'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
/>
- {'False'}
+ {'false'}
</label>
- <p className='help-text'>{'This is some sample help text for the Bypass Email field'}</p>
+ <p className='help-text'>{'When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='smtpUsername'
+ htmlFor='sendEmailNotifications'
>
- {'SMTP Username:'}
+ {'Send Email Notifications: '}
</label>
<div className='col-sm-8'>
- <input
- type='email'
- className='form-control'
- id='smtpUsername'
- placeholder='Enter your SMTP username'
- value=''
- />
- <div className='help-text'>
- <div className='alert alert-warning'><i className='fa fa-warning'></i>{' This is some error text for the Bypass Email field'}</div>
- </div>
- <p className='help-text'>{'This is some sample help text for the SMTP username field'}</p>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendEmailNotifications'
+ value='true'
+ ref='sendEmailNotifications'
+ defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
+ onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='sendEmailNotifications'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
+ onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='smtpPassword'
+ htmlFor='requireEmailVerification'
>
- {'SMTP Password:'}
+ {'Require Email Verification: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='requireEmailVerification'
+ value='true'
+ ref='requireEmailVerification'
+ defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
+ onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='requireEmailVerification'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
+ onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='feedbackName'
+ >
+ {'Feedback Name:'}
</label>
<div className='col-sm-8'>
<input
- type='password'
+ type='text'
className='form-control'
- id='smtpPassword'
- placeholder='Enter your SMTP password'
- value=''
+ id='feedbackName'
+ ref='feedbackName'
+ placeholder='Ex: "Mattermost Notification", "System", "No-Reply"'
+ defaultValue={this.props.config.EmailSettings.FeedbackName}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
+ <p className='help-text'>{'Display name on email account used when sending notification emails from Mattermost.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='smtpServer'
+ htmlFor='feedbackEmail'
>
- {'SMTP Server:'}
+ {'Feedback Email:'}
</label>
<div className='col-sm-8'>
<input
- type='text'
+ type='email'
className='form-control'
- id='smtpServer'
- placeholder='Enter your SMTP server'
- value=''
+ id='feedbackEmail'
+ ref='feedbackEmail'
+ placeholder='Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
+ defaultValue={this.props.config.EmailSettings.FeedbackEmail}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
- <div className='help-text'>
- <a
- href='#'
- className='help-link'
- >
- {'Test Connection'}
- </a>
- <div className='alert alert-success'><i className='fa fa-check'></i>{' Connection successful'}</div>
- <div className='alert alert-warning hide'><i className='fa fa-warning'></i>{' Connection unsuccessful'}</div>
- </div>
+ <p className='help-text'>{'Email address displayed on email account used when sending notification emails from Mattermost.'}</p>
</div>
</div>
+
<div className='form-group'>
- <label className='control-label col-sm-4'>{'Use TLS:'}</label>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPUsername'
+ >
+ {'SMTP Username:'}
+ </label>
<div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='tls'
- value='option1'
- />
- {'True'}
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='tls'
- value='option2'
- />
- {'False'}
- </label>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPUsername'
+ ref='SMTPUsername'
+ placeholder='Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
+ defaultValue={this.props.config.EmailSettings.SMTPUsername}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
</div>
</div>
+
<div className='form-group'>
- <label className='control-label col-sm-4'>{'Use Start TLS:'}</label>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SMTPPassword'
+ >
+ {'SMTP Password:'}
+ </label>
<div className='col-sm-8'>
- <label className='radio-inline'>
- <input
- type='radio'
- name='starttls'
- value='option1'
- />
- {'True'}
- </label>
- <label className='radio-inline'>
- <input
- type='radio'
- name='starttls'
- value='option2'
- />
- {'False'}
- </label>
+ <input
+ type='text'
+ className='form-control'
+ id='SMTPPassword'
+ ref='SMTPPassword'
+ placeholder='Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.EmailSettings.SMTPPassword}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{' Obtain this credential from administrator setting up your email server.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='feedbackEmail'
+ htmlFor='SMTPServer'
>
- {'Feedback Email:'}
+ {'SMTP Server:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
- id='feedbackEmail'
- placeholder='Enter your feedback email'
- value=''
+ id='SMTPServer'
+ ref='SMTPServer'
+ placeholder='Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
+ defaultValue={this.props.config.EmailSettings.SMTPServer}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
+ <p className='help-text'>{'Location of SMTP email server.'}</p>
</div>
</div>
+
<div className='form-group'>
<label
className='control-label col-sm-4'
- htmlFor='feedbackUsername'
+ htmlFor='SMTPPort'
>
- {'Feedback Username:'}
+ {'SMTP Port:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
- id='feedbackUsername'
- placeholder='Enter your feedback username'
- value=''
+ id='SMTPPort'
+ ref='SMTPPort'
+ placeholder='Ex: "25", "465"'
+ defaultValue={this.props.config.EmailSettings.SMTPPort}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
/>
+ <p className='help-text'>{'Port of SMTP email server.'}</p>
</div>
</div>
+
<div className='form-group'>
- <div className='col-sm-offset-4 col-sm-8'>
- <div className='checkbox'>
- <label><input type='checkbox' />{'Remember me'}</label>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ConnectionSecurity'
+ >
+ {'Connection Security:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='ConnectionSecurity'
+ ref='ConnectionSecurity'
+ defaultValue={this.props.config.EmailSettings.ConnectionSecurity}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ <option value=''>{'None'}</option>
+ <option value='TLS'>{'TLS (Recommended)'}</option>
+ <option value='STARTTLS'>{'STARTTLS'}</option>
+ </select>
+ <div className='help-text'>
+ <table
+ className='table-bordered'
+ cellPadding='5'
+ >
+ <tr><td className='help-text'>{'None'}</td><td className='help-text'>{'Mattermost will send email over an unsecure connection.'}</td></tr>
+ <tr><td className='help-text'>{'TLS'}</td><td className='help-text'>{'Encrypts the communication between Mattermost and your email server.'}</td></tr>
+ <tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>{'Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'}</td></tr>
+ </table>
+ </div>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleTestConnection}
+ disabled={!this.state.sendEmailNotifications}
+ id='connection-button'
+ data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Testing...'}
+ >
+ {'Test Connection'}
+ </button>
+ {emailSuccess}
+ {emailFail}
</div>
</div>
</div>
- <div
- className='panel-group'
- id='accordion'
- role='tablist'
- aria-multiselectable='true'
- >
- <div className='panel panel-default'>
- <div
- className='panel-heading'
- role='tab'
- id='headingOne'
- >
- <h3 className='panel-title'>
- <a
- className='collapsed'
- role='button'
- data-toggle='collapse'
- data-parent='#accordion'
- href='#collapseOne'
- aria-expanded='true'
- aria-controls='collapseOne'
- >
- {'Advanced Settings '}
- <i className='fa fa-plus'></i>
- <i className='fa fa-minus'></i>
- </a>
- </h3>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='InviteSalt'
+ >
+ {'Invite Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='InviteSalt'
+ ref='InviteSalt'
+ placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ defaultValue={this.props.config.EmailSettings.InviteSalt}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerateInvite}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ {'Re-Generate'}
+ </button>
</div>
- <div
- id='collapseOne'
- className='panel-collapse collapse'
- role='tabpanel'
- aria-labelledby='headingOne'
- >
- <div className='panel-body'>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackUsername'
- >
- {'Apple push server:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackUsername'
- placeholder='Enter your Apple push server'
- value=''
- />
- <p className='help-text'>{'This is some sample help text for the Apple push server field'}</p>
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackUsername'
- >
- {'Apple push certificate public:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackUsername'
- placeholder='Enter your public apple push certificate'
- value=''
- />
- </div>
- </div>
- <div className='form-group'>
- <label
- className='control-label col-sm-4'
- htmlFor='feedbackUsername'
- >
- {'Apple push certificate private:'}
- </label>
- <div className='col-sm-8'>
- <input
- type='text'
- className='form-control'
- id='feedbackUsername'
- placeholder='Enter your private apple push certificate'
- value=''
- />
- </div>
- </div>
- </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PasswordResetSalt'
+ >
+ {'Password Reset Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PasswordResetSalt'
+ ref='PasswordResetSalt'
+ placeholder='Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
+ defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
+ onChange={this.handleChange}
+ disabled={!this.state.sendEmailNotifications}
+ />
+ <p className='help-text'>{'32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerateReset}
+ disabled={!this.state.sendEmailNotifications}
+ >
+ {'Re-Generate'}
+ </button>
</div>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
+ {serverError}
<button
+ disabled={!this.state.saveNeeded}
type='submit'
- className='btn btn-primary'
+ 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>
);
}
-} \ No newline at end of file
+}
+
+EmailSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/gitlab_settings.jsx b/web/react/components/admin_console/gitlab_settings.jsx
new file mode 100644
index 000000000..1e10c5592
--- /dev/null
+++ b/web/react/components/admin_console/gitlab_settings.jsx
@@ -0,0 +1,277 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class GitLabSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ Allow: this.props.config.GitLabSettings.Allow,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'AllowTrue') {
+ s.Allow = true;
+ }
+
+ if (action === 'AllowFalse') {
+ s.Allow = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.GitLabSettings.Allow = React.findDOMNode(this.refs.Allow).checked;
+ config.GitLabSettings.Secret = React.findDOMNode(this.refs.Secret).value.trim();
+ config.GitLabSettings.Id = React.findDOMNode(this.refs.Id).value.trim();
+ config.GitLabSettings.Scope = React.findDOMNode(this.refs.Scope).value.trim();
+ config.GitLabSettings.AuthEndpoint = React.findDOMNode(this.refs.AuthEndpoint).value.trim();
+ config.GitLabSettings.TokenEndpoint = React.findDOMNode(this.refs.TokenEndpoint).value.trim();
+ config.GitLabSettings.UserApiEndpoint = React.findDOMNode(this.refs.UserApiEndpoint).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>{'GitLab Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Allow'
+ >
+ {'Enable Sign Up With GitLab: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Allow'
+ value='true'
+ ref='Allow'
+ defaultChecked={this.props.config.GitLabSettings.Allow}
+ onChange={this.handleChange.bind(this, 'AllowTrue')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Allow'
+ value='false'
+ defaultChecked={!this.props.config.GitLabSettings.Allow}
+ onChange={this.handleChange.bind(this, 'AllowFalse')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, Mattermost allows team creation and account signup using GitLab OAuth. To configure, log in to your GitLab account and go to Applications -> Profile Settings. Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". Then use "Secret" and "Id" fields to complete the options below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Secret'
+ >
+ {'Secret:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Secret'
+ ref='Secret'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Secret}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Id'
+ >
+ {'Id:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Id'
+ ref='Id'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.GitLabSettings.Id}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Obtain this value via the instructions above for logging into GitLab'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Scope'
+ >
+ {'Scope:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Scope'
+ ref='Scope'
+ placeholder='Not currently used by GitLab. Please leave blank'
+ defaultValue={this.props.config.GitLabSettings.Scope}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AuthEndpoint'
+ >
+ {'Auth Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AuthEndpoint'
+ ref='AuthEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/authorize (example http://localhost:3000/oauth/authorize).'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='TokenEndpoint'
+ >
+ {'Token Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='TokenEndpoint'
+ ref='TokenEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Enter <your-gitlab-url>/oauth/token.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='UserApiEndpoint'
+ >
+ {'User API Endpoint:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='UserApiEndpoint'
+ ref='UserApiEndpoint'
+ placeholder='Ex ""'
+ defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
+ onChange={this.handleChange}
+ disabled={!this.state.Allow}
+ />
+ <p className='help-text'>{'Enter <your-gitlab-url>/api/v3/user.'}</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>
+ );
+ }
+}
+
+GitLabSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/image_settings.jsx b/web/react/components/admin_console/image_settings.jsx
new file mode 100644
index 000000000..e52f516e8
--- /dev/null
+++ b/web/react/components/admin_console/image_settings.jsx
@@ -0,0 +1,496 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class FileSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.bind(this);
+
+ this.state = {
+ saveNeeded: false,
+ serverError: null,
+ DriverName: this.props.config.FileSettings.DriverName
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'DriverName') {
+ s.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ }
+
+ this.setState(s);
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ 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.FileSettings.DriverName = React.findDOMNode(this.refs.DriverName).value;
+ config.FileSettings.Directory = React.findDOMNode(this.refs.Directory).value;
+ config.FileSettings.AmazonS3AccessKeyId = React.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
+ config.FileSettings.AmazonS3SecretAccessKey = React.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
+ config.FileSettings.AmazonS3Bucket = React.findDOMNode(this.refs.AmazonS3Bucket).value;
+ config.FileSettings.AmazonS3Region = React.findDOMNode(this.refs.AmazonS3Region).value;
+ config.FileSettings.EnablePublicLink = React.findDOMNode(this.refs.EnablePublicLink).checked;
+
+ config.FileSettings.PublicLinkSalt = React.findDOMNode(this.refs.PublicLinkSalt).value.trim();
+
+ if (config.FileSettings.PublicLinkSalt === '') {
+ config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
+ }
+
+ var thumbnailWidth = 120;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
+ thumbnailWidth = parseInt(React.findDOMNode(this.refs.ThumbnailWidth).value, 10);
+ }
+ config.FileSettings.ThumbnailWidth = thumbnailWidth;
+ React.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
+
+ var thumbnailHeight = 100;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
+ thumbnailHeight = parseInt(React.findDOMNode(this.refs.ThumbnailHeight).value, 10);
+ }
+ config.FileSettings.ThumbnailHeight = thumbnailHeight;
+ React.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
+
+ var previewWidth = 1024;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10))) {
+ previewWidth = parseInt(React.findDOMNode(this.refs.PreviewWidth).value, 10);
+ }
+ config.FileSettings.PreviewWidth = previewWidth;
+ React.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
+
+ var previewHeight = 0;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10))) {
+ previewHeight = parseInt(React.findDOMNode(this.refs.PreviewHeight).value, 10);
+ }
+ config.FileSettings.PreviewHeight = previewHeight;
+ React.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
+
+ var profileWidth = 128;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10))) {
+ profileWidth = parseInt(React.findDOMNode(this.refs.ProfileWidth).value, 10);
+ }
+ config.FileSettings.ProfileWidth = profileWidth;
+ React.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
+
+ var profileHeight = 128;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10))) {
+ profileHeight = parseInt(React.findDOMNode(this.refs.ProfileHeight).value, 10);
+ }
+ config.FileSettings.ProfileHeight = profileHeight;
+ React.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
+
+ 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';
+ }
+
+ var enableFile = false;
+ var enableS3 = false;
+
+ if (this.state.DriverName === 'local') {
+ enableFile = true;
+ }
+
+ if (this.state.DriverName === 'amazons3') {
+ enableS3 = true;
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'File Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ {'Store Files In:'}
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='DriverName'
+ ref='DriverName'
+ defaultValue={this.props.config.FileSettings.DriverName}
+ onChange={this.handleChange.bind(this, 'DriverName')}
+ >
+ <option value=''>{'Disable File Storage'}</option>
+ <option value='local'>{'Local File System'}</option>
+ <option value='amazons3'>{'Amazon S3'}</option>
+ </select>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Directory'
+ >
+ {'Local Directory Location:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='Directory'
+ ref='Directory'
+ placeholder='Ex "./data/"'
+ defaultValue={this.props.config.FileSettings.Directory}
+ onChange={this.handleChange}
+ disabled={!enableFile}
+ />
+ <p className='help-text'>{'Directory to which image files are written. If blank, will be set to ./data/.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3AccessKeyId'
+ >
+ {'Amazon S3 Access Key Id:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3AccessKeyId'
+ ref='AmazonS3AccessKeyId'
+ placeholder='Ex "AKIADTOVBGERKLCBV"'
+ defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3SecretAccessKey'
+ >
+ {'Amazon S3 Secret Access Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3SecretAccessKey'
+ ref='AmazonS3SecretAccessKey'
+ placeholder='Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
+ defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Obtain this credential from your Amazon EC2 administrator.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3Bucket'
+ >
+ {'Amazon S3 Bucket:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3Bucket'
+ ref='AmazonS3Bucket'
+ placeholder='Ex "mattermost-media"'
+ defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'Name you selected for your S3 bucket in AWS.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AmazonS3Region'
+ >
+ {'Amazon S3 Region:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AmazonS3Region'
+ ref='AmazonS3Region'
+ placeholder='Ex "us-east-1"'
+ defaultValue={this.props.config.FileSettings.AmazonS3Region}
+ onChange={this.handleChange}
+ disabled={!enableS3}
+ />
+ <p className='help-text'>{'AWS region you selected for creating your S3 bucket.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ThumbnailWidth'
+ >
+ {'Thumbnail Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ThumbnailWidth'
+ ref='ThumbnailWidth'
+ placeholder='Ex "120"'
+ defaultValue={this.props.config.FileSettings.ThumbnailWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ThumbnailHeight'
+ >
+ {'Thumbnail Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ThumbnailHeight'
+ ref='ThumbnailHeight'
+ placeholder='Ex "100"'
+ defaultValue={this.props.config.FileSettings.ThumbnailHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PreviewWidth'
+ >
+ {'Preview Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PreviewWidth'
+ ref='PreviewWidth'
+ placeholder='Ex "1024"'
+ defaultValue={this.props.config.FileSettings.PreviewWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PreviewHeight'
+ >
+ {'Preview Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PreviewHeight'
+ ref='PreviewHeight'
+ placeholder='Ex "0"'
+ defaultValue={this.props.config.FileSettings.PreviewHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ProfileWidth'
+ >
+ {'Profile Width:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ProfileWidth'
+ ref='ProfileWidth'
+ placeholder='Ex "1024"'
+ defaultValue={this.props.config.FileSettings.ProfileWidth}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Width of profile picture.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ProfileHeight'
+ >
+ {'Profile Height:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ProfileHeight'
+ ref='ProfileHeight'
+ placeholder='Ex "0"'
+ defaultValue={this.props.config.FileSettings.ProfileHeight}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Height of profile picture.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnablePublicLink'
+ >
+ {'Share Public File Link: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePublicLink'
+ value='true'
+ ref='EnablePublicLink'
+ defaultChecked={this.props.config.FileSettings.EnablePublicLink}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnablePublicLink'
+ value='false'
+ defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'Allow users to share public links to files and images.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PublicLinkSalt'
+ >
+ {'Public Link Salt:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PublicLinkSalt'
+ ref='PublicLinkSalt'
+ placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ defaultValue={this.props.config.FileSettings.PublicLinkSalt}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerate}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </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>
+ );
+ }
+}
+
+FileSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/log_settings.jsx b/web/react/components/admin_console/log_settings.jsx
index 4e3db8f68..1c39c60e8 100644
--- a/web/react/components/admin_console/log_settings.jsx
+++ b/web/react/components/admin_console/log_settings.jsx
@@ -12,13 +12,33 @@ export default class LogSettings extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
+ consoleEnable: this.props.config.LogSettings.EnableConsole,
+ fileEnable: this.props.config.LogSettings.EnableFile,
saveNeeded: false,
serverError: null
};
}
- handleChange() {
- this.setState({saveNeeded: true, serverError: this.state.serverError});
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'console_true') {
+ s.consoleEnable = true;
+ }
+
+ if (action === 'console_false') {
+ s.consoleEnable = false;
+ }
+
+ if (action === 'file_true') {
+ s.fileEnable = true;
+ }
+
+ if (action === 'file_false') {
+ s.fileEnable = false;
+ }
+
+ this.setState(s);
}
handleSubmit(e) {
@@ -26,9 +46,9 @@ export default class LogSettings extends React.Component {
$('#save-button').button('loading');
var config = this.props.config;
- config.LogSettings.ConsoleEnable = React.findDOMNode(this.refs.consoleEnable).checked;
+ config.LogSettings.EnableConsole = React.findDOMNode(this.refs.consoleEnable).checked;
config.LogSettings.ConsoleLevel = React.findDOMNode(this.refs.consoleLevel).value;
- config.LogSettings.FileEnable = React.findDOMNode(this.refs.fileEnable).checked;
+ config.LogSettings.EnableFile = React.findDOMNode(this.refs.fileEnable).checked;
config.LogSettings.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
@@ -37,11 +57,21 @@ export default class LogSettings extends React.Component {
config,
() => {
AsyncClient.getConfig();
- this.setState({serverError: null, saveNeeded: false});
+ this.setState({
+ consoleEnable: config.LogSettings.EnableConsole,
+ fileEnable: config.LogSettings.EnableFile,
+ serverError: null,
+ saveNeeded: false
+ });
$('#save-button').button('reset');
},
(err) => {
- this.setState({serverError: err.message, saveNeeded: true});
+ this.setState({
+ consoleEnable: config.LogSettings.EnableConsole,
+ fileEnable: config.LogSettings.EnableFile,
+ serverError: err.message,
+ saveNeeded: true
+ });
$('#save-button').button('reset');
}
);
@@ -71,7 +101,7 @@ export default class LogSettings extends React.Component {
className='control-label col-sm-4'
htmlFor='consoleEnable'
>
- {'Log To the Console: '}
+ {'Log To The Console: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
@@ -80,8 +110,8 @@ export default class LogSettings extends React.Component {
name='consoleEnable'
value='true'
ref='consoleEnable'
- defaultChecked={this.props.config.LogSettings.ConsoleEnable}
- onChange={this.handleChange}
+ defaultChecked={this.props.config.LogSettings.EnableConsole}
+ onChange={this.handleChange.bind(this, 'console_true')}
/>
{'true'}
</label>
@@ -90,12 +120,12 @@ export default class LogSettings extends React.Component {
type='radio'
name='consoleEnable'
value='false'
- defaultChecked={!this.props.config.LogSettings.ConsoleEnable}
- onChange={this.handleChange}
+ defaultChecked={!this.props.config.LogSettings.EnableConsole}
+ onChange={this.handleChange.bind(this, 'console_false')}
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true then the server will output messages to the standard output stream (stdout).'}</p>
+ <p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'}</p>
</div>
</div>
@@ -113,12 +143,13 @@ export default class LogSettings extends React.Component {
ref='consoleLevel'
defaultValue={this.props.config.LogSettings.consoleLevel}
onChange={this.handleChange}
+ disabled={!this.state.consoleEnable}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
- <p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
+ <p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}</p>
</div>
</div>
@@ -135,8 +166,8 @@ export default class LogSettings extends React.Component {
name='fileEnable'
ref='fileEnable'
value='true'
- defaultChecked={this.props.config.LogSettings.FileEnable}
- onChange={this.handleChange}
+ defaultChecked={this.props.config.LogSettings.EnableFile}
+ onChange={this.handleChange.bind(this, 'file_true')}
/>
{'true'}
</label>
@@ -145,12 +176,12 @@ export default class LogSettings extends React.Component {
type='radio'
name='fileEnable'
value='false'
- defaultChecked={!this.props.config.LogSettings.FileEnable}
- onChange={this.handleChange}
+ defaultChecked={!this.props.config.LogSettings.EnableFile}
+ onChange={this.handleChange.bind(this, 'file_false')}
/>
{'false'}
</label>
- <p className='help-text'>{'Typically set to true in production. When true log files are written to the file specified in file location field below.'}</p>
+ <p className='help-text'>{'Typically set to true in production. When true, log files are written to the log file specified in file location field below.'}</p>
</div>
</div>
@@ -168,12 +199,13 @@ export default class LogSettings extends React.Component {
ref='fileLevel'
defaultValue={this.props.config.LogSettings.FileLevel}
onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
- <p className='help-text'>{'This setting determines the level of detail at which log events are written to the file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
+ <p className='help-text'>{'This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'}</p>
</div>
</div>
@@ -193,8 +225,9 @@ export default class LogSettings extends React.Component {
placeholder='Enter your file location'
defaultValue={this.props.config.LogSettings.FileLocation}
onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
/>
- <p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p>
+ <p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'}</p>
</div>
</div>
@@ -214,6 +247,7 @@ export default class LogSettings extends React.Component {
placeholder='Enter your file format'
defaultValue={this.props.config.LogSettings.FileFormat}
onChange={this.handleChange}
+ disabled={!this.state.fileEnable}
/>
<p className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx
new file mode 100644
index 000000000..affd8ae11
--- /dev/null
+++ b/web/react/components/admin_console/privacy_settings.jsx
@@ -0,0 +1,163 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class PrivacySettings 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.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked;
+ config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked;
+
+ 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>{'Privacy Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ShowEmailAddress'
+ >
+ {'Show Email Address: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowEmailAddress'
+ value='true'
+ ref='ShowEmailAddress'
+ defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowEmailAddress'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ShowFullName'
+ >
+ {'Show Full Name: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowFullName'
+ value='true'
+ ref='ShowFullName'
+ defaultChecked={this.props.config.PrivacySettings.ShowFullName}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='ShowFullName'
+ value='false'
+ defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, hides full name of users from other users including team owner and team administrators.'}</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>
+ );
+ }
+}
+
+PrivacySettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/rate_settings.jsx b/web/react/components/admin_console/rate_settings.jsx
new file mode 100644
index 000000000..0081daca3
--- /dev/null
+++ b/web/react/components/admin_console/rate_settings.jsx
@@ -0,0 +1,272 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class RateSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
+ VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
+ saveNeeded: false,
+ serverError: null
+ };
+ }
+
+ handleChange(action) {
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+
+ if (action === 'EnableRateLimiterTrue') {
+ s.EnableRateLimiter = true;
+ }
+
+ if (action === 'EnableRateLimiterFalse') {
+ s.EnableRateLimiter = false;
+ }
+
+ if (action === 'VaryByRemoteAddrTrue') {
+ s.VaryByRemoteAddr = true;
+ }
+
+ if (action === 'VaryByRemoteAddrFalse') {
+ s.VaryByRemoteAddr = false;
+ }
+
+ this.setState(s);
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ $('#save-button').button('loading');
+
+ var config = this.props.config;
+ config.RateLimitSettings.EnableRateLimiter = React.findDOMNode(this.refs.EnableRateLimiter).checked;
+ config.RateLimitSettings.VaryByRemoteAddr = React.findDOMNode(this.refs.VaryByRemoteAddr).checked;
+ config.RateLimitSettings.VaryByHeader = React.findDOMNode(this.refs.VaryByHeader).value.trim();
+
+ var PerSec = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.PerSec).value, 10))) {
+ PerSec = parseInt(React.findDOMNode(this.refs.PerSec).value, 10);
+ }
+ config.RateLimitSettings.PerSec = PerSec;
+ React.findDOMNode(this.refs.PerSec).value = PerSec;
+
+ var MemoryStoreSize = 10000;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
+ MemoryStoreSize = parseInt(React.findDOMNode(this.refs.MemoryStoreSize).value, 10);
+ }
+ config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
+ React.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
+
+ 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'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <h3>{'Rate Limit Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableRateLimiter'
+ >
+ {'Enable Rate Limiter: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableRateLimiter'
+ value='true'
+ ref='EnableRateLimiter'
+ defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
+ onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableRateLimiter'
+ value='false'
+ defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
+ onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, APIs are throttled at rates specified below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='PerSec'
+ >
+ {'Number Of Queries Per Second:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='PerSec'
+ ref='PerSec'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.RateLimitSettings.PerSec}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ <p className='help-text'>{'Throttles API at this number of requests per second.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MemoryStoreSize'
+ >
+ {'Memory Store Size:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MemoryStoreSize'
+ ref='MemoryStoreSize'
+ placeholder='Ex "10000"'
+ defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ <p className='help-text'>{'Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='VaryByRemoteAddr'
+ >
+ {'Vary By Remote Address: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='VaryByRemoteAddr'
+ value='true'
+ ref='VaryByRemoteAddr'
+ defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
+ onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='VaryByRemoteAddr'
+ value='false'
+ defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
+ onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
+ disabled={!this.state.EnableRateLimiter}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, rate limit API access by IP address.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='VaryByHeader'
+ >
+ {'Vary By HTTP Header:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='VaryByHeader'
+ ref='VaryByHeader'
+ placeholder='Ex "X-Real-IP", "X-Forwarded-For"'
+ defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
+ onChange={this.handleChange}
+ disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
+ />
+ <p className='help-text'>{'When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring Ngnix set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'}</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>
+ );
+ }
+}
+
+RateSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..0b83edb17
--- /dev/null
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,132 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = React.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < 5) {
+ this.setState({serverError: 'Please enter at least 5 characters.'});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Reset Password'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ {'New Password'}
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index fa30de7b2..343f65131 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -1,124 +1,99 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-export default class SelectTeam extends React.Component {
+var Modal = ReactBootstrap.Modal;
+
+export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
- this.state = {
- };
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
}
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
return (
- <div className='modal fade'
- id='select-team'
- tabIndex='-1'
- role='dialog'
- aria-labelledby='teamsModalLabel'
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
>
- <div className='modal-dialog'
- role='document'
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Select Team'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='teamsModalLabel'
- >
- {'Select a team'}
- </h4>
- </div>
- <div className='modal-body'>
- <table className='more-channel-table table'>
- <tbody>
- <tr>
- <td>
- <p className='more-channel-name'>{'Descartes'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Grouping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Adventure'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Crossroads'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Sky scraping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Outdoors'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Microsoft'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Apple'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Close'}
- </button>
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ style={{width: '100%'}}
+ >
+ {options}
+ </select>
+ </div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
);
}
-} \ No newline at end of file
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
new file mode 100644
index 000000000..245ffa871
--- /dev/null
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -0,0 +1,296 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class ServiceSettings 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.ServiceSettings.ListenAddress = React.findDOMNode(this.refs.ListenAddress).value.trim();
+ if (config.ServiceSettings.ListenAddress === '') {
+ config.ServiceSettings.ListenAddress = ':8065';
+ React.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
+ }
+
+ config.ServiceSettings.SegmentDeveloperKey = React.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
+ config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
+ //config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
+ config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
+ config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked;
+
+ var MaximumLoginAttempts = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
+ MaximumLoginAttempts = parseInt(React.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
+ }
+ config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
+ React.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
+
+ 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>{'Service Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='ListenAddress'
+ >
+ {'Listen Address:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='ListenAddress'
+ ref='ListenAddress'
+ placeholder='Ex ":8065"'
+ defaultValue={this.props.config.ServiceSettings.ListenAddress}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaximumLoginAttempts'
+ >
+ {'Maximum Login Attempts:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaximumLoginAttempts'
+ ref='MaximumLoginAttempts'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Login attempts allowed before user is locked out and required to reset password via email.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SegmentDeveloperKey'
+ >
+ {'Segment Developer Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SegmentDeveloperKey'
+ ref='SegmentDeveloperKey'
+ placeholder='Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
+ defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'For users running a SaaS services, sign up for a key at Segment.com to track metrics.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='GoogleDeveloperKey'
+ >
+ {'Google Developer Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='GoogleDeveloperKey'
+ ref='GoogleDeveloperKey'
+ placeholder='Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
+ defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at '}<a href='https://www.youtube.com/watch?v=Im69kzhpR3I'>{'https://www.youtube.com/watch?v=Im69kzhpR3I'}</a>{'. Leaving field blank disables the automatic generation of YouTube video previews from links.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableIncomingWebhooks'
+ >
+ {'Enable Incoming Webhooks: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableIncomingWebhooks'
+ value='true'
+ ref='EnableIncomingWebhooks'
+ defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableIncomingWebhooks'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTesting'
+ >
+ {'Enable Testing: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTesting'
+ value='true'
+ ref='EnableTesting'
+ defaultChecked={this.props.config.ServiceSettings.EnableTesting}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTesting'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'}</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>
+ );
+ }
+}
+
+// <div className='form-group'>
+// <label
+// className='control-label col-sm-4'
+// htmlFor='EnableOAuthServiceProvider'
+// >
+// {'Enable OAuth Service Provider: '}
+// </label>
+// <div className='col-sm-8'>
+// <label className='radio-inline'>
+// <input
+// type='radio'
+// name='EnableOAuthServiceProvider'
+// value='true'
+// ref='EnableOAuthServiceProvider'
+// defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+// onChange={this.handleChange}
+// />
+// {'true'}
+// </label>
+// <label className='radio-inline'>
+// <input
+// type='radio'
+// name='EnableOAuthServiceProvider'
+// value='false'
+// defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
+// onChange={this.handleChange}
+// />
+// {'false'}
+// </label>
+// <p className='help-text'>{'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}</p>
+// </div>
+// </div>
+
+ServiceSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/sql_settings.jsx b/web/react/components/admin_console/sql_settings.jsx
new file mode 100644
index 000000000..430a7453b
--- /dev/null
+++ b/web/react/components/admin_console/sql_settings.jsx
@@ -0,0 +1,283 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var crypto = require('crypto');
+
+export default class SqlSettings extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleGenerate = this.handleGenerate.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.SqlSettings.Trace = React.findDOMNode(this.refs.Trace).checked;
+ config.SqlSettings.AtRestEncryptKey = React.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
+
+ if (config.SqlSettings.AtRestEncryptKey === '') {
+ config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ React.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
+ }
+
+ var MaxOpenConns = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
+ MaxOpenConns = parseInt(React.findDOMNode(this.refs.MaxOpenConns).value, 10);
+ }
+ config.SqlSettings.MaxOpenConns = MaxOpenConns;
+ React.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
+
+ var MaxIdleConns = 10;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
+ MaxIdleConns = parseInt(React.findDOMNode(this.refs.MaxIdleConns).value, 10);
+ }
+ config.SqlSettings.MaxIdleConns = MaxIdleConns;
+ React.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
+
+ 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');
+ }
+ );
+ }
+
+ handleGenerate(e) {
+ e.preventDefault();
+ React.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
+ var s = {saveNeeded: true, serverError: this.state.serverError};
+ this.setState(s);
+ }
+
+ 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';
+ }
+
+ var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
+
+ var dataSourceReplicas = '';
+ this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
+ dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
+ });
+
+ if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
+ dataSourceReplicas = 'none';
+ }
+
+ return (
+ <div className='wrapper--fixed'>
+
+ <div className='banner'>
+ <div className='banner__content'>
+ <h4 className='banner__heading'>{'Note:'}</h4>
+ <p>{'Changing properties in this section will require a server restart before taking effect.'}</p>
+ </div>
+ </div>
+
+ <h3>{'SQL Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DriverName'
+ >
+ {'Driver Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSource'
+ >
+ {'Data Source:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSource}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='DataSourceReplicas'
+ >
+ {'Data Source Replicas:'}
+ </label>
+ <div className='col-sm-8'>
+ <p className='help-text'>{dataSourceReplicas}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxIdleConns'
+ >
+ {'Maximum Idle Connections:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxIdleConns'
+ ref='MaxIdleConns'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.SqlSettings.MaxIdleConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of idle connections held open to the database.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxOpenConns'
+ >
+ {'Maximum Open Connections:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxOpenConns'
+ ref='MaxOpenConns'
+ placeholder='Ex "10"'
+ defaultValue={this.props.config.SqlSettings.MaxOpenConns}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum number of open connections held open to the database.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='AtRestEncryptKey'
+ >
+ {'At Rest Encrypt Key:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='AtRestEncryptKey'
+ ref='AtRestEncryptKey'
+ placeholder='Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
+ defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'32-character salt available to encrypt and decrypt sensitive fields in database.'}</p>
+ <div className='help-text'>
+ <button
+ className='help-link'
+ onClick={this.handleGenerate}
+ >
+ {'Re-Generate'}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='Trace'
+ >
+ {'Trace: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='true'
+ ref='Trace'
+ defaultChecked={this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='Trace'
+ value='false'
+ defaultChecked={!this.props.config.SqlSettings.Trace}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'(Development Mode) When true, executing SQL statements are written to the log.'}</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>
+ );
+ }
+}
+
+SqlSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx
new file mode 100644
index 000000000..0f6f819d3
--- /dev/null
+++ b/web/react/components/admin_console/team_settings.jsx
@@ -0,0 +1,235 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+
+export default class TeamSettings 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.TeamSettings.SiteName = React.findDOMNode(this.refs.SiteName).value.trim();
+ config.TeamSettings.RestrictCreationToDomains = React.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
+ config.TeamSettings.EnableTeamCreation = React.findDOMNode(this.refs.EnableTeamCreation).checked;
+ config.TeamSettings.EnableUserCreation = React.findDOMNode(this.refs.EnableUserCreation).checked;
+
+ var MaxUsersPerTeam = 50;
+ if (!isNaN(parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
+ MaxUsersPerTeam = parseInt(React.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
+ }
+ config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
+ React.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
+
+ 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>{'Team Settings'}</h3>
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='SiteName'
+ >
+ {'Site Name:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='SiteName'
+ ref='SiteName'
+ placeholder='Ex "Mattermost"'
+ defaultValue={this.props.config.TeamSettings.SiteName}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Name of service shown in login screens and UI.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='MaxUsersPerTeam'
+ >
+ {'Max Users Per Team:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='MaxUsersPerTeam'
+ ref='MaxUsersPerTeam'
+ placeholder='Ex "25"'
+ defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Maximum total number of users per team, including both active and inactive users.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableTeamCreation'
+ >
+ {'Enable Team Creation: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='true'
+ ref='EnableTeamCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableTeamCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, the ability to create teams is disabled. The create team button displays error when pressed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableUserCreation'
+ >
+ {'Enable User Creation: '}
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='true'
+ ref='EnableUserCreation'
+ defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableUserCreation'
+ value='false'
+ defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
+ onChange={this.handleChange}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>{'When false, the ability to create accounts is disabled. The create account button displays error when pressed.'}</p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='RestrictCreationToDomains'
+ >
+ {'Restrict Creation To Domains:'}
+ </label>
+ <div className='col-sm-8'>
+ <input
+ type='text'
+ className='form-control'
+ id='RestrictCreationToDomains'
+ ref='RestrictCreationToDomains'
+ placeholder='Ex "corp.mattermost.com, mattermost.org"'
+ defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
+ onChange={this.handleChange}
+ />
+ <p className='help-text'>{'Teams can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'}</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>
+ );
+ }
+}
+
+TeamSettings.propTypes = {
+ config: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..0a971ff15
--- /dev/null
+++ b/web/react/components/admin_console/team_users.jsx
@@ -0,0 +1,178 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var UserItem = require('./user_item.jsx');
+var ResetPasswordModal = require('./reset_password_modal.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ this.getTeamProfiles(this.props.team.id);
+ }
+
+ // this.setState({
+ // teamId: this.state.teamId,
+ // users: this.state.users,
+ // serverError: this.state.serverError,
+ // showPasswordModal: this.state.showPasswordModal,
+ // user: this.state.user
+ // });
+
+ getTeamProfiles(teamId) {
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.state.showPasswordModal = false;
+ this.state.user = null;
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ if (this.state.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name}</h3>
+ {serverError}
+ <LoadingScreen />
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='member-list-holder'>
+ {memberList}
+ </div>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..32812e875
--- /dev/null
+++ b/web/react/components/admin_console/user_item.jsx
@@ -0,0 +1,266 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+
+ this.state = {};
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = 'Member';
+ if (user.roles.length > 0) {
+ if (user.roles.indexOf('system_admin') > -1) {
+ currentRoles = 'System Admin';
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = 'Inactive';
+ currentRoles = 'Inactive';
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ {'Make System Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ {'Make Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ {'Make Member'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ {'Make Active'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ {'Make Inactive'}
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </div>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 8d23ec646..b81936b57 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -55,7 +55,7 @@ export default class ChannelHeader extends React.Component {
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState);
}
- $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover click', html: true, delay: {show: 500, hide: 500}});
+ $('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
}
onSocketChange(msg) {
if (msg.action === 'new_user') {
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index ce6f60f87..39c86405c 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -12,6 +12,7 @@ var PostStore = require('../stores/post_store.jsx');
var UserStore = require('../stores/user_store.jsx');
var Utils = require('../utils/utils.jsx');
+var Constants = require('../utils/constants.jsx');
export default class ChannelLoader extends React.Component {
constructor(props) {
@@ -68,33 +69,19 @@ export default class ChannelLoader extends React.Component {
/* Update CSS classes to match user theme */
var user = UserStore.getCurrentUser();
- if (user.props && user.props.theme) {
- Utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';');
- Utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';');
- Utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';');
- Utils.changeCss('.mention', 'background: ' + user.props.theme + ';');
- Utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';');
- Utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}');
- Utils.changeCss('.search-item-container:hover', 'background: ' + Utils.changeOpacity(user.props.theme, 0.05) + ';');
- }
-
- if (user.props.theme !== '#000000' && user.props.theme !== '#585858') {
- Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, -10) + ';');
- Utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;');
- } else if (user.props.theme === '#000000') {
- Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +50) + ';');
- $('.team__header').addClass('theme--black');
- } else if (user.props.theme === '#585858') {
- Utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + Utils.changeColor(user.props.theme, +10) + ';');
- $('.team__header').addClass('theme--gray');
+ if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
+ Utils.applyTheme(user.theme_props);
+ } else {
+ Utils.applyTheme(Constants.THEMES.default);
}
/* Setup global mouse events */
- $('body').on('click.userpopover', function popOver(e) {
- if ($(e.target).attr('data-toggle') !== 'popover' &&
- $(e.target).parents('.popover.in').length === 0) {
- $('.user-popover').popover('hide');
- }
+ $('body').on('click', function hidePopover(e) {
+ $('[data-toggle="popover"]').each(function eachPopover() {
+ if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) {
+ $(this).popover('hide');
+ }
+ });
});
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
@@ -122,6 +109,13 @@ export default class ChannelLoader extends React.Component {
$('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7);
});
+
+ /* Prevent backspace from navigating back a page */
+ $(window).on('keydown.preventBackspace', (e) => {
+ if (e.which === 8 && !$(e.target).is('input, textarea')) {
+ e.preventDefault();
+ }
+ });
}
componentWillUnmount() {
clearInterval(this.intervalId);
@@ -136,6 +130,8 @@ export default class ChannelLoader extends React.Component {
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
+
+ $(window).off('keydown.preventBackspace');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 83067240d..9eda68b38 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -163,10 +163,22 @@ export default class ChannelNotifications extends React.Component {
}.bind(this);
let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (<span>These settings will override the global notification settings</span>);
+ let extraInfo = (
+ <span>
+ These settings will override the global notification settings.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
if (curChannel && curChannel.display_name) {
- extraInfo = (<span>These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel</span>);
+ extraInfo = (
+ <span>
+ These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
}
return (
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index c2fc0dcf3..5097b3aa5 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -28,6 +28,7 @@ export default class CreateComment extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
@@ -42,6 +43,12 @@ export default class CreateComment extends React.Component {
submitting: false
};
}
+ componentDidUpdate(prevProps, prevState) {
+ if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
+ $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
+ $('.post-right__scroll').perfectScrollbar('update');
+ }
+ }
handleSubmit(e) {
e.preventDefault();
@@ -178,6 +185,11 @@ export default class CreateComment extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
let previews = this.state.previews;
let uploadsInProgress = this.state.uploadsInProgress;
@@ -264,6 +276,7 @@ export default class CreateComment extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='comment'
channelId={this.props.channelId}
/>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index d9e67836d..0cd14747d 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -23,6 +23,7 @@ export default class CreatePost extends React.Component {
this.lastTime = 0;
+ this.getCurrentDraft = this.getCurrentDraft.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
@@ -30,35 +31,52 @@ export default class CreatePost extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
PostStore.clearDraftUploads();
- const draft = PostStore.getCurrentDraft();
- let previews = [];
- let messageText = '';
- let uploadsInProgress = [];
- if (draft && draft.previews && draft.message) {
- previews = draft.previews;
- messageText = draft.message;
- uploadsInProgress = draft.uploadsInProgress;
- }
+ const draft = this.getCurrentDraft();
this.state = {
channelId: ChannelStore.getCurrentId(),
- messageText: messageText,
- uploadsInProgress: uploadsInProgress,
- previews: previews,
+ messageText: draft.messageText,
+ uploadsInProgress: draft.uploadsInProgress,
+ previews: draft.previews,
submitting: false,
- initialText: messageText
+ initialText: draft.messageText
};
}
componentDidUpdate(prevProps, prevState) {
if (prevState.previews.length !== this.state.previews.length) {
this.resizePostHolder();
+ return;
+ }
+
+ if (prevState.uploadsInProgress !== this.state.uploadsInProgress) {
+ this.resizePostHolder();
+ return;
+ }
+ }
+ getCurrentDraft() {
+ const draft = PostStore.getCurrentDraft();
+ const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
+
+ if (draft) {
+ if (draft.message) {
+ safeDraft.messageText = draft.message;
+ }
+ if (draft.previews) {
+ safeDraft.previews = draft.previews;
+ }
+ if (draft.uploadsInProgress) {
+ safeDraft.uploadsInProgress = draft.uploadsInProgress;
+ }
}
+
+ return safeDraft;
}
handleSubmit(e) {
e.preventDefault();
@@ -67,7 +85,7 @@ export default class CreatePost extends React.Component {
return;
}
- let post = {};
+ const post = {};
post.filenames = [];
post.message = this.state.messageText;
@@ -87,20 +105,20 @@ export default class CreatePost extends React.Component {
this.state.channelId,
post.message,
false,
- function handleCommandSuccess(data) {
+ (data) => {
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
if (data.goto_location.length > 0) {
window.location.href = data.goto_location;
}
- }.bind(this),
- function handleCommandError(err) {
- let state = {};
+ },
+ (err) => {
+ const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
- }.bind(this)
+ }
);
} else {
post.channel_id = this.state.channelId;
@@ -121,10 +139,10 @@ export default class CreatePost extends React.Component {
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
- function handlePostSuccess(data) {
+ (data) => {
AsyncClient.getPosts();
- let member = ChannelStore.getMember(channel.id);
+ const member = ChannelStore.getMember(channel.id);
member.msg_count = channel.total_msg_count;
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
@@ -134,8 +152,8 @@ export default class CreatePost extends React.Component {
post: data
});
},
- function handlePostError(err) {
- let state = {};
+ (err) => {
+ const state = {};
if (err.message === 'Invalid RootId parameter') {
if ($('#post_deleted').length > 0) {
@@ -149,7 +167,7 @@ export default class CreatePost extends React.Component {
state.submitting = false;
this.setState(state);
- }.bind(this)
+ }
);
}
}
@@ -167,9 +185,9 @@ export default class CreatePost extends React.Component {
}
}
handleUserInput(messageText) {
- this.setState({messageText: messageText});
+ this.setState({messageText});
- let draft = PostStore.getCurrentDraft();
+ const draft = PostStore.getCurrentDraft();
draft.message = messageText;
PostStore.storeCurrentDraft(draft);
}
@@ -179,7 +197,7 @@ export default class CreatePost extends React.Component {
$(window).trigger('resize');
}
handleUploadStart(clientIds, channelId) {
- let draft = PostStore.getDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeDraft(channelId, draft);
@@ -187,7 +205,7 @@ export default class CreatePost extends React.Component {
this.setState({uploadsInProgress: draft.uploadsInProgress});
}
handleFileUploadComplete(filenames, clientIds, channelId) {
- let draft = PostStore.getDraft(channelId);
+ const draft = PostStore.getDraft(channelId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
@@ -205,7 +223,7 @@ export default class CreatePost extends React.Component {
}
handleUploadError(err, clientId) {
if (clientId !== -1) {
- let draft = PostStore.getDraft(this.state.channelId);
+ const draft = PostStore.getDraft(this.state.channelId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
@@ -219,9 +237,14 @@ export default class CreatePost extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
- let previews = this.state.previews;
- let uploadsInProgress = this.state.uploadsInProgress;
+ const previews = Object.assign([], this.state.previews);
+ const uploadsInProgress = this.state.uploadsInProgress;
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
@@ -236,12 +259,12 @@ export default class CreatePost extends React.Component {
}
}
- let draft = PostStore.getCurrentDraft();
+ const draft = PostStore.getCurrentDraft();
draft.previews = previews;
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCurrentDraft(draft);
- this.setState({previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({previews, uploadsInProgress});
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
@@ -253,18 +276,9 @@ export default class CreatePost extends React.Component {
onChange() {
const channelId = ChannelStore.getCurrentId();
if (this.state.channelId !== channelId) {
- let draft = PostStore.getCurrentDraft();
-
- let previews = [];
- let messageText = '';
- let uploadsInProgress = [];
- if (draft && draft.previews && draft.message) {
- previews = draft.previews;
- messageText = draft.message;
- uploadsInProgress = draft.uploadsInProgress;
- }
+ const draft = this.getCurrentDraft();
- this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress});
+ this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
getFileCount(channelId) {
@@ -332,6 +346,7 @@ export default class CreatePost extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='post'
channelId=''
/>
diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx
index 4efb9cb23..44c54db72 100644
--- a/web/react/components/delete_channel_modal.jsx
+++ b/web/react/components/delete_channel_modal.jsx
@@ -4,6 +4,7 @@
const Client = require('../utils/client.jsx');
const AsyncClient = require('../utils/async_client.jsx');
const ChannelStore = require('../stores/channel_store.jsx');
+var TeamStore = require('../stores/team_store.jsx');
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -24,7 +25,7 @@ export default class DeleteChannelModal extends React.Component {
Client.deleteChannel(this.state.channelId,
function handleDeleteSuccess() {
AsyncClient.getChannels(true);
- window.location.href = '/';
+ window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
function handleDeleteError(err) {
AsyncClient.dispatchError(err, 'handleDelete');
diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx
index 92123956f..8d3f15525 100644
--- a/web/react/components/email_verify.jsx
+++ b/web/react/components/email_verify.jsx
@@ -10,12 +10,14 @@ export default class EmailVerify extends React.Component {
this.state = {};
}
handleResend() {
- window.location.href = window.location.href + '&resend=true';
+ const newAddress = window.location.href.replace('&resend_success=true', '');
+ window.location.href = newAddress + '&resend=true';
}
render() {
var title = '';
var body = '';
var resend = '';
+ var resendConfirm = '';
if (this.props.isVerified === 'true') {
title = global.window.config.SiteName + ' Email Verified';
body = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>;
@@ -30,6 +32,9 @@ export default class EmailVerify extends React.Component {
Resend Email
</button>
);
+ if (this.props.resendSuccess) {
+ resendConfirm = <div><br /><p className='alert alert-success'><i className='fa fa-check'></i>{' Verification email sent.'}</p></div>;
+ }
}
return (
@@ -41,6 +46,7 @@ export default class EmailVerify extends React.Component {
<div className='panel-body'>
{body}
{resend}
+ {resendConfirm}
</div>
</div>
</div>
@@ -51,10 +57,12 @@ export default class EmailVerify extends React.Component {
EmailVerify.defaultProps = {
isVerified: 'false',
teamURL: '',
- userEmail: ''
+ userEmail: '',
+ resendSuccess: 'false'
};
EmailVerify.propTypes = {
isVerified: React.PropTypes.string,
teamURL: React.PropTypes.string,
- userEmail: React.PropTypes.string
+ userEmail: React.PropTypes.string,
+ resendSuccess: React.PropTypes.string
};
diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx
index 87d94a41d..05726e860 100644
--- a/web/react/components/error_bar.jsx
+++ b/web/react/components/error_bar.jsx
@@ -2,10 +2,6 @@
// See License.txt for license information.
var ErrorStore = require('../stores/error_store.jsx');
-var utils = require('../utils/utils.jsx');
-var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
-var Constants = require('../utils/constants.jsx');
-var ActionTypes = Constants.ActionTypes;
export default class ErrorBar extends React.Component {
constructor() {
@@ -13,70 +9,79 @@ export default class ErrorBar extends React.Component {
this.onErrorChange = this.onErrorChange.bind(this);
this.handleClose = this.handleClose.bind(this);
+ this.prevTimer = null;
- this.state = this.getStateFromStores();
- if (this.state.message) {
- setTimeout(this.handleClose, 10000);
+ this.state = ErrorStore.getLastError();
+ if (this.state && this.state.message) {
+ this.prevTimer = setTimeout(this.handleClose, 10000);
}
}
- getStateFromStores() {
- var error = ErrorStore.getLastError();
- if (!error || error.message === 'There appears to be a problem with your internet connection') {
- return {message: null};
- }
- return {message: error.message};
- }
componentDidMount() {
ErrorStore.addChangeListener(this.onErrorChange);
$('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
- $(window).resize(function onResize() {
- if (this.state.message) {
+ $(window).resize(() => {
+ if (this.state && this.state.message) {
$('body').css('padding-top', $(React.findDOMNode(this)).outerHeight());
}
- }.bind(this));
+ });
}
+
componentWillUnmount() {
ErrorStore.removeChangeListener(this.onErrorChange);
}
+
onErrorChange() {
- var newState = this.getStateFromStores();
- if (!utils.areStatesEqual(newState, this.state)) {
- if (newState.message) {
- setTimeout(this.handleClose, 10000);
- }
+ var newState = ErrorStore.getLastError();
+
+ if (this.prevTimer != null) {
+ clearInterval(this.prevTimer);
+ this.prevTimer = null;
+ }
+ if (newState) {
this.setState(newState);
+ this.prevTimer = setTimeout(this.handleClose, 10000);
+ } else {
+ this.setState({message: null});
}
}
+
handleClose(e) {
if (e) {
e.preventDefault();
}
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_ERROR,
- err: null
- });
+ ErrorStore.storeLastError(null);
+ ErrorStore.emitChange();
$('body').css('padding-top', '0');
}
+
render() {
- if (this.state.message) {
- return (
- <div className='error-bar'>
- <span>{this.state.message}</span>
- <a
- href='#'
- className='error-bar__close'
- onClick={this.handleClose}
- >
- &times;
- </a>
- </div>
- );
+ if (!this.state) {
+ return <div/>;
+ }
+
+ if (!this.state.message) {
+ return <div/>;
+ }
+
+ if (this.state.connErrorCount < 7) {
+ return <div/>;
}
- return <div/>;
+ return (
+ <div className='error-bar'>
+ <span>{this.state.message}</span>
+ <a
+ href='#'
+ className='error-bar__close'
+ onClick={this.handleClose}
+ >
+ &times;
+ </a>
+ </div>
+ );
}
}
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c9aa06a97..888f24aa5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -143,10 +143,7 @@ export default class FileAttachment extends React.Component {
>
<a className='post-image__thumbnail'
href='#'
- onClick={this.props.handleImageClick}
- data-img-id={this.props.index}
- data-toggle='modal'
- data-target={'#' + this.props.modalId}
+ onClick={() => this.props.handleImageClick(this.props.index)}
>
{thumbnail}
</a>
@@ -187,9 +184,6 @@ FileAttachment.propTypes = {
// the index of this attachment preview in the parent FileAttachmentList
index: React.PropTypes.number.isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
- // handler for when the thumbnail is clicked
+ // handler for when the thumbnail is clicked passed the index above
handleImageClick: React.PropTypes.func
};
diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx
index abe72089a..212d4a958 100644
--- a/web/react/components/file_attachment_list.jsx
+++ b/web/react/components/file_attachment_list.jsx
@@ -11,23 +11,21 @@ export default class FileAttachmentList extends React.Component {
this.handleImageClick = this.handleImageClick.bind(this);
- this.state = {startImgId: 0};
+ this.state = {showPreviewModal: false, startImgId: 0};
}
- handleImageClick(e) {
- this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'), 10)});
+ handleImageClick(indexClicked) {
+ this.setState({showPreviewModal: true, startImgId: indexClicked});
}
render() {
var filenames = this.props.filenames;
- var modalId = this.props.modalId;
var postFiles = [];
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
postFiles.push(
<FileAttachment
- key={i}
+ key={'file_attachment_' + i}
filename={filenames[i]}
index={i}
- modalId={modalId}
handleImageClick={this.handleImageClick}
/>
);
@@ -39,9 +37,10 @@ export default class FileAttachmentList extends React.Component {
{postFiles}
</div>
<ViewImageModal
+ show={this.state.showPreviewModal}
+ onModalDismissed={() => this.setState({showPreviewModal: false})}
channelId={this.props.channelId}
userId={this.props.userId}
- modalId={modalId}
startId={this.state.startImgId}
filenames={filenames}
/>
@@ -55,9 +54,6 @@ FileAttachmentList.propTypes = {
// a list of file pathes displayed by this
filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
// the channel that this is part of
channelId: React.PropTypes.string,
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 3cb284171..3dc4e5de2 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -110,7 +110,7 @@ export default class FileUpload extends React.Component {
if (typeof files !== 'string' && files.length) {
this.uploadFiles(files);
} else {
- this.props.onUploadError('Invalid file upload', -1);
+ this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text'));
}
}
@@ -266,6 +266,7 @@ FileUpload.propTypes = {
getFileCount: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func,
+ onTextDrop: React.PropTypes.func,
channelId: React.PropTypes.string,
postType: React.PropTypes.string
};
diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx
index 650a72516..395b98630 100644
--- a/web/react/components/invite_member_modal.jsx
+++ b/web/react/components/invite_member_modal.jsx
@@ -21,7 +21,7 @@ export default class InviteMemberModal extends React.Component {
emailErrors: {},
firstNameErrors: {},
lastNameErrors: {},
- emailEnabled: !global.window.config.ByPassEmail
+ emailEnabled: global.window.config.SendEmailNotifications === 'true'
};
}
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index ffc07a4dd..8cc4f1483 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -5,7 +5,6 @@ const Utils = require('../utils/utils.jsx');
const Client = require('../utils/client.jsx');
const UserStore = require('../stores/user_store.jsx');
const BrowserStore = require('../stores/browser_store.jsx');
-const Constants = require('../utils/constants.jsx');
export default class Login extends React.Component {
constructor(props) {
@@ -95,10 +94,8 @@ export default class Login extends React.Component {
focusEmail = true;
}
- const authServices = JSON.parse(this.props.authServices);
-
let loginMessage = [];
- if (authServices.indexOf(Constants.GITLAB_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -116,7 +113,7 @@ export default class Login extends React.Component {
}
let emailSignup;
- if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className={'form-group' + errorClass}>
@@ -205,11 +202,9 @@ export default class Login extends React.Component {
Login.defaultProps = {
teamName: '',
- teamDisplayName: '',
- authServices: ''
+ teamDisplayName: ''
};
Login.propTypes = {
teamName: React.PropTypes.string,
- teamDisplayName: React.PropTypes.string,
- authServices: React.PropTypes.string
+ teamDisplayName: React.PropTypes.string
};
diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx
index f27b09ecc..b7bce9b34 100644
--- a/web/react/components/more_direct_channels.jsx
+++ b/web/react/components/more_direct_channels.jsx
@@ -114,7 +114,7 @@ export default class MoreDirectChannels extends React.Component {
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Close</span>
</button>
- <h4 className='modal-title'>More Private Messages</h4>
+ <h4 className='modal-title'>More Direct Messages</h4>
</div>
<div className='modal-body'>
<ul className='nav nav-pills nav-stacked'>
diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx
index da9874b0b..bdb50cd9e 100644
--- a/web/react/components/navbar.jsx
+++ b/web/react/components/navbar.jsx
@@ -262,7 +262,7 @@ export default class Navbar extends React.Component {
return (
<div className='navbar-brand'>
<a
- href='/'
+ href={TeamStore.getCurrentTeamUrl() + '/channels/town-square'}
className='heading'
>
{channelTitle}
diff --git a/web/react/components/new_channel_modal.jsx b/web/react/components/new_channel_modal.jsx
index c43137744..c8ef59b4a 100644
--- a/web/react/components/new_channel_modal.jsx
+++ b/web/react/components/new_channel_modal.jsx
@@ -93,6 +93,7 @@ export default class NewChannelModal extends React.Component {
<span>
<Modal
show={this.props.show}
+ bsSize='large'
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
@@ -122,7 +123,7 @@ export default class NewChannelModal extends React.Component {
/>
{displayNameError}
<p className='input__help dark'>
- {'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
+ {'URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
<a
href='#'
onClick={this.props.onChangeURLPressed}
diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx
index 1e6cc3607..37d4a58cb 100644
--- a/web/react/components/password_reset_send_link.jsx
+++ b/web/react/components/password_reset_send_link.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
+const Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
export default class PasswordResetSendLink extends React.Component {
@@ -15,8 +16,8 @@ export default class PasswordResetSendLink extends React.Component {
e.preventDefault();
var state = {};
- var email = React.findDOMNode(this.refs.email).value.trim();
- if (!email) {
+ var email = React.findDOMNode(this.refs.email).value.trim().toLowerCase();
+ if (!email || !Utils.isEmail(email)) {
state.error = 'Please enter a valid email address.';
this.setState(state);
return;
@@ -67,7 +68,7 @@ export default class PasswordResetSendLink extends React.Component {
<p>{'To reset your password, enter the email address you used to sign up for ' + this.props.teamDisplayName + '.'}</p>
<div className={formClass}>
<input
- type='text'
+ type='email'
className='form-control'
name='email'
ref='email'
diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx
index ec873dd00..a2ca8b00f 100644
--- a/web/react/components/popover_list_members.jsx
+++ b/web/react/components/popover_list_members.jsx
@@ -65,7 +65,7 @@ export default class PopoverListMembers extends React.Component {
>
{count}
<span
- className='glyphicon glyphicon-user'
+ className='fa fa-user'
aria-hidden='true'
/>
</div>
diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx
index d3c6befd0..9127f00de 100644
--- a/web/react/components/post.jsx
+++ b/web/react/components/post.jsx
@@ -51,7 +51,7 @@ export default class Post extends React.Component {
var post = this.props.post;
client.createPost(post, post.channel_id,
- function success(data) {
+ (data) => {
AsyncClient.getPosts();
var channel = ChannelStore.get(post.channel_id);
@@ -65,11 +65,11 @@ export default class Post extends React.Component {
post: data
});
},
- function error() {
+ () => {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
this.forceUpdate();
- }.bind(this)
+ }
);
post.state = Constants.POST_LOADING;
@@ -81,31 +81,52 @@ export default class Post extends React.Component {
return true;
}
- return false;
- }
- render() {
- var post = this.props.post;
- var parentPost = this.props.parentPost;
- var posts = this.props.posts;
+ if (nextProps.sameRoot !== this.props.sameRoot) {
+ return true;
+ }
- var type = 'Post';
- if (post.root_id && post.root_id.length > 0) {
- type = 'Comment';
+ if (nextProps.sameUser !== this.props.sameUser) {
+ return true;
+ }
+
+ if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
+ return true;
}
- var commentCount = 0;
- var commentRootId;
+ return false;
+ }
+ getCommentCount(props) {
+ const post = props.post;
+ const parentPost = props.parentPost;
+ const posts = props.posts;
+
+ let commentCount = 0;
+ let commentRootId;
if (parentPost) {
commentRootId = post.root_id;
} else {
commentRootId = post.id;
}
- for (var postId in posts) {
+ for (let postId in posts) {
if (posts[postId].root_id === commentRootId) {
commentCount += 1;
}
}
+ return commentCount;
+ }
+ render() {
+ var post = this.props.post;
+ var parentPost = this.props.parentPost;
+ var posts = this.props.posts;
+
+ var type = 'Post';
+ if (post.root_id && post.root_id.length > 0) {
+ type = 'Comment';
+ }
+
+ const commentCount = this.getCommentCount(this.props);
+
var rootUser;
if (this.props.sameRoot) {
rootUser = 'same--root';
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index e0682e997..6e98e4aba 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -35,7 +35,6 @@ export default class PostBody extends React.Component {
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
- global.window.emojify.run(React.findDOMNode(this.refs.message_span));
}
componentDidMount() {
@@ -142,7 +141,6 @@ export default class PostBody extends React.Component {
fileAttachmentHolder = (
<FileAttachmentList
filenames={filenames}
- modalId={`view_image_modal_${post.id}`}
channelId={post.channel_id}
userId={post.user_id}
/>
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index d2a0a4035..824e7ef39 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -5,6 +5,8 @@ var UserStore = require('../stores/user_store.jsx');
var utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
+var Tooltip = ReactBootstrap.Tooltip;
+var OverlayTrigger = ReactBootstrap.OverlayTrigger;
export default class PostInfo extends React.Component {
constructor(props) {
@@ -148,15 +150,19 @@ export default class PostInfo extends React.Component {
var dropdown = this.createDropdown();
+ let tooltip = <Tooltip>{utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}</Tooltip>;
+
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
+ <OverlayTrigger
+ placement='top'
+ overlay={tooltip}
>
- {utils.displayDateTime(post.create_at)}
- </time>
+ <time className='post-profile-time'>
+ {utils.displayDateTime(post.create_at)}
+ </time>
+ </OverlayTrigger>
</li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx
index 703e548fb..3e1e075bb 100644
--- a/web/react/components/post_list.jsx
+++ b/web/react/components/post_list.jsx
@@ -105,18 +105,18 @@ export default class PostList extends React.Component {
UserStore.addStatusesChangeListener(this.onTimeChange);
SocketStore.addChangeListener(this.onSocketChange);
- var postHolder = $(React.findDOMNode(this.refs.postlist));
+ const postHolder = $(React.findDOMNode(this.refs.postlist));
- $(window).on('resize.' + this.props.channelId, function resize() {
+ $(window).resize(() => {
this.resize();
if (!this.scrolled) {
this.scrollToBottom();
}
- }.bind(this));
+ });
- postHolder.on('scroll', function scroll() {
- var position = postHolder.scrollTop() + postHolder.height() + 14;
- var bottom = postHolder[0].scrollHeight;
+ postHolder.on('scroll', () => {
+ const position = postHolder.scrollTop() + postHolder.height() + 14;
+ const bottom = postHolder[0].scrollHeight;
if (position >= bottom) {
this.scrolled = false;
@@ -128,7 +128,7 @@ export default class PostList extends React.Component {
this.userHasSeenNew = true;
}
this.isUserScroll = true;
- }.bind(this));
+ });
$('.post-list__content div .post').removeClass('post--last');
$('.post-list__content div:last-child .post').addClass('post--last');
@@ -146,7 +146,7 @@ export default class PostList extends React.Component {
UserStore.removeStatusesChangeListener(this.onTimeChange);
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
- $(window).off('resize.' + this.props.channelId);
+ $(window).off('resize');
var postHolder = $(React.findDOMNode(this.refs.postlist));
postHolder.off('scroll');
}
@@ -326,8 +326,8 @@ export default class PostList extends React.Component {
<strong><UserProfile userId={teammate.id} /></strong>
</div>
<p className='channel-intro-text'>
- {'This is the start of your private message history with ' + teammateName + '.'}<br/>
- {'Private messages and files shared here are not shown to people outside this area.'}
+ {'This is the start of your direct message history with ' + teammateName + '.'}<br/>
+ {'Direct messages and files shared here are not shown to people outside this area.'}
</p>
<a
className='intro-links'
@@ -346,7 +346,7 @@ export default class PostList extends React.Component {
return (
<div className='channel-intro'>
- <p className='channel-intro-text'>{'This is the start of your private message history with this teammate. Private messages and files shared here are not shown to people outside this area.'}</p>
+ <p className='channel-intro-text'>{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}</p>
</div>
);
}
diff --git a/web/react/components/post_list_container.jsx b/web/react/components/post_list_container.jsx
index 0815ac883..e59d85d41 100644
--- a/web/react/components/post_list_container.jsx
+++ b/web/react/components/post_list_container.jsx
@@ -49,6 +49,7 @@ export default class PostListContainer extends React.Component {
for (let i = 0; i <= this.state.postLists.length - 1; i++) {
postListCtls.push(
<PostList
+ key={'postlistkey' + i}
channelId={postLists[i]}
isActive={postLists[i] === channelId}
/>
diff --git a/web/react/components/register_app_modal.jsx b/web/react/components/register_app_modal.jsx
index 3dd5c094e..473ff3f91 100644
--- a/web/react/components/register_app_modal.jsx
+++ b/web/react/components/register_app_modal.jsx
@@ -228,7 +228,7 @@ export default class RegisterAppModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>{'x'}</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index fe31ac381..5b4694eb1 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -56,7 +56,6 @@ export default class RhsComment extends React.Component {
}
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
- global.window.emojify.run(React.findDOMNode(this.refs.message_holder));
}
componentDidMount() {
this.parseEmojis();
@@ -114,14 +113,7 @@ export default class RhsComment extends React.Component {
var ownerOptions;
if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) {
ownerOptions = (
- <div
- className='dropdown'
- onClick={
- function scroll() {
- $('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);
- }
- }
- >
+ <div className='dropdown'>
<a
href='#'
className='dropdown-toggle theme'
@@ -171,7 +163,6 @@ export default class RhsComment extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_comment_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -194,10 +185,7 @@ export default class RhsComment extends React.Component {
<strong><UserProfile userId={post.user_id} /></strong>
</li>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
- >
+ <time className='post-profile-time'>
{Utils.displayCommentDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/rhs_header_post.jsx b/web/react/components/rhs_header_post.jsx
index 5156ec4d7..f55c4095e 100644
--- a/web/react/components/rhs_header_post.jsx
+++ b/web/react/components/rhs_header_post.jsx
@@ -65,6 +65,7 @@ export default class RhsHeaderPost extends React.Component {
aria-label='Close'
onClick={this.handleClose}
>
+ <i className='fa fa-sign-out'/>
</button>
</div>
);
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index 2ea697c5b..13ab0c982 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -20,7 +20,6 @@ export default class RhsRootPost extends React.Component {
}
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
- global.window.emojify.run(React.findDOMNode(this.refs.message_holder));
}
componentDidMount() {
this.parseEmojis();
@@ -54,7 +53,7 @@ export default class RhsRootPost extends React.Component {
var channelName;
if (channel) {
if (channel.type === 'D') {
- channelName = 'Private Message';
+ channelName = 'Direct Message';
} else {
channelName = channel.display_name;
}
@@ -112,7 +111,6 @@ export default class RhsRootPost extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -134,10 +132,7 @@ export default class RhsRootPost extends React.Component {
<ul className='post-header'>
<li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
- >
+ <time className='post-profile-time'>
{utils.displayCommentDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/search_results_header.jsx b/web/react/components/search_results_header.jsx
index 694f0c55d..4e8a3ef10 100644
--- a/web/react/components/search_results_header.jsx
+++ b/web/react/components/search_results_header.jsx
@@ -50,6 +50,7 @@ export default class SearchResultsHeader extends React.Component {
title='Close'
onClick={this.handleClose}
>
+ <i className='fa fa-sign-out'/>
</button>
</div>
);
diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx
index 0e951f5c6..32b521560 100644
--- a/web/react/components/search_results_item.jsx
+++ b/web/react/components/search_results_item.jsx
@@ -64,7 +64,7 @@ export default class SearchResultsItem extends React.Component {
if (channel) {
channelName = channel.display_name;
if (channel.type === 'D') {
- channelName = 'Private Message';
+ channelName = 'Direct Message';
}
}
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index e5cbd6e92..4c4675788 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -7,7 +7,8 @@ export default class SettingsSidebar extends React.Component {
this.handleClick = this.handleClick.bind(this);
}
- handleClick(tab) {
+ handleClick(tab, e) {
+ e.preventDefault();
this.props.updateTab(tab.name);
$('.settings-modal').addClass('display--content');
}
diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx
index 87007edcc..14664ed4d 100644
--- a/web/react/components/sidebar.jsx
+++ b/web/react/components/sidebar.jsx
@@ -566,7 +566,7 @@ export default class Sidebar extends React.Component {
{privateChannelItems}
</ul>
<ul className='nav nav-pills nav-stacked'>
- <li><h4>Private Messages</h4></li>
+ <li><h4>Direct Messages</h4></li>
{directMessageItems}
{directMessageMore}
</ul>
diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx
index 2671d560b..f1341d9d7 100644
--- a/web/react/components/sidebar_right_menu.jsx
+++ b/web/react/components/sidebar_right_menu.jsx
@@ -6,6 +6,10 @@ var client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx');
export default class SidebarRightMenu extends React.Component {
+ componentDidMount() {
+ $('.sidebar--left .dropdown-menu').perfectScrollbar();
+ }
+
constructor(props) {
super(props);
diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx
index bf08e6508..4112138fa 100644
--- a/web/react/components/signup_team.jsx
+++ b/web/react/components/signup_team.jsx
@@ -12,38 +12,42 @@ export default class TeamSignUp extends React.Component {
this.updatePage = this.updatePage.bind(this);
- if (props.services.length === 1) {
- if (props.services[0] === Constants.EMAIL_SERVICE) {
- this.state = {page: 'email', service: ''};
- } else {
- this.state = {page: 'service', service: props.services[0]};
- }
- } else {
- this.state = {page: 'choose', service: ''};
+ var count = 0;
+
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
+ count = count + 1;
+ }
+
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ count = count + 1;
+ }
+
+ if (count > 1) {
+ this.state = {page: 'choose'};
+ } else if (global.window.config.EnableSignUpWithEmail === 'true') {
+ this.state = {page: 'email'};
+ } else if (global.window.config.EnableSignUpWithGitLab === 'true') {
+ this.state = {page: 'gitlab'};
}
}
- updatePage(page, service) {
- this.setState({page: page, service: service});
+
+ updatePage(page) {
+ this.setState({page});
}
+
render() {
+ if (this.state.page === 'choose') {
+ return (
+ <ChoosePage
+ updatePage={this.updatePage}
+ />
+ );
+ }
+
if (this.state.page === 'email') {
return <EmailSignUpPage />;
- } else if (this.state.page === 'service' && this.state.service !== '') {
- return <SSOSignupPage service={this.state.service} />;
+ } else if (this.state.page === 'gitlab') {
+ return <SSOSignupPage service={Constants.GITLAB_SERVICE} />;
}
-
- return (
- <ChoosePage
- services={this.props.services}
- updatePage={this.updatePage}
- />
- );
}
}
-
-TeamSignUp.defaultProps = {
- services: []
-};
-TeamSignUp.propTypes = {
- services: React.PropTypes.array
-};
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 19c3b2d22..495159efc 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -1,11 +1,10 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var utils = require('../utils/utils.jsx');
+var Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
-var Constants = require('../utils/constants.jsx');
export default class SignupUserComplete extends React.Component {
constructor(props) {
@@ -31,13 +30,26 @@ export default class SignupUserComplete extends React.Component {
handleSubmit(e) {
e.preventDefault();
+ const providedEmail = React.findDOMNode(this.refs.email).value.trim();
+ if (!providedEmail) {
+ this.setState({nameError: '', emailError: 'This field is required', passwordError: ''});
+ return;
+ }
+
+ if (!Utils.isEmail(providedEmail)) {
+ this.setState({nameError: '', emailError: 'Please enter a valid email address', passwordError: ''});
+ return;
+ }
+
+ this.state.user.email = providedEmail;
+
this.state.user.username = React.findDOMNode(this.refs.name).value.trim().toLowerCase();
if (!this.state.user.username) {
this.setState({nameError: 'This field is required', emailError: '', passwordError: '', serverError: ''});
return;
}
- var usernameError = utils.isValidUsername(this.state.user.username);
+ var usernameError = Utils.isValidUsername(this.state.user.username);
if (usernameError === 'Cannot use a reserved word as a username.') {
this.setState({nameError: 'This username is reserved, please choose a new one.', emailError: '', passwordError: '', serverError: ''});
return;
@@ -51,12 +63,6 @@ export default class SignupUserComplete extends React.Component {
return;
}
- this.state.user.email = React.findDOMNode(this.refs.email).value.trim();
- if (!this.state.user.email) {
- this.setState({nameError: '', emailError: 'This field is required', passwordError: ''});
- return;
- }
-
this.state.user.password = React.findDOMNode(this.refs.password).value.trim();
if (!this.state.user.password || this.state.user.password .length < 5) {
this.setState({nameError: '', emailError: '', passwordError: 'Please enter at least 5 characters', serverError: ''});
@@ -78,7 +84,7 @@ export default class SignupUserComplete extends React.Component {
if (this.props.hash > 0) {
BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'}));
}
- window.location.href = '/';
+ window.location.href = '/' + this.props.teamName + '/channels/town-square';
}.bind(this),
function emailLoginFailure(err) {
if (err.message === 'Login failed because email address has not been verified') {
@@ -161,11 +167,8 @@ export default class SignupUserComplete extends React.Component {
</div>
);
- // add options to log in using another service
- var authServices = JSON.parse(this.props.authServices);
-
var signupMessage = [];
- if (authServices.indexOf(Constants.GITLAB_SERVICE) >= 0) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
<a
className='btn btn-custom-login gitlab'
@@ -178,7 +181,7 @@ export default class SignupUserComplete extends React.Component {
}
var emailSignup;
- if (authServices.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
<div className='inner__content'>
@@ -262,7 +265,6 @@ SignupUserComplete.defaultProps = {
teamId: '',
email: '',
data: null,
- authServices: '',
teamDisplayName: ''
};
SignupUserComplete.propTypes = {
@@ -271,6 +273,5 @@ SignupUserComplete.propTypes = {
teamId: React.PropTypes.string,
email: React.PropTypes.string,
data: React.PropTypes.string,
- authServices: React.PropTypes.string,
teamDisplayName: React.PropTypes.string
};
diff --git a/web/react/components/team_feature_tab.jsx b/web/react/components/team_feature_tab.jsx
deleted file mode 100644
index 3251746b8..000000000
--- a/web/react/components/team_feature_tab.jsx
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-
-export default class FeatureTab extends React.Component {
- constructor(props) {
- super(props);
-
- this.submitValetFeature = this.submitValetFeature.bind(this);
- this.handleValetRadio = this.handleValetRadio.bind(this);
- this.onUpdateSection = this.onUpdateSection.bind(this);
- this.setupInitialState = this.setupInitialState.bind(this);
-
- this.state = this.setupInitialState();
- }
- componentWillReceiveProps(newProps) {
- var team = newProps.team;
-
- var allowValet = 'false';
- if (team && team.allow_valet) {
- allowValet = 'true';
- }
-
- this.setState({allowValet: allowValet});
- }
- submitValetFeature() {
- var data = {};
- data.allow_valet = this.state.allowValet;
-
- Client.updateValetFeature(data,
- function success() {
- this.props.updateSection('');
- AsyncClient.getMyTeam();
- }.bind(this),
- function fail(err) {
- var state = this.setupInitialState();
- state.serverError = err;
- this.setState(state);
- }.bind(this)
- );
- }
- handleValetRadio(val) {
- this.setState({allowValet: val});
- React.findDOMNode(this.refs.wrapper).focus();
- }
- onUpdateSection(e) {
- e.preventDefault();
- if (this.props.activeSection === 'valet') {
- this.props.updateSection('');
- } else {
- this.props.updateSection('valet');
- }
- }
- setupInitialState() {
- var allowValet;
- var team = this.props.team;
-
- if (team && team.allow_valet) {
- allowValet = 'true';
- } else {
- allowValet = 'false';
- }
-
- return {allowValet: allowValet};
- }
- render() {
- var clientError = null;
- var serverError = null;
- if (this.state.clientError) {
- clientError = this.state.clientError;
- }
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
-
- var valetSection;
-
- if (this.props.activeSection === 'valet') {
- var valetActive = [false, false];
- if (this.state.allowValet === 'false') {
- valetActive[1] = true;
- } else {
- valetActive[0] = true;
- }
-
- let inputs = [];
-
- inputs.push(
- <div key='teamValetSetting'>
- <div className='radio'>
- <label>
- <input
- type='radio'
- checked={valetActive[0]}
- onChange={this.handleValetRadio.bind(this, 'true')}
- >
- On
- </input>
- </label>
- <br/>
- </div>
- <div className='radio'>
- <label>
- <input
- type='radio'
- checked={valetActive[1]}
- onChange={this.handleValetRadio.bind(this, 'false')}
- >
- Off
- </input>
- </label>
- <br/>
- </div>
- <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div>
- </div>
- );
-
- valetSection = (
- <SettingItemMax
- title='Valet (Preview - EXPERTS ONLY)'
- inputs={inputs}
- submit={this.submitValetFeature}
- server_error={serverError}
- client_error={clientError}
- updateSection={this.onUpdateSection}
- />
- );
- } else {
- var describe = '';
- if (this.state.allowValet === 'false') {
- describe = 'Off';
- } else {
- describe = 'On';
- }
-
- valetSection = (
- <SettingItemMin
- title='Valet (Preview - EXPERTS ONLY)'
- describe={describe}
- updateSection={this.onUpdateSection}
- />
- );
- }
-
- return (
- <div>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- <i className='modal-back'></i>Advanced Features
- </h4>
- </div>
- <div
- ref='wrapper'
- className='user-settings'
- >
- <h3 className='tab-header'>Advanced Features</h3>
- <div className='divider-dark first'/>
- {valetSection}
- <div className='divider-dark'/>
- </div>
- </div>
- );
- }
-}
-
-FeatureTab.defaultProps = {
- team: {},
- activeSection: ''
-};
-FeatureTab.propTypes = {
- updateSection: React.PropTypes.func.isRequired,
- team: React.PropTypes.object.isRequired,
- activeSection: React.PropTypes.string.isRequired
-};
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 8315430e4..79f03510f 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component {
var uploadHelpText = (
<div>
<p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
- <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import.'}</p>
+ <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
</div>
);
diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx
index 396521af9..e91aa20bc 100644
--- a/web/react/components/team_settings.jsx
+++ b/web/react/components/team_settings.jsx
@@ -4,7 +4,6 @@
var TeamStore = require('../stores/team_store.jsx');
var ImportTab = require('./team_import_tab.jsx');
var ExportTab = require('./team_export_tab.jsx');
-var FeatureTab = require('./team_feature_tab.jsx');
var GeneralTab = require('./team_general_tab.jsx');
var Utils = require('../utils/utils.jsx');
@@ -25,7 +24,7 @@ export default class TeamSettings extends React.Component {
onChange() {
var team = TeamStore.getCurrent();
if (!Utils.areStatesEqual(this.state.team, team)) {
- this.setState({team: team});
+ this.setState({team});
}
}
render() {
@@ -43,17 +42,6 @@ export default class TeamSettings extends React.Component {
</div>
);
break;
- case 'feature':
- result = (
- <div>
- <FeatureTab
- team={this.state.team}
- activeSection={this.props.activeSection}
- updateSection={this.props.updateSection}
- />
- </div>
- );
- break;
case 'import':
result = (
<div>
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 0513c811f..a96aadccf 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -20,8 +20,8 @@ export default class TeamSettingsModal extends React.Component {
$('body').on('click', '.modal-back', function handleBackClick() {
$(this).closest('.modal-dialog').removeClass('display--content');
});
- $('body').on('click', '.modal-header .close', function handleCloseClick() {
- setTimeout(function removeContent() {
+ $('body').on('click', '.modal-header .close', () => {
+ setTimeout(() => {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
@@ -33,11 +33,12 @@ export default class TeamSettingsModal extends React.Component {
this.setState({activeSection: section});
}
render() {
- let tabs = [];
+ const tabs = [];
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
- tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
- tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
+
+ // To enable export uncomment this line
+ //tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
return (
<div
@@ -63,7 +64,7 @@ export default class TeamSettingsModal extends React.Component {
className='modal-title'
ref='title'
>
- Team Settings
+ {'Team Settings'}
</h4>
</div>
<div className='modal-body'>
diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx
index d3107c5c7..b8264b887 100644
--- a/web/react/components/team_signup_choose_auth.jsx
+++ b/web/react/components/team_signup_choose_auth.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Constants = require('../utils/constants.jsx');
-
export default class ChooseAuthPage extends React.Component {
constructor(props) {
super(props);
@@ -10,7 +8,7 @@ export default class ChooseAuthPage extends React.Component {
}
render() {
var buttons = [];
- if (this.props.services.indexOf(Constants.GITLAB_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithGitLab === 'true') {
buttons.push(
<a
className='btn btn-custom-login gitlab btn-full'
@@ -18,17 +16,17 @@ export default class ChooseAuthPage extends React.Component {
onClick={
function clickGit(e) {
e.preventDefault();
- this.props.updatePage('service', Constants.GITLAB_SERVICE);
+ this.props.updatePage('gitlab');
}.bind(this)
}
>
<span className='icon' />
- <span>Create new team with GitLab Account</span>
+ <span>{'Create new team with GitLab Account'}</span>
</a>
);
}
- if (this.props.services.indexOf(Constants.EMAIL_SERVICE) !== -1) {
+ if (global.window.config.EnableSignUpWithEmail === 'true') {
buttons.push(
<a
className='btn btn-custom-login email btn-full'
@@ -36,18 +34,18 @@ export default class ChooseAuthPage extends React.Component {
onClick={
function clickEmail(e) {
e.preventDefault();
- this.props.updatePage('email', '');
+ this.props.updatePage('email');
}.bind(this)
}
>
<span className='fa fa-envelope' />
- <span>Create new team with email address</span>
+ <span>{'Create new team with email address'}</span>
</a>
);
}
if (buttons.length === 0) {
- buttons = <span>No sign-up methods configured, please contact your system administrator.</span>;
+ buttons = <span>{'No sign-up methods configured, please contact your system administrator.'}</span>;
}
return (
@@ -61,10 +59,6 @@ export default class ChooseAuthPage extends React.Component {
}
}
-ChooseAuthPage.defaultProps = {
- services: []
-};
ChooseAuthPage.propTypes = {
- services: React.PropTypes.array,
updatePage: React.PropTypes.func
};
diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx
index b26d9f6ce..105e4817a 100644
--- a/web/react/components/team_signup_password_page.jsx
+++ b/web/react/components/team_signup_password_page.jsx
@@ -53,7 +53,7 @@ export default class TeamSignupPasswordPage extends React.Component {
props.state.wizard = 'finished';
props.updateParent(props.state, true);
- window.location.href = '/';
+ window.location.href = '/' + teamSignup.team.name + '/channels/town-square';
}.bind(this),
function loginFail(err) {
if (err.message === 'Login failed because email address has not been verified') {
diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx
index 41ac98303..524bd5b50 100644
--- a/web/react/components/team_signup_send_invites_page.jsx
+++ b/web/react/components/team_signup_send_invites_page.jsx
@@ -13,7 +13,7 @@ export default class TeamSignupSendInvitesPage extends React.Component {
this.submitSkip = this.submitSkip.bind(this);
this.keySubmit = this.keySubmit.bind(this);
this.state = {
- emailEnabled: !global.window.config.ByPassEmail
+ emailEnabled: global.window.config.SendEmailNotifications === 'true'
};
if (!this.state.emailEnabled) {
diff --git a/web/react/components/team_signup_with_sso.jsx b/web/react/components/team_signup_with_sso.jsx
index 2849b4cbb..a4972dd8d 100644
--- a/web/react/components/team_signup_with_sso.jsx
+++ b/web/react/components/team_signup_with_sso.jsx
@@ -42,7 +42,7 @@ export default class SSOSignUpPage extends React.Component {
if (data.follow_link) {
window.location.href = data.follow_link;
} else {
- window.location.href = '/';
+ window.location.href = '/' + team.name + '/channels/town-square';
}
},
function fail(err) {
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index ea8126bec..5f5316013 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -5,7 +5,6 @@ const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const PostStore = require('../stores/post_store.jsx');
const CommandList = require('./command_list.jsx');
const ErrorStore = require('../stores/error_store.jsx');
-const AsyncClient = require('../utils/async_client.jsx');
const Utils = require('../utils/utils.jsx');
const Constants = require('../utils/constants.jsx');
@@ -18,7 +17,6 @@ export default class Textbox extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.onRecievedError = this.onRecievedError.bind(this);
- this.onTimerInterrupt = this.onTimerInterrupt.bind(this);
this.updateMentionTab = this.updateMentionTab.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
@@ -35,8 +33,7 @@ export default class Textbox extends React.Component {
this.state = {
mentionText: '-1',
mentions: [],
- connection: '',
- timerInterrupt: null
+ connection: ''
};
this.caret = -1;
@@ -44,6 +41,7 @@ export default class Textbox extends React.Component {
this.doProcessMentions = false;
this.mentions = [];
}
+
getStateFromStores() {
const error = ErrorStore.getLastError();
@@ -53,6 +51,7 @@ export default class Textbox extends React.Component {
return {message: null};
}
+
componentDidMount() {
PostStore.addAddMentionListener(this.onListenerChange);
ErrorStore.addChangeListener(this.onRecievedError);
@@ -60,46 +59,28 @@ export default class Textbox extends React.Component {
this.resize();
this.updateMentionTab(null);
}
+
componentWillUnmount() {
PostStore.removeAddMentionListener(this.onListenerChange);
ErrorStore.removeChangeListener(this.onRecievedError);
}
+
onListenerChange(id, username) {
if (id === this.props.id) {
this.addMention(username);
}
}
- onRecievedError() {
- const errorState = this.getStateFromStores();
- if (this.state.timerInterrupt !== null) {
- window.clearInterval(this.state.timerInterrupt);
- this.setState({timerInterrupt: null});
- }
+ onRecievedError() {
+ const errorState = ErrorStore.getLastError();
- if (errorState.message === 'There appears to be a problem with your internet connection') {
+ if (errorState && errorState.connErrorCount > 0) {
this.setState({connection: 'bad-connection'});
- const timerInterrupt = window.setInterval(this.onTimerInterrupt, 5000);
- this.setState({timerInterrupt: timerInterrupt});
} else {
this.setState({connection: ''});
}
}
- onTimerInterrupt() {
- // Since these should only happen when you have no connection and slightly briefly after any
- // performance hit should not matter
- if (this.state.connection === 'bad-connection') {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECIEVED_ERROR,
- err: null
- });
-
- AsyncClient.updateLastViewedAt();
- }
- window.clearInterval(this.state.timerInterrupt);
- this.setState({timerInterrupt: null});
- }
componentDidUpdate() {
if (this.caret >= 0) {
Utils.setCaretPosition(React.findDOMNode(this.refs.message), this.caret);
@@ -111,6 +92,7 @@ export default class Textbox extends React.Component {
}
this.resize();
}
+
componentWillReceiveProps(nextProps) {
if (!this.addedMention) {
this.checkForNewMention(nextProps.messageText);
@@ -122,19 +104,22 @@ export default class Textbox extends React.Component {
this.addedMention = false;
this.refs.commands.getSuggestedCommands(nextProps.messageText);
}
+
updateMentionTab(mentionText) {
// using setTimeout so dispatch isn't called during an in progress dispatch
- setTimeout(function updateMentionTabAfterTimeout() {
+ setTimeout(() => {
AppDispatcher.handleViewAction({
type: ActionTypes.RECIEVED_MENTION_DATA,
id: this.props.id,
mention_text: mentionText
});
- }.bind(this), 1);
+ }, 1);
}
+
handleChange() {
this.props.onUserInput(React.findDOMNode(this.refs.message).value);
}
+
handleKeyPress(e) {
const text = React.findDOMNode(this.refs.message).value;
@@ -157,6 +142,7 @@ export default class Textbox extends React.Component {
this.props.onKeyPress(e);
}
+
handleKeyDown(e) {
if (Utils.getSelectedText(React.findDOMNode(this.refs.message)) !== '') {
this.doProcessMentions = true;
@@ -166,6 +152,7 @@ export default class Textbox extends React.Component {
this.handleBackspace(e);
}
}
+
handleBackspace() {
const text = React.findDOMNode(this.refs.message).value;
if (text.indexOf('/') === 0) {
@@ -185,6 +172,7 @@ export default class Textbox extends React.Component {
this.doProcessMentions = true;
}
}
+
checkForNewMention(text) {
const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
@@ -211,6 +199,7 @@ export default class Textbox extends React.Component {
const name = preText.substring(atIndex + 1, preText.length).toLowerCase();
this.updateMentionTab(name);
}
+
addMention(name) {
const caret = Utils.getCaretPosition(React.findDOMNode(this.refs.message));
@@ -233,11 +222,13 @@ export default class Textbox extends React.Component {
this.props.onUserInput(`${prefix}@${name} ${suffix}`);
}
+
addCommand(cmd) {
const elm = React.findDOMNode(this.refs.message);
elm.value = cmd;
this.handleChange();
}
+
resize() {
const e = React.findDOMNode(this.refs.message);
const w = React.findDOMNode(this.refs.wrapper);
@@ -264,21 +255,25 @@ export default class Textbox extends React.Component {
this.props.onHeightChange();
}
}
+
handleFocus() {
const elm = React.findDOMNode(this.refs.message);
if (elm.title === elm.value) {
elm.value = '';
}
}
+
handleBlur() {
const elm = React.findDOMNode(this.refs.message);
if (elm.value === '') {
elm.value = elm.title;
}
}
+
handlePaste() {
this.doProcessMentions = true;
}
+
render() {
return (
<div
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 7cfac69e7..c5d028d31 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -57,7 +57,7 @@ export default class UserProfile extends React.Component {
}
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
- if (!global.window.config.ShowEmailAddress) {
+ if (!global.window.config.ShowEmailAddress === 'true') {
dataContent += '<div class="text-nowrap">Email not shared</div>';
} else {
dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>';
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
new file mode 100644
index 000000000..c680d75d1
--- /dev/null
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -0,0 +1,111 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Constants = require('../../utils/constants.jsx');
+
+export default class CustomThemeChooser extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onPickerChange = this.onPickerChange.bind(this);
+ this.onInputChange = this.onInputChange.bind(this);
+ this.pasteBoxChange = this.pasteBoxChange.bind(this);
+
+ this.state = {};
+ }
+ componentDidMount() {
+ $('.color-picker').colorpicker().on('changeColor', this.onPickerChange);
+ }
+ onPickerChange(e) {
+ const theme = this.props.theme;
+ theme[e.target.id] = e.color.toHex();
+ theme.type = 'custom';
+ this.props.updateTheme(theme);
+ }
+ onInputChange(e) {
+ const theme = this.props.theme;
+ theme[e.target.parentNode.id] = e.target.value;
+ theme.type = 'custom';
+ this.props.updateTheme(theme);
+ }
+ pasteBoxChange(e) {
+ const text = e.target.value;
+
+ if (text.length === 0) {
+ return;
+ }
+
+ const colors = text.split(',');
+
+ const theme = {type: 'custom'};
+ let index = 0;
+ Constants.THEME_ELEMENTS.forEach((element) => {
+ if (index < colors.length) {
+ theme[element.id] = colors[index];
+ }
+ index++;
+ });
+
+ this.props.updateTheme(theme);
+ }
+ render() {
+ const theme = this.props.theme;
+
+ const elements = [];
+ let colors = '';
+ Constants.THEME_ELEMENTS.forEach((element, index) => {
+ elements.push(
+ <div
+ className='col-sm-4 form-group'
+ key={'custom-theme-key' + index}
+ >
+ <label className='custom-label'>{element.uiName}</label>
+ <div
+ className='input-group color-picker'
+ id={element.id}
+ >
+ <input
+ className='form-control'
+ type='text'
+ defaultValue={theme[element.id]}
+ onChange={this.onInputChange}
+ />
+ <span className='input-group-addon'><i></i></span>
+ </div>
+ </div>
+ );
+
+ colors += theme[element.id] + ',';
+ });
+
+ const pasteBox = (
+ <div className='col-sm-12'>
+ <label className='custom-label'>
+ {'Copy and paste to share theme colors:'}
+ </label>
+ <input
+ type='text'
+ className='form-control'
+ value={colors}
+ onChange={this.pasteBoxChange}
+ />
+ </div>
+ );
+
+ return (
+ <div>
+ <div className='row form-group'>
+ {elements}
+ </div>
+ <div className='row'>
+ {pasteBox}
+ </div>
+ </div>
+ );
+ }
+}
+
+CustomThemeChooser.propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ updateTheme: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings/import_theme_modal.jsx b/web/react/components/user_settings/import_theme_modal.jsx
new file mode 100644
index 000000000..48be83afe
--- /dev/null
+++ b/web/react/components/user_settings/import_theme_modal.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+const UserStore = require('../../stores/user_store.jsx');
+const Utils = require('../../utils/utils.jsx');
+const Client = require('../../utils/client.jsx');
+const Modal = ReactBootstrap.Modal;
+
+const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
+const Constants = require('../../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+
+export default class ImportThemeModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateShow = this.updateShow.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+
+ this.state = {
+ inputError: '',
+ show: false
+ };
+ }
+ componentDidMount() {
+ UserStore.addImportModalListener(this.updateShow);
+ }
+ componentWillUnmount() {
+ UserStore.removeImportModalListener(this.updateShow);
+ }
+ updateShow(show) {
+ this.setState({show});
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+
+ const text = React.findDOMNode(this.refs.input).value;
+
+ if (!this.isInputValid(text)) {
+ this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
+ return;
+ }
+
+ const colors = text.split(',');
+ const theme = {type: 'custom'};
+
+ theme.sidebarBg = colors[0];
+ theme.sidebarText = colors[5];
+ theme.sidebarUnreadText = colors[5];
+ theme.sidebarTextHoverBg = colors[4];
+ theme.sidebarTextHoverColor = colors[5];
+ theme.sidebarTextActiveBg = colors[2];
+ theme.sidebarTextActiveColor = colors[3];
+ theme.sidebarHeaderBg = colors[1];
+ theme.sidebarHeaderTextColor = colors[5];
+ theme.onlineIndicator = colors[6];
+ theme.mentionBj = colors[7];
+ theme.mentionColor = '#ffffff';
+ theme.centerChannelBg = '#ffffff';
+ theme.centerChannelColor = '#333333';
+ theme.linkColor = '#2389d7';
+ theme.buttonBg = '#26a970';
+ theme.buttonColor = '#ffffff';
+
+ let user = UserStore.getCurrentUser();
+ user.theme_props = theme;
+
+ Client.updateUser(user,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ME,
+ me: data
+ });
+
+ this.setState({show: false});
+ Utils.applyTheme(theme);
+ $('#user_settings').modal('show');
+ },
+ (err) => {
+ var state = this.getStateFromStores();
+ state.serverError = err;
+ this.setState(state);
+ }
+ );
+ }
+ isInputValid(text) {
+ if (text.length === 0) {
+ return false;
+ }
+
+ if (text.indexOf(' ') !== -1) {
+ return false;
+ }
+
+ if (text.length > 0 && text.indexOf(',') === -1) {
+ return false;
+ }
+
+ if (text.length > 0) {
+ const colors = text.split(',');
+
+ if (colors.length !== 8) {
+ return false;
+ }
+
+ for (let i = 0; i < colors.length; i++) {
+ if (colors[i].length !== 7 && colors[i].length !== 4) {
+ return false;
+ }
+
+ if (colors[i].charAt(0) !== '#') {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ handleChange(e) {
+ if (this.isInputValid(e.target.value)) {
+ this.setState({inputError: null});
+ } else {
+ this.setState({inputError: 'Invalid format, please try copying and pasting in again.'});
+ }
+ }
+ render() {
+ return (
+ <span>
+ <Modal
+ show={this.state.show}
+ onHide={() => this.setState({show: false})}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Import Slack Theme'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <p>
+ {'To import a theme, go to a Slack team and look for “”Preferences” -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:'}
+ </p>
+ <div className='form-group less'>
+ <div className='col-sm-9'>
+ <input
+ ref='input'
+ type='text'
+ className='form-control'
+ onChange={this.handleChange}
+ />
+ {this.state.inputError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={() => this.setState({show: false})}
+ >
+ {'Cancel'}
+ </button>
+ <button
+ onClick={this.handleSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='3'
+ >
+ {'Submit'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ </span>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
new file mode 100644
index 000000000..1bbfbd162
--- /dev/null
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -0,0 +1,174 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+var Constants = require('../../utils/constants.jsx');
+var ChannelStore = require('../../stores/channel_store.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+
+export default class ManageIncomingHooks extends React.Component {
+ constructor() {
+ super();
+
+ this.getHooks = this.getHooks.bind(this);
+ this.addNewHook = this.addNewHook.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+
+ this.state = {hooks: [], channelId: ChannelStore.getByName(Constants.DEFAULT_CHANNEL).id, getHooksComplete: false};
+ }
+ componentDidMount() {
+ this.getHooks();
+ }
+ addNewHook() {
+ let hook = {}; //eslint-disable-line prefer-const
+ hook.channel_id = this.state.channelId;
+
+ Client.addIncomingHook(
+ hook,
+ (data) => {
+ let hooks = this.state.hooks;
+ if (!hooks) {
+ hooks = [];
+ }
+ hooks.push(data);
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ removeHook(id) {
+ let data = {}; //eslint-disable-line prefer-const
+ data.id = id;
+
+ Client.deleteIncomingHook(
+ data,
+ () => {
+ let hooks = this.state.hooks; //eslint-disable-line prefer-const
+ let index = -1;
+ for (let i = 0; i < hooks.length; i++) {
+ if (hooks[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ hooks.splice(index, 1);
+ }
+
+ this.setState({hooks});
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ getHooks() {
+ Client.listIncomingHooks(
+ (data) => {
+ let state = this.state; //eslint-disable-line prefer-const
+
+ if (data) {
+ state.hooks = data;
+ }
+
+ state.getHooksComplete = true;
+ this.setState(state);
+ },
+ (err) => {
+ this.setState({serverError: err});
+ }
+ );
+ }
+ updateChannelId(e) {
+ this.setState({channelId: e.target.value});
+ }
+ render() {
+ let serverError;
+ if (this.state.serverError) {
+ serverError = <label className='has-error'>{this.state.serverError}</label>;
+ }
+
+ const channels = ChannelStore.getAll();
+ let options = []; //eslint-disable-line prefer-const
+ channels.forEach((channel) => {
+ options.push(<option value={channel.id}>{channel.name}</option>);
+ });
+
+ let disableButton = '';
+ if (this.state.channelId === '') {
+ disableButton = ' disable';
+ }
+
+ let hooks = []; //eslint-disable-line prefer-const
+ this.state.hooks.forEach((hook) => {
+ const c = ChannelStore.get(hook.channel_id);
+ hooks.push(
+ <div className='font--small'>
+ <div className='padding-top x2 divider-light'></div>
+ <div className='padding-top x2'>
+ <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
+ </div>
+ <div className='padding-top'>
+ <strong>{'Channel: '}</strong>{c.name}
+ </div>
+ <div className='padding-top'>
+ <a
+ className={'text-danger'}
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ {'Remove'}
+ </a>
+ </div>
+ </div>
+ );
+ });
+
+ let displayHooks;
+ if (!this.state.getHooksComplete) {
+ displayHooks = <LoadingScreen/>;
+ } else if (hooks.length > 0) {
+ displayHooks = hooks;
+ } else {
+ displayHooks = <label>{': None'}</label>;
+ }
+
+ const existingHooks = (
+ <div className='padding-top x2'>
+ <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
+ {displayHooks}
+ </div>
+ );
+
+ return (
+ <div key='addIncomingHook'>
+ <label className='control-label'>{'Add a new incoming webhook'}</label>
+ <div className='padding-top'>
+ <select
+ ref='channelName'
+ className='form-control'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ >
+ {options}
+ </select>
+ {serverError}
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary' + disableButton}
+ href='#'
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingHooks}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
new file mode 100644
index 000000000..f8f916bd0
--- /dev/null
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -0,0 +1,58 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Utils = require('../../utils/utils.jsx');
+var Constants = require('../../utils/constants.jsx');
+
+export default class PremadeThemeChooser extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+ render() {
+ const theme = this.props.theme;
+
+ const premadeThemes = [];
+ for (const k in Constants.THEMES) {
+ if (Constants.THEMES.hasOwnProperty(k)) {
+ const premadeTheme = $.extend(true, {}, Constants.THEMES[k]);
+
+ let activeClass = '';
+ if (premadeTheme.type === theme.type) {
+ activeClass = 'active';
+ }
+
+ premadeThemes.push(
+ <div
+ className='col-sm-3 premade-themes'
+ key={'premade-theme-key' + k}
+ >
+ <div
+ className={activeClass}
+ onClick={() => this.props.updateTheme(premadeTheme)}
+ >
+ <label>
+ <img
+ className='img-responsive'
+ src={'/static/images/themes/' + premadeTheme.type.toLowerCase() + '.png'}
+ />
+ <div className='theme-label'>{Utils.toTitleCase(premadeTheme.type)}</div>
+ </label>
+ </div>
+ </div>
+ );
+ }
+ }
+
+ return (
+ <div className='row'>
+ {premadeThemes}
+ </div>
+ );
+ }
+}
+
+PremadeThemeChooser.propTypes = {
+ theme: React.PropTypes.object.isRequired,
+ updateTheme: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx
index 48b499068..0eab333c4 100644
--- a/web/react/components/user_settings.jsx
+++ b/web/react/components/user_settings/user_settings.jsx
@@ -1,13 +1,14 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var utils = require('../utils/utils.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+var utils = require('../../utils/utils.jsx');
var NotificationsTab = require('./user_settings_notifications.jsx');
var SecurityTab = require('./user_settings_security.jsx');
var GeneralTab = require('./user_settings_general.jsx');
var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
+var IntegrationsTab = require('./user_settings_integrations.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -86,6 +87,17 @@ export default class UserSettings extends React.Component {
/>
</div>
);
+ } else if (this.props.activeTab === 'integrations') {
+ return (
+ <div>
+ <IntegrationsTab
+ user={this.state.user}
+ activeSection={this.props.activeSection}
+ updateSection={this.props.updateSection}
+ updateTab={this.props.updateTab}
+ />
+ </div>
+ );
}
return <div/>;
diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx
new file mode 100644
index 000000000..4372069e7
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_appearance.jsx
@@ -0,0 +1,236 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var UserStore = require('../../stores/user_store.jsx');
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+const CustomThemeChooser = require('./custom_theme_chooser.jsx');
+const PremadeThemeChooser = require('./premade_theme_chooser.jsx');
+const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx');
+const Constants = require('../../utils/constants.jsx');
+const ActionTypes = Constants.ActionTypes;
+
+export default class UserSettingsAppearance extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ this.submitTheme = this.submitTheme.bind(this);
+ this.updateTheme = this.updateTheme.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.handleImportModal = this.handleImportModal.bind(this);
+
+ this.state = this.getStateFromStores();
+
+ this.originalTheme = this.state.theme;
+ }
+ componentDidMount() {
+ UserStore.addChangeListener(this.onChange);
+
+ if (this.props.activeSection === 'theme') {
+ $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
+ }
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+ componentDidUpdate() {
+ if (this.props.activeSection === 'theme') {
+ $('.color-btn').removeClass('active-border');
+ $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
+ }
+ }
+ componentWillUnmount() {
+ UserStore.removeChangeListener(this.onChange);
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+ getStateFromStores() {
+ const user = UserStore.getCurrentUser();
+ let theme = null;
+
+ if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
+ theme = user.theme_props;
+ } else {
+ theme = $.extend(true, {}, Constants.THEMES.default);
+ }
+
+ let type = 'premade';
+ if (theme.type === 'custom') {
+ type = 'custom';
+ }
+
+ return {theme, type};
+ }
+ onChange() {
+ const newState = this.getStateFromStores();
+
+ if (!Utils.areStatesEqual(this.state, newState)) {
+ this.setState(newState);
+ }
+ }
+ submitTheme(e) {
+ e.preventDefault();
+ var user = UserStore.getCurrentUser();
+ user.theme_props = this.state.theme;
+
+ Client.updateUser(user,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ME,
+ me: data
+ });
+
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ this.props.updateTab('general');
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
+ $('#user_settings').modal('hide');
+ },
+ (err) => {
+ var state = this.getStateFromStores();
+ state.serverError = err;
+ this.setState(state);
+ }
+ );
+ }
+ updateTheme(theme) {
+ this.setState({theme});
+ Utils.applyTheme(theme);
+ }
+ updateType(type) {
+ this.setState({type});
+ }
+ handleClose() {
+ const state = this.getStateFromStores();
+ state.serverError = null;
+
+ Utils.applyTheme(state.theme);
+
+ this.setState(state);
+
+ $('.ps-container.modal-body').scrollTop(0);
+ $('.ps-container.modal-body').perfectScrollbar('update');
+ $('#user_settings').modal('hide');
+ }
+ handleImportModal() {
+ $('#user_settings').modal('hide');
+ AppDispatcher.handleViewAction({
+ type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL,
+ value: true
+ });
+ }
+ render() {
+ var serverError;
+ if (this.state.serverError) {
+ serverError = this.state.serverError;
+ }
+
+ const displayCustom = this.state.type === 'custom';
+
+ let custom;
+ let premade;
+ if (displayCustom) {
+ custom = (
+ <CustomThemeChooser
+ theme={this.state.theme}
+ updateTheme={this.updateTheme}
+ />
+ );
+ } else {
+ premade = (
+ <PremadeThemeChooser
+ theme={this.state.theme}
+ updateTheme={this.updateTheme}
+ />
+ );
+ }
+
+ const themeUI = (
+ <div className='section-max appearance-section'>
+ <div className='col-sm-12'>
+ <div className='radio'>
+ <label>
+ <input type='radio'
+ checked={!displayCustom}
+ onChange={this.updateType.bind(this, 'premade')}
+ >
+ {'Theme Colors'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ {premade}
+ <div className='radio'>
+ <label>
+ <input type='radio'
+ checked={displayCustom}
+ onChange={this.updateType.bind(this, 'custom')}
+ >
+ {'Custom Theme'}
+ </input>
+ </label>
+ <br/>
+ </div>
+ {custom}
+ <hr />
+ {serverError}
+ <a
+ className='btn btn-sm btn-primary'
+ href='#'
+ onClick={this.submitTheme}
+ >
+ {'Submit'}
+ </a>
+ <a
+ className='btn btn-sm theme'
+ href='#'
+ onClick={this.handleClose}
+ >
+ {'Cancel'}
+ </a>
+ </div>
+ </div>
+ );
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>{'Appearance Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Appearance Settings'}</h3>
+ <div className='divider-dark first'/>
+ {themeUI}
+ <div className='divider-dark'/>
+ </div>
+ <br/>
+ <a
+ className='theme'
+ onClick={this.handleImportModal}
+ >
+ {'Import from Slack'}
+ </a>
+ </div>
+ );
+ }
+}
+
+UserSettingsAppearance.defaultProps = {
+ activeSection: ''
+};
+UserSettingsAppearance.propTypes = {
+ activeSection: React.PropTypes.string,
+ updateTab: React.PropTypes.func
+};
diff --git a/web/react/components/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx
index 1b04149dc..d9fb43902 100644
--- a/web/react/components/user_settings_developer.jsx
+++ b/web/react/components/user_settings/user_settings_developer.jsx
@@ -1,8 +1,8 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
export default class DeveloperTab extends React.Component {
constructor(props) {
@@ -64,7 +64,7 @@ export default class DeveloperTab extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>{'x'}</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
diff --git a/web/react/components/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx
index 66cde6ca2..c1d4c4ab5 100644
--- a/web/react/components/user_settings_general.jsx
+++ b/web/react/components/user_settings/user_settings_general.jsx
@@ -1,13 +1,13 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var SettingPicture = require('./setting_picture.jsx');
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var utils = require('../utils/utils.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var SettingPicture = require('../setting_picture.jsx');
+var client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var utils = require('../../utils/utils.jsx');
var assign = require('object-assign');
export default class UserSettingsGeneralTab extends React.Component {
@@ -208,7 +208,7 @@ export default class UserSettingsGeneralTab extends React.Component {
}
setupInitialState(props) {
var user = props.user;
- var emailEnabled = !global.window.config.ByPassEmail;
+ var emailEnabled = global.window.config.SendEmailNotifications === 'true';
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled};
}
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
new file mode 100644
index 000000000..cb45c5178
--- /dev/null
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -0,0 +1,95 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var ManageIncomingHooks = require('./manage_incoming_hooks.jsx');
+
+export default class UserSettingsIntegrationsTab extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.updateSection = this.updateSection.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+
+ this.state = {};
+ }
+ updateSection(section) {
+ this.props.updateSection(section);
+ }
+ handleClose() {
+ this.updateSection('');
+ }
+ componentDidMount() {
+ $('#user_settings').on('hidden.bs.modal', this.handleClose);
+ }
+ componentWillUnmount() {
+ $('#user_settings').off('hidden.bs.modal', this.handleClose);
+ }
+ render() {
+ let incomingHooksSection;
+ var inputs = [];
+
+ if (this.props.activeSection === 'incoming-hooks') {
+ inputs.push(
+ <ManageIncomingHooks />
+ );
+
+ incomingHooksSection = (
+ <SettingItemMax
+ title='Incoming Webhooks'
+ inputs={inputs}
+ updateSection={function clearSection(e) {
+ this.updateSection('');
+ e.preventDefault();
+ }.bind(this)}
+ />
+ );
+ } else {
+ incomingHooksSection = (
+ <SettingItemMin
+ title='Incoming Webhooks'
+ describe='Manage your incoming webhooks'
+ updateSection={function updateNameSection() {
+ this.updateSection('incoming-hooks');
+ }.bind(this)}
+ />
+ );
+ }
+
+ return (
+ <div>
+ <div className='modal-header'>
+ <button
+ type='button'
+ className='close'
+ data-dismiss='modal'
+ aria-label='Close'
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </button>
+ <h4
+ className='modal-title'
+ ref='title'
+ >
+ <i className='modal-back'></i>
+ {'Integration Settings'}
+ </h4>
+ </div>
+ <div className='user-settings'>
+ <h3 className='tab-header'>{'Integration Settings'}</h3>
+ <div className='divider-dark first'/>
+ {incomingHooksSection}
+ <div className='divider-dark'/>
+ </div>
+ </div>
+ );
+ }
+}
+
+UserSettingsIntegrationsTab.propTypes = {
+ user: React.PropTypes.object,
+ updateSection: React.PropTypes.func,
+ updateTab: React.PropTypes.func,
+ activeSection: React.PropTypes.string
+};
diff --git a/web/react/components/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx
index 67a4d0041..5113d2429 100644
--- a/web/react/components/user_settings_modal.jsx
+++ b/web/react/components/user_settings/user_settings_modal.jsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var SettingsSidebar = require('./settings_sidebar.jsx');
+var SettingsSidebar = require('../settings_sidebar.jsx');
var UserSettings = require('./user_settings.jsx');
export default class UserSettingsModal extends React.Component {
@@ -38,6 +38,9 @@ export default class UserSettingsModal extends React.Component {
if (global.window.config.EnableOAuthServiceProvider === 'true') {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
+ if (global.window.config.EnableIncomingWebhooks === 'true') {
+ tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'});
+ }
return (
<div
@@ -57,7 +60,7 @@ export default class UserSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
- <span aria-hidden='true'>{'x'}</span>
+ <span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
diff --git a/web/react/components/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index dadbb669b..ba14f019f 100644
--- a/web/react/components/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -1,12 +1,12 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var UserStore = require('../stores/user_store.jsx');
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var utils = require('../utils/utils.jsx');
+var UserStore = require('../../stores/user_store.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var utils = require('../../utils/utils.jsx');
var assign = require('object-assign');
function getNotificationsStateFromStores() {
@@ -241,7 +241,7 @@ export default class NotificationsTab extends React.Component {
checked={notifyActive[1]}
onChange={this.handleNotifyRadio.bind(this, 'mention')}
>
- Only for mentions and private messages
+ Only for mentions and direct messages
</input>
</label>
<br/>
@@ -265,9 +265,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>;
+
desktopSection = (
<SettingItemMax
title='Send desktop notifications'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -277,7 +280,7 @@ export default class NotificationsTab extends React.Component {
} else {
let describe = '';
if (this.state.notifyLevel === 'mention') {
- describe = 'Only for mentions and private messages';
+ describe = 'Only for mentions and direct messages';
} else if (this.state.notifyLevel === 'none') {
describe = 'Never';
} else {
@@ -343,9 +346,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>;
+
soundSection = (
<SettingItemMax
title='Desktop notification sounds'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -414,7 +420,7 @@ export default class NotificationsTab extends React.Component {
</label>
<br/>
</div>
- <div><br/>{'Email notifications are sent for mentions and private messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
+ <div><br/>{'Email notifications are sent for mentions and direct messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}</div>
</div>
);
diff --git a/web/react/components/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx
index c10d790ae..b59c08af0 100644
--- a/web/react/components/user_settings_security.jsx
+++ b/web/react/components/user_settings/user_settings_security.jsx
@@ -1,11 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var Client = require('../utils/client.jsx');
-var AsyncClient = require('../utils/async_client.jsx');
-var Constants = require('../utils/constants.jsx');
+var SettingItemMin = require('../setting_item_min.jsx');
+var SettingItemMax = require('../setting_item_max.jsx');
+var Client = require('../../utils/client.jsx');
+var AsyncClient = require('../../utils/async_client.jsx');
+var Constants = require('../../utils/constants.jsx');
export default class SecurityTab extends React.Component {
constructor(props) {
diff --git a/web/react/components/user_settings_appearance.jsx b/web/react/components/user_settings_appearance.jsx
deleted file mode 100644
index 3df013d03..000000000
--- a/web/react/components/user_settings_appearance.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-var UserStore = require('../stores/user_store.jsx');
-var SettingItemMin = require('./setting_item_min.jsx');
-var SettingItemMax = require('./setting_item_max.jsx');
-var Client = require('../utils/client.jsx');
-var Utils = require('../utils/utils.jsx');
-
-var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000'];
-
-export default class UserSettingsAppearance extends React.Component {
- constructor(props) {
- super(props);
-
- this.submitTheme = this.submitTheme.bind(this);
- this.updateTheme = this.updateTheme.bind(this);
- this.handleClose = this.handleClose.bind(this);
-
- this.state = this.getStateFromStores();
- }
- getStateFromStores() {
- var user = UserStore.getCurrentUser();
- var theme = '#2389d7';
- if (ThemeColors != null) {
- theme = ThemeColors[0];
- }
- if (user.props && user.props.theme) {
- theme = user.props.theme;
- }
-
- return {theme: theme.toLowerCase()};
- }
- submitTheme(e) {
- e.preventDefault();
- var user = UserStore.getCurrentUser();
- if (!user.props) {
- user.props = {};
- }
- user.props.theme = this.state.theme;
-
- Client.updateUser(user,
- function success() {
- this.props.updateSection('');
- window.location.reload();
- }.bind(this),
- function fail(err) {
- var state = this.getStateFromStores();
- state.serverError = err;
- this.setState(state);
- }.bind(this)
- );
- }
- updateTheme(e) {
- var hex = Utils.rgb2hex(e.target.style.backgroundColor);
- this.setState({theme: hex.toLowerCase()});
- }
- handleClose() {
- this.setState({serverError: null});
- this.props.updateTab('general');
- }
- componentDidMount() {
- if (this.props.activeSection === 'theme') {
- $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
- }
- $('#user_settings').on('hidden.bs.modal', this.handleClose);
- }
- componentDidUpdate() {
- if (this.props.activeSection === 'theme') {
- $('.color-btn').removeClass('active-border');
- $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
- }
- }
- componentWillUnmount() {
- $('#user_settings').off('hidden.bs.modal', this.handleClose);
- this.props.updateSection('');
- }
- render() {
- var serverError;
- if (this.state.serverError) {
- serverError = this.state.serverError;
- }
-
- var themeSection;
- var self = this;
-
- if (ThemeColors != null) {
- if (this.props.activeSection === 'theme') {
- var themeButtons = [];
-
- for (var i = 0; i < ThemeColors.length; i++) {
- themeButtons.push(
- <button
- key={ThemeColors[i] + 'key' + i}
- ref={ThemeColors[i]}
- type='button'
- className='btn btn-lg color-btn'
- style={{backgroundColor: ThemeColors[i]}}
- onClick={this.updateTheme}
- />
- );
- }
-
- var inputs = [];
-
- inputs.push(
- <li
- key='themeColorSetting'
- className='setting-list-item'
- >
- <div
- className='btn-group'
- data-toggle='buttons-radio'
- >
- {themeButtons}
- </div>
- </li>
- );
-
- themeSection = (
- <SettingItemMax
- title='Theme Color'
- inputs={inputs}
- submit={this.submitTheme}
- serverError={serverError}
- updateSection={function updateSection(e) {
- self.props.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- themeSection = (
- <SettingItemMin
- title='Theme Color'
- describe={this.state.theme}
- updateSection={function updateSection() {
- self.props.updateSection('theme');
- }}
- />
- );
- }
- }
-
- return (
- <div>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- ref='title'
- >
- <i className='modal-back'></i>Appearance Settings
- </h4>
- </div>
- <div className='user-settings'>
- <h3 className='tab-header'>Appearance Settings</h3>
- <div className='divider-dark first'/>
- {themeSection}
- <div className='divider-dark'/>
- </div>
- </div>
- );
- }
-}
-
-UserSettingsAppearance.defaultProps = {
- activeSection: ''
-};
-UserSettingsAppearance.propTypes = {
- activeSection: React.PropTypes.string,
- updateSection: React.PropTypes.func,
- updateTab: React.PropTypes.func
-};
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index f7c980396..a7fecb689 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -1,8 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-var Client = require('../utils/client.jsx');
-var Utils = require('../utils/utils.jsx');
+const Client = require('../utils/client.jsx');
+const Utils = require('../utils/utils.jsx');
+const Constants = require('../utils/constants.jsx');
+const ViewImagePopoverBar = require('./view_image_popover_bar.jsx');
+const Modal = ReactBootstrap.Modal;
export default class ViewImageModal extends React.Component {
constructor(props) {
@@ -16,6 +19,10 @@ export default class ViewImageModal extends React.Component {
this.handleKeyPress = this.handleKeyPress.bind(this);
this.getPublicLink = this.getPublicLink.bind(this);
this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
+ this.onModalShown = this.onModalShown.bind(this);
+ this.onModalHidden = this.onModalHidden.bind(this);
+ this.onMouseEnterImage = this.onMouseEnterImage.bind(this);
+ this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this);
var loaded = [];
var progress = [];
@@ -23,9 +30,20 @@ export default class ViewImageModal extends React.Component {
loaded.push(false);
progress.push(0);
}
- this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}};
+ this.state = {
+ imgId: this.props.startId,
+ imgHeight: '100%',
+ loaded: loaded,
+ progress: progress,
+ images: {},
+ fileSizes: {},
+ showFooter: false
+ };
}
- handleNext() {
+ handleNext(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId + 1;
if (id > this.props.filenames.length - 1) {
id = 0;
@@ -33,7 +51,10 @@ export default class ViewImageModal extends React.Component {
this.setState({imgId: id});
this.loadImage(id);
}
- handlePrev() {
+ handlePrev(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId - 1;
if (id < 0) {
id = this.props.filenames.length - 1;
@@ -50,15 +71,27 @@ export default class ViewImageModal extends React.Component {
this.handlePrev();
}
}
- componentWillReceiveProps(nextProps) {
+ onModalShown(nextProps) {
this.setState({imgId: nextProps.startId});
+ this.loadImage(nextProps.startId);
+ }
+ onModalHidden() {
+ if (this.refs.video) {
+ var video = React.findDOMNode(this.refs.video);
+ video.pause();
+ video.currentTime = 0;
+ }
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.show === true && this.props.show === false) {
+ this.onModalShown(nextProps);
+ } else if (nextProps.show === false && this.props.show === true) {
+ this.onModalHidden();
+ }
}
loadImage(id) {
var imgHeight = $(window).height() - 100;
- if (this.state.loaded[id] || this.state.images[id]) {
- $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight);
- return;
- }
+ this.setState({imgHeight});
var filename = this.props.filenames[id];
@@ -68,84 +101,27 @@ export default class ViewImageModal extends React.Component {
if (fileType === 'image') {
var img = new Image();
img.load(this.getPreviewImagePath(filename),
- function load() {
- var progress = this.state.progress;
- progress[id] = img.completedPercentage;
- this.setState({progress: progress});
- }.bind(this));
- img.onload = (function onload(imgid) {
- return function onloadReturn() {
- var loaded = this.state.loaded;
- loaded[imgid] = true;
- this.setState({loaded: loaded});
- $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight);
- }.bind(this);
- }.bind(this)(id));
+ () => {
+ const progress = this.state.progress;
+ progress[id] = img.completedPercentage;
+ this.setState({progress});
+ });
+ img.onload = () => {
+ const loaded = this.state.loaded;
+ loaded[id] = true;
+ this.setState({loaded});
+ };
var images = this.state.images;
images[id] = img;
- this.setState({images: images});
+ this.setState({images});
} else {
// there's nothing to load for non-image files
var loaded = this.state.loaded;
loaded[id] = true;
- this.setState({loaded: loaded});
- }
- }
- componentDidUpdate() {
- if (this.state.loaded[this.state.imgId]) {
- if (this.refs.imageWrap) {
- $(React.findDOMNode(this.refs.imageWrap)).removeClass('default');
- }
+ this.setState({loaded});
}
}
componentDidMount() {
- $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() {
- this.setState({viewed: true});
- this.loadImage(this.state.imgId);
- }.bind(this));
-
- $('#' + this.props.modalId).on('hidden.bs.modal', function onModalHide() {
- if (this.refs.video) {
- var video = React.findDOMNode(this.refs.video);
- video.pause();
- video.currentTime = 0;
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
- if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
- $('.image_modal').modal('hide');
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.imageWrap)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
-
- if (this.refs.previewArrowLeft) {
- $(React.findDOMNode(this.refs.previewArrowLeft)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
- if (this.refs.previewArrowRight) {
- $(React.findDOMNode(this.refs.previewArrowRight)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
$(window).on('keyup', this.handleKeyPress);
// keep track of whether or not this component is mounted so we can safely set the state asynchronously
@@ -189,6 +165,12 @@ export default class ViewImageModal extends React.Component {
// only images have proper previews, so just use a placeholder icon for non-images
return Utils.getPreviewImagePathForFileType(fileType);
}
+ onMouseEnterImage() {
+ this.setState({showFooter: true});
+ }
+ onMouseLeaveImage() {
+ this.setState({showFooter: false});
+ }
render() {
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
return <div/>;
@@ -219,11 +201,20 @@ export default class ViewImageModal extends React.Component {
</a>
);
} else if (fileType === 'video' || fileType === 'audio') {
+ let width = Constants.WEB_VIDEO_WIDTH;
+ let height = Constants.WEB_VIDEO_HEIGHT;
+ if (Utils.isMobile()) {
+ width = Constants.MOBILE_VIDEO_WIDTH;
+ height = Constants.MOBILE_VIDEO_HEIGHT;
+ }
+
content = (
<video
ref='video'
data-setup='{}'
controls='controls'
+ width={width}
+ height={height}
>
<source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} />
</video>
@@ -299,23 +290,6 @@ export default class ViewImageModal extends React.Component {
bgClass = 'black-bg';
}
- var publicLink = '';
- if (global.window.config.AllowPublicLink) {
- publicLink = (
- <div>
- <a
- href='#'
- className='public-link text'
- data-title='Public Image'
- onClick={this.getPublicLink}
- >
- Get Public Link
- </a>
- <span className='text'> | </span>
- </div>
- );
- }
-
var leftArrow = '';
var rightArrow = '';
if (this.props.filenames.length > 1) {
@@ -342,65 +316,61 @@ export default class ViewImageModal extends React.Component {
);
}
+ let closeButtonClass = 'modal-close';
+ if (this.state.showFooter) {
+ closeButtonClass += ' modal-close--show';
+ }
+
return (
- <div
- className='modal fade image_modal'
- ref='modal'
- id={this.props.modalId}
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onModalDismissed}
+ className='image_modal'
+ dialogClassName='modal-image'
>
- <div className='modal-dialog modal-image'>
- <div className='modal-content image-content'>
+ <Modal.Body
+ modalClassName='image-body'
+ onClick={this.props.onModalDismissed}
+ >
+ <div
+ className={'image-wrapper ' + bgClass}
+ style={{maxHeight: this.state.imgHeight}}
+ onMouseEnter={this.onMouseEnterImage}
+ onMouseLeave={this.onMouseLeaveImage}
+ onClick={(e) => e.stopPropagation()}
+ >
<div
- ref='imageBody'
- className='modal-body image-body'
- >
- <div
- ref='imageWrap'
- className={'image-wrapper default ' + bgClass}
- >
- <div
- className='modal-close'
- data-dismiss='modal'
- />
- {content}
- <div
- ref='imageFooter'
- className='modal-button-bar'
- >
- <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span>
- <div className='image-links'>
- {publicLink}
- <a
- href={fileUrl}
- download={name}
- className='text'
- >
- Download
- </a>
- </div>
- </div>
- </div>
- {leftArrow}
- {rightArrow}
- </div>
+ className={closeButtonClass}
+ onClick={this.props.onModalDismissed}
+ />
+ {content}
+ <ViewImagePopoverBar
+ show={this.state.showFooter}
+ fileId={this.state.imgId}
+ totalFiles={this.props.filenames.length}
+ filename={name}
+ fileURL={fileUrl}
+ onGetPublicLinkPressed={this.getPublicLink}
+ />
</div>
- </div>
- </div>
+ {leftArrow}
+ {rightArrow}
+ </Modal.Body>
+ </Modal>
);
}
}
ViewImageModal.defaultProps = {
+ show: false,
filenames: [],
- modalId: '',
channelId: '',
userId: '',
startId: 0
};
ViewImageModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired,
filenames: React.PropTypes.array,
modalId: React.PropTypes.string,
channelId: React.PropTypes.string,
diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx
new file mode 100644
index 000000000..68817d751
--- /dev/null
+++ b/web/react/components/view_image_popover_bar.jsx
@@ -0,0 +1,66 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class ViewImagePopoverBar extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ var publicLink = '';
+ if (global.window.config.EnablePublicLink === 'true') {
+ publicLink = (
+ <div>
+ <a
+ href='#'
+ className='public-link text'
+ data-title='Public Image'
+ onClick={this.getPublicLink}
+ >
+ {'Get Public Link'}
+ </a>
+ <span className='text'>{' | '}</span>
+ </div>
+ );
+ }
+
+ var footerClass = 'modal-button-bar';
+ if (this.props.show) {
+ footerClass += ' footer--show';
+ }
+
+ return (
+ <div
+ ref='imageFooter'
+ className={footerClass}
+ >
+ <span className='pull-left text'>{'File ' + (this.props.fileId + 1) + ' of ' + this.props.totalFiles}</span>
+ <div className='image-links'>
+ {publicLink}
+ <a
+ href={this.props.fileURL}
+ download={this.props.filename}
+ className='text'
+ >
+ {'Download'}
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
+ViewImagePopoverBar.defaultProps = {
+ show: false,
+ imgId: 0,
+ totalFiles: 0,
+ filename: '',
+ fileURL: ''
+};
+
+ViewImagePopoverBar.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ fileId: React.PropTypes.number.isRequired,
+ totalFiles: React.PropTypes.number.isRequired,
+ filename: React.PropTypes.string.isRequired,
+ fileURL: React.PropTypes.string.isRequired,
+ onGetPublicLinkPressed: React.PropTypes.func.isRequired
+};