summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/about_build_modal.jsx133
-rw-r--r--webapp/components/admin_console/admin_navbar_dropdown.jsx2
-rw-r--r--webapp/components/admin_console/compliance_settings.jsx2
-rw-r--r--webapp/components/admin_console/service_settings.jsx58
-rw-r--r--webapp/components/admin_console/user_item.jsx2
-rw-r--r--webapp/components/analytics/team_analytics.jsx2
-rw-r--r--webapp/components/backstage/add_incoming_webhook.jsx198
-rw-r--r--webapp/components/backstage/add_integration.jsx76
-rw-r--r--webapp/components/backstage/add_integration_option.jsx39
-rw-r--r--webapp/components/backstage/add_outgoing_webhook.jsx270
-rw-r--r--webapp/components/backstage/backstage_category.jsx68
-rw-r--r--webapp/components/backstage/backstage_navbar.jsx61
-rw-r--r--webapp/components/backstage/backstage_section.jsx80
-rw-r--r--webapp/components/backstage/backstage_sidebar.jsx68
-rw-r--r--webapp/components/backstage/installed_incoming_webhook.jsx71
-rw-r--r--webapp/components/backstage/installed_integrations.jsx293
-rw-r--r--webapp/components/backstage/installed_outgoing_webhook.jsx91
-rw-r--r--webapp/components/center_panel.jsx145
-rw-r--r--webapp/components/channel_header.jsx17
-rw-r--r--webapp/components/channel_invite_button.jsx1
-rw-r--r--webapp/components/channel_notifications_modal.jsx2
-rw-r--r--webapp/components/channel_select.jsx79
-rw-r--r--webapp/components/channel_view.jsx58
-rw-r--r--webapp/components/claim/components/email_to_ldap.jsx14
-rw-r--r--webapp/components/error_page.jsx58
-rw-r--r--webapp/components/form_error.jsx50
-rw-r--r--webapp/components/invite_member_modal.jsx18
-rw-r--r--webapp/components/logged_in.jsx74
-rw-r--r--webapp/components/login/components/login_email.jsx (renamed from webapp/components/login_email.jsx)74
-rw-r--r--webapp/components/login/components/login_ldap.jsx (renamed from webapp/components/login_ldap.jsx)76
-rw-r--r--webapp/components/login/components/login_mfa.jsx92
-rw-r--r--webapp/components/login/components/login_username.jsx (renamed from webapp/components/login_username.jsx)91
-rw-r--r--webapp/components/login/login.jsx (renamed from webapp/components/login.jsx)371
-rw-r--r--webapp/components/more_channels.jsx4
-rw-r--r--webapp/components/more_direct_channels.jsx4
-rw-r--r--webapp/components/msg_typing.jsx12
-rw-r--r--webapp/components/navbar.jsx5
-rw-r--r--webapp/components/navbar_dropdown.jsx17
-rw-r--r--webapp/components/new_channel_flow.jsx7
-rw-r--r--webapp/components/permalink_view.jsx93
-rw-r--r--webapp/components/popover_list_members.jsx12
-rw-r--r--webapp/components/post.jsx14
-rw-r--r--webapp/components/post_body_additional_content.jsx2
-rw-r--r--webapp/components/post_info.jsx2
-rw-r--r--webapp/components/posts_view.jsx31
-rw-r--r--webapp/components/posts_view_container.jsx12
-rw-r--r--webapp/components/removed_from_channel_modal.jsx4
-rw-r--r--webapp/components/rename_channel_modal.jsx5
-rw-r--r--webapp/components/rhs_root_post.jsx2
-rw-r--r--webapp/components/root.jsx2
-rw-r--r--webapp/components/search_results_item.jsx22
-rw-r--r--webapp/components/sidebar.jsx102
-rw-r--r--webapp/components/sidebar_right.jsx4
-rw-r--r--webapp/components/signup_team.jsx55
-rw-r--r--webapp/components/signup_team_complete/components/signup_team_complete.jsx4
-rw-r--r--webapp/components/signup_user_complete.jsx89
-rw-r--r--webapp/components/spinner_button.jsx22
-rw-r--r--webapp/components/suggestion/search_suggestion_list.jsx1
-rw-r--r--webapp/components/team_general_tab.jsx1
-rw-r--r--webapp/components/team_settings_modal.jsx1
-rw-r--r--webapp/components/team_signup_with_sso.jsx2
-rw-r--r--webapp/components/textbox.jsx6
-rw-r--r--webapp/components/tutorial/tutorial_intro_screens.jsx22
-rw-r--r--webapp/components/tutorial/tutorial_tip.jsx9
-rw-r--r--webapp/components/tutorial/tutorial_view.jsx44
-rw-r--r--webapp/components/user_settings/manage_incoming_hooks.jsx225
-rw-r--r--webapp/components/user_settings/manage_outgoing_hooks.jsx397
-rw-r--r--webapp/components/user_settings/premade_theme_chooser.jsx13
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx4
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx419
-rw-r--r--webapp/components/user_settings/user_settings_integrations.jsx90
-rw-r--r--webapp/components/user_settings/user_settings_modal.jsx14
-rw-r--r--webapp/components/user_settings/user_settings_notifications.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx465
-rw-r--r--webapp/components/user_settings/user_settings_theme.jsx13
76 files changed, 3233 insertions, 1757 deletions
diff --git a/webapp/components/about_build_modal.jsx b/webapp/components/about_build_modal.jsx
index e73d842d0..4fd946401 100644
--- a/webapp/components/about_build_modal.jsx
+++ b/webapp/components/about_build_modal.jsx
@@ -6,6 +6,7 @@ import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import React from 'react';
+import Constants from 'utils/constants.jsx';
export default class AboutBuildModal extends React.Component {
constructor(props) {
@@ -20,6 +21,7 @@ export default class AboutBuildModal extends React.Component {
render() {
const config = global.window.mm_config;
const license = global.window.mm_license;
+ const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
let title = (
<FormattedMessage
@@ -28,6 +30,28 @@ export default class AboutBuildModal extends React.Component {
/>
);
+ let subTitle = (
+ <FormattedMessage
+ id='about.teamEditionSt'
+ defaultMessage='All your team communication in one place, instantly searchable and accessible anywhere.'
+ />
+ );
+
+ let learnMore = (
+ <div>
+ <FormattedMessage
+ id='about.teamEditionLearn'
+ defaultMessage='Join the Mattermost community at '
+ />
+ <a
+ target='_blank'
+ href='http://www.mattermost.org/'
+ >
+ {'mattermost.org'}
+ </a>
+ </div>
+ );
+
let licensee;
if (config.BuildEnterpriseReady === 'true') {
title = (
@@ -36,6 +60,29 @@ export default class AboutBuildModal extends React.Component {
defaultMessage='Enterprise Edition'
/>
);
+
+ subTitle = (
+ <FormattedMessage
+ id='about.enterpriseEditionSt'
+ defaultMessage='Modern enterprise communication from behind your firewall.'
+ />
+ );
+
+ learnMore = (
+ <div>
+ <FormattedMessage
+ id='about.enterpriseEditionLearn'
+ defaultMessage='Learn more about Enterprise Edition at '
+ />
+ <a
+ target='_blank'
+ href='http://about.mattermost.com/'
+ >
+ {'about.mattermost.com'}
+ </a>
+ </div>
+ );
+
if (license.IsLicensed === 'true') {
title = (
<FormattedMessage
@@ -44,14 +91,12 @@ export default class AboutBuildModal extends React.Component {
/>
);
licensee = (
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>
- <FormattedMessage
- id='about.licensed'
- defaultMessage='Licensed by:'
- />
- </div>
- <div className='col-sm-9'>{license.Company}</div>
+ <div className='form-group'>
+ <FormattedMessage
+ id='about.licensed'
+ defaultMessage='Licensed by:'
+ />
+ &nbsp;{license.Company}
</div>
);
}
@@ -59,6 +104,7 @@ export default class AboutBuildModal extends React.Component {
return (
<Modal
+ dialogClassName='about-modal'
show={this.props.show}
onHide={this.doHide}
>
@@ -71,57 +117,54 @@ export default class AboutBuildModal extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <h4>{'Mattermost'} {title}</h4>
- {licensee}
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>
- <FormattedMessage
- id='about.version'
- defaultMessage='Version:'
+ <div className='about-modal__content'>
+ <div className='about-modal__logo'>
+ <span
+ className='icon'
+ dangerouslySetInnerHTML={{__html: mattermostLogo}}
/>
</div>
- <div className='col-sm-9'>{config.Version}</div>
- </div>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>
- <FormattedMessage
- id='about.number'
- defaultMessage='Build Number:'
- />
+ <div>
+ <h3 className='about-modal__title'>{'Mattermost'} {title}</h3>
+ <p className='about-modal__subtitle padding-bottom'>{subTitle}</p>
+ <div className='form-group less'>
+ <div>
+ <FormattedMessage
+ id='about.version'
+ defaultMessage='Version:'
+ />
+ &nbsp;{config.Version}&nbsp;({config.BuildNumber})
+ </div>
+ </div>
+ {licensee}
</div>
- <div className='col-sm-9'>{config.BuildNumber}</div>
</div>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>
+ <div className='about-modal__footer'>
+ {learnMore}
+ <div className='form-group about-modal__copyright'>
<FormattedMessage
- id='about.date'
- defaultMessage='Build Date:'
+ id='about.copyright'
+ defaultMessage='Copyright 2016 Mattermost, Inc. All rights reserved'
/>
</div>
- <div className='col-sm-9'>{config.BuildDate}</div>
</div>
- <div className='row form-group'>
- <div className='col-sm-3 info__label'>
+ <div className='about-modal__hash form-group padding-top x2'>
+ <p>
<FormattedMessage
id='about.hash'
defaultMessage='Build Hash:'
/>
- </div>
- <div className='col-sm-9'>{config.BuildHash}</div>
+ &nbsp;{config.BuildHash}
+ </p>
+ <p>
+ <FormattedMessage
+ id='about.date'
+ defaultMessage='Build Date:'
+ />
+ &nbsp;{config.BuildDate}
+ </p>
</div>
</Modal.Body>
- <Modal.Footer>
- <button
- type='button'
- className='btn btn-default'
- onClick={this.doHide}
- >
- <FormattedMessage
- id='about.close'
- defaultMessage='Close'
- />
- </button>
- </Modal.Footer>
</Modal>
);
}
diff --git a/webapp/components/admin_console/admin_navbar_dropdown.jsx b/webapp/components/admin_console/admin_navbar_dropdown.jsx
index 527f97959..729d4b14d 100644
--- a/webapp/components/admin_console/admin_navbar_dropdown.jsx
+++ b/webapp/components/admin_console/admin_navbar_dropdown.jsx
@@ -64,7 +64,7 @@ export default class AdminNavbarDropdown extends React.Component {
>
<li>
<Link
- to={Utils.getWindowLocationOrigin() + '/' + this.state.currentTeam.name + '/channels/town-square'}
+ to={'/' + this.state.currentTeam.name + '/channels/town-square'}
>
<FormattedMessage
id='admin.nav.switch'
diff --git a/webapp/components/admin_console/compliance_settings.jsx b/webapp/components/admin_console/compliance_settings.jsx
index fb2ae26f9..206bb0faa 100644
--- a/webapp/components/admin_console/compliance_settings.jsx
+++ b/webapp/components/admin_console/compliance_settings.jsx
@@ -223,7 +223,7 @@ export default class ComplianceSettings extends React.Component {
</label>
<p className='help-text'>
<FormattedMessage
- id='admin.compliance.enableDesc'
+ id='admin.compliance.enableDailyDesc'
defaultMessage='When true, Mattermost will generate a daily compliance report.'
/>
</p>
diff --git a/webapp/components/admin_console/service_settings.jsx b/webapp/components/admin_console/service_settings.jsx
index 881d22d76..2c3f4081c 100644
--- a/webapp/components/admin_console/service_settings.jsx
+++ b/webapp/components/admin_console/service_settings.jsx
@@ -87,6 +87,10 @@ class ServiceSettings extends React.Component {
config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
+ if (this.refs.EnableMultifactorAuthentication) {
+ config.ServiceSettings.EnableMultifactorAuthentication = ReactDOM.findDOMNode(this.refs.EnableMultifactorAuthentication).checked;
+ }
+
//config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
var MaximumLoginAttempts = DefaultMaximumLoginAttempts;
@@ -173,6 +177,58 @@ class ServiceSettings extends React.Component {
saveClass = 'btn btn-primary';
}
+ let mfaSetting;
+ if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
+ mfaSetting = (
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableMultifactorAuthentication'
+ >
+ <FormattedMessage
+ id='admin.service.mfaTitle'
+ defaultMessage='Enable Multi-factor Authentication:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableMultifactorAuthentication'
+ value='true'
+ ref='EnableMultifactorAuthentication'
+ defaultChecked={this.props.config.ServiceSettings.EnableMultifactorAuthentication}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableMultifactorAuthentication'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableMultifactorAuthentication}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.mfaDesc'
+ defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
+ />
+ </p>
+ </div>
+ </div>
+ );
+ }
+
return (
<div className='wrapper--fixed'>
@@ -773,6 +829,8 @@ class ServiceSettings extends React.Component {
</div>
</div>
+ {mfaSetting}
+
<div className='form-group'>
<label
className='control-label col-sm-4'
diff --git a/webapp/components/admin_console/user_item.jsx b/webapp/components/admin_console/user_item.jsx
index 91f567d4d..c00050584 100644
--- a/webapp/components/admin_console/user_item.jsx
+++ b/webapp/components/admin_console/user_item.jsx
@@ -333,7 +333,7 @@ export default class UserItem extends React.Component {
<div>
<FormattedMessage
id='admin.user_item.confirmDemoteDescription'
- defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
+ defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
/>
<br/>
<br/>
diff --git a/webapp/components/analytics/team_analytics.jsx b/webapp/components/analytics/team_analytics.jsx
index efc965f24..9b4eb1f94 100644
--- a/webapp/components/analytics/team_analytics.jsx
+++ b/webapp/components/analytics/team_analytics.jsx
@@ -154,7 +154,7 @@ class TeamAnalytics extends React.Component {
<TableChart
title={
<FormattedMessage
- id='analytics.team.activeUsers'
+ id='analytics.team.recentUsers'
defaultMessage='Recent Active Users'
/>
}
diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx
new file mode 100644
index 000000000..83027c6b3
--- /dev/null
+++ b/webapp/components/backstage/add_incoming_webhook.jsx
@@ -0,0 +1,198 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import {browserHistory} from 'react-router';
+
+import ChannelSelect from 'components/channel_select.jsx';
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {Link} from 'react-router';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class AddIncomingWebhook extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+
+ this.state = {
+ name: '',
+ description: '',
+ channelId: '',
+ saving: false,
+ serverError: '',
+ clientError: null
+ };
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ if (!this.state.channelId) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_incoming_webhook.channelRequired'
+ defaultMessage='A valid channel is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ const hook = {
+ channel_id: this.state.channelId
+ };
+
+ AsyncClient.addIncomingHook(
+ hook,
+ () => {
+ browserHistory.push('/settings/integrations/installed');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateChannelId(e) {
+ this.setState({
+ channelId: e.target.value
+ });
+ }
+
+ render() {
+ return (
+ <div className='backstage-content row'>
+ <div className='add-incoming-webhook'>
+ <div className='backstage-header'>
+ <h1>
+ <FormattedMessage
+ id='add_incoming_webhook.header'
+ defaultMessage='Add Incoming Webhook'
+ />
+ </h1>
+ </div>
+ </div>
+ <div className='backstage-form'>
+ <form className='form-horizontal'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='name'
+ >
+ <FormattedMessage
+ id='add_incoming_webhook.name'
+ defaultMessage='Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='name'
+ type='text'
+ className='form-control'
+ value={this.state.name}
+ onChange={this.updateName}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='add_incoming_webhook.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='description'
+ type='text'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='channelId'
+ >
+ <FormattedMessage
+ id='add_incoming_webhook.channel'
+ defaultMessage='Channel'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <ChannelSelect
+ id='channelId'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ />
+ </div>
+ </div>
+ <div className='backstage-form__footer'>
+ <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <Link
+ className='btn btn-sm'
+ to={'/settings/integrations/add'}
+ >
+ <FormattedMessage
+ id='add_incoming_webhook.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='add_incoming_webhook.save'
+ defaultMessage='Save'
+ />
+ </SpinnerButton>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/add_integration.jsx
new file mode 100644
index 000000000..5f4a69bfe
--- /dev/null
+++ b/webapp/components/backstage/add_integration.jsx
@@ -0,0 +1,76 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {FormattedMessage} from 'react-intl';
+import AddIntegrationOption from './add_integration_option.jsx';
+
+import WebhookIcon from 'images/webhook_icon.jpg';
+
+export default class AddIntegration extends React.Component {
+ render() {
+ const options = [];
+
+ if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ options.push(
+ <AddIntegrationOption
+ key='incomingWebhook'
+ image={WebhookIcon}
+ title={
+ <FormattedMessage
+ id='add_integration.incomingWebhook.title'
+ defaultMessage='Incoming Webhook'
+ />
+ }
+ description={
+ <FormattedMessage
+ id='add_integration.incomingWebhook.description'
+ defaultMessage='Create webhook URLs for use in external integrations.'
+ />
+ }
+ link={'/settings/integrations/add/incoming_webhook'}
+ />
+ );
+ }
+
+ if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ options.push(
+ <AddIntegrationOption
+ key='outgoingWebhook'
+ image={WebhookIcon}
+ title={
+ <FormattedMessage
+ id='add_integration.outgoingWebhook.title'
+ defaultMessage='Outgoing Webhook'
+ />
+ }
+ description={
+ <FormattedMessage
+ id='add_integration.outgoingWebhook.description'
+ defaultMessage='Create webhooks to send new message events to an external integration.'
+ />
+ }
+ link={'/settings/integrations/add/outgoing_webhook'}
+ />
+ );
+ }
+
+ return (
+ <div className='backstage-content row'>
+ <div className='backstage-header'>
+ <h1>
+ <FormattedMessage
+ id='add_integration.header'
+ defaultMessage='Add Integration'
+ />
+ </h1>
+ </div>
+ <div>
+ {options}
+ </div>
+ </div>
+ );
+ }
+}
+
diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/add_integration_option.jsx
new file mode 100644
index 000000000..b17ebb185
--- /dev/null
+++ b/webapp/components/backstage/add_integration_option.jsx
@@ -0,0 +1,39 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class AddIntegrationOption extends React.Component {
+ static get propTypes() {
+ return {
+ image: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ description: React.PropTypes.node.isRequired,
+ link: React.PropTypes.string.isRequired
+ };
+ }
+
+ render() {
+ const {image, title, description, link} = this.props;
+
+ return (
+ <Link
+ to={link}
+ className='add-integration'
+ >
+ <img
+ className='add-integration__image'
+ src={image}
+ />
+ <div className='add-integration__title'>
+ {title}
+ </div>
+ <div className='add-integration__description'>
+ {description}
+ </div>
+ </Link>
+ );
+ }
+}
diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx
new file mode 100644
index 000000000..5d98138df
--- /dev/null
+++ b/webapp/components/backstage/add_outgoing_webhook.jsx
@@ -0,0 +1,270 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import {browserHistory} from 'react-router';
+
+import ChannelSelect from 'components/channel_select.jsx';
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {Link} from 'react-router';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+export default class AddOutgoingWebhook extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+ this.updateTriggerWords = this.updateTriggerWords.bind(this);
+ this.updateCallbackUrls = this.updateCallbackUrls.bind(this);
+
+ this.state = {
+ name: '',
+ description: '',
+ channelId: '',
+ triggerWords: '',
+ callbackUrls: '',
+ saving: false,
+ serverError: '',
+ clientError: null
+ };
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ if (!this.state.channelId && !this.state.triggerWords) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_outgoing_webhook.triggerWordsOrChannelRequired'
+ defaultMessage='A valid channel or a list of trigger words is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.callbackUrls) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_outgoing_webhook.callbackUrlsRequired'
+ defaultMessage='One or more callback URLs are required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ const hook = {
+ channel_id: this.state.channelId,
+ trigger_words: this.state.triggerWords.split('\n').map((word) => word.trim()),
+ callback_urls: this.state.callbackUrls.split('\n').map((url) => url.trim())
+ };
+
+ AsyncClient.addOutgoingHook(
+ hook,
+ () => {
+ browserHistory.push('/settings/integrations/installed');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateChannelId(e) {
+ this.setState({
+ channelId: e.target.value
+ });
+ }
+
+ updateTriggerWords(e) {
+ this.setState({
+ triggerWords: e.target.value
+ });
+ }
+
+ updateCallbackUrls(e) {
+ this.setState({
+ callbackUrls: e.target.value
+ });
+ }
+
+ render() {
+ return (
+ <div className='backstage-content row'>
+ <div className='add-outgoing-webhook'>
+ <div className='backstage-header'>
+ <h1>
+ <FormattedMessage
+ id='add_outgoing_webhook.header'
+ defaultMessage='Add Outgoing Webhook'
+ />
+ </h1>
+ </div>
+ </div>
+ <div className='backstage-form'>
+ <form className='form-horizontal'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='name'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.name'
+ defaultMessage='Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='name'
+ type='text'
+ className='form-control'
+ value={this.state.name}
+ onChange={this.updateName}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='description'
+ type='text'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='channelId'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.channel'
+ defaultMessage='Channel'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <ChannelSelect
+ id='channelId'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='triggerWords'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.triggerWords'
+ defaultMessage='Trigger Words (One Per Line)'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <textarea
+ id='triggerWords'
+ rows='3'
+ className='form-control'
+ value={this.state.triggerWords}
+ onChange={this.updateTriggerWords}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='callbackUrls'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.callbackUrls'
+ defaultMessage='Callback URLs (One Per Line)'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <textarea
+ id='callbackUrls'
+ rows='3'
+ className='form-control'
+ value={this.state.callbackUrls}
+ onChange={this.updateCallbackUrls}
+ />
+ </div>
+ </div>
+ <div className='backstage-form__footer'>
+ <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <Link
+ className='btn btn-sm'
+ to={'/settings/integrations/add'}
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.save'
+ defaultMessage='Save'
+ />
+ </SpinnerButton>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/backstage_category.jsx
new file mode 100644
index 000000000..913c7562c
--- /dev/null
+++ b/webapp/components/backstage/backstage_category.jsx
@@ -0,0 +1,68 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class BackstageCategory extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ icon: React.PropTypes.string.isRequired,
+ parentLink: React.PropTypes.string,
+ children: React.PropTypes.arrayOf(React.PropTypes.element)
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: '',
+ children: []
+ };
+ }
+
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
+ render() {
+ const {name, title, icon, parentLink, children} = this.props;
+
+ const link = parentLink + '/' + name;
+
+ let clonedChildren = null;
+ if (children.length > 0 && this.context.router.isActive(link)) {
+ clonedChildren = (
+ <ul className='sections'>
+ {
+ React.Children.map(children, (child) => {
+ return React.cloneElement(child, {
+ parentLink: link
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ return (
+ <li className='backstage-sidebar__category'>
+ <Link
+ to={link}
+ className='category-title'
+ activeClassName='category-title--active'
+ >
+ <i className={'fa ' + icon}/>
+ <span className='category-title__text'>
+ {title}
+ </span>
+ </Link>
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/backstage_navbar.jsx
new file mode 100644
index 000000000..d1dac6043
--- /dev/null
+++ b/webapp/components/backstage/backstage_navbar.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import TeamStore from 'stores/team_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import {Link} from 'react-router';
+
+export default class BackstageNavbar extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+
+ this.state = {
+ team: TeamStore.getCurrent()
+ };
+ }
+
+ componentDidMount() {
+ TeamStore.addChangeListener(this.handleChange);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.handleChange);
+ }
+
+ handleChange() {
+ this.setState({
+ team: TeamStore.getCurrent()
+ });
+ }
+
+ render() {
+ if (!this.state.team) {
+ return null;
+ }
+
+ return (
+ <div className='backstage-navbar row'>
+ <Link
+ className='backstage-navbar__back'
+ to={`/${this.state.team.display_name}/channels/town-square`}
+ >
+ <i className='fa fa-angle-left'/>
+ <span>
+ <FormattedMessage
+ id='backstage_navbar.backToMattermost'
+ defaultMessage='Back to {siteName}'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </span>
+ </Link>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx
new file mode 100644
index 000000000..d6ce2b258
--- /dev/null
+++ b/webapp/components/backstage/backstage_section.jsx
@@ -0,0 +1,80 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class BackstageSection extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ parentLink: React.PropTypes.string,
+ subsection: React.PropTypes.bool,
+ children: React.PropTypes.arrayOf(React.PropTypes.element)
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: '',
+ subsection: false,
+ children: []
+ };
+ }
+
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
+ getLink() {
+ return this.props.parentLink + '/' + this.props.name;
+ }
+
+ render() {
+ const {title, subsection, children} = this.props;
+
+ const link = this.getLink();
+
+ let clonedChildren = null;
+ if (children.length > 0) {
+ clonedChildren = (
+ <ul className='subsections'>
+ {
+ React.Children.map(children, (child) => {
+ return React.cloneElement(child, {
+ parentLink: link,
+ subsection: true
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ let className = 'section';
+ if (subsection) {
+ className = 'subsection';
+ }
+
+ return (
+ <li className={className}>
+ <Link
+ className={`${className}-title`}
+ activeClassName={`${className}-title--active`}
+ onlyActiveOnIndex={true}
+ onClick={this.handleClick}
+ to={link}
+ >
+ <span className={`${className}-title__text`}>
+ {title}
+ </span>
+ </Link>
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx
new file mode 100644
index 000000000..13c4f8b50
--- /dev/null
+++ b/webapp/components/backstage/backstage_sidebar.jsx
@@ -0,0 +1,68 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import BackstageCategory from './backstage_category.jsx';
+import BackstageSection from './backstage_section.jsx';
+import {FormattedMessage} from 'react-intl';
+
+export default class BackstageSidebar extends React.Component {
+ render() {
+ return (
+ <div className='backstage-sidebar'>
+ <ul>
+ <BackstageCategory
+ name='integrations'
+ parentLink={'/settings'}
+ icon='fa-link'
+ title={
+ <FormattedMessage
+ id='backstage_sidebar.integrations'
+ defaultMessage='Integrations'
+ />
+ }
+ >
+ <BackstageSection
+ name='installed'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.installed'
+ defaultMessage='Installed Integrations'
+ />
+ )}
+ />
+ <BackstageSection
+ name='add'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.add'
+ defaultMessage='Add Integration'
+ />
+ )}
+ >
+ <BackstageSection
+ name='incoming_webhook'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.add.incomingWebhook'
+ defaultMessage='Incoming Webhook'
+ />
+ )}
+ />
+ <BackstageSection
+ name='outgoing_webhook'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.add.outgoingWebhook'
+ defaultMessage='Outgoing Webhook'
+ />
+ )}
+ />
+ </BackstageSection>
+ </BackstageCategory>
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx
new file mode 100644
index 000000000..f65cf6327
--- /dev/null
+++ b/webapp/components/backstage/installed_incoming_webhook.jsx
@@ -0,0 +1,71 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class InstalledIncomingWebhook extends React.Component {
+ static get propTypes() {
+ return {
+ incomingWebhook: React.PropTypes.object.isRequired,
+ onDeleteClick: React.PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleDeleteClick = this.handleDeleteClick.bind(this);
+ }
+
+ handleDeleteClick(e) {
+ e.preventDefault();
+
+ this.props.onDeleteClick(this.props.incomingWebhook);
+ }
+
+ render() {
+ const incomingWebhook = this.props.incomingWebhook;
+
+ const channel = ChannelStore.get(incomingWebhook.channel_id);
+ const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ return (
+ <div className='backstage-list__item'>
+ <div className='item-details'>
+ <div className='item-details__row'>
+ <span className='item-details__name'>
+ {channelName}
+ </span>
+ <span className='item-details__type'>
+ <FormattedMessage
+ id='installed_integrations.incomingWebhookType'
+ defaultMessage='(Incoming Webhook)'
+ />
+ </span>
+ </div>
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id}
+ </span>
+ </div>
+ </div>
+ <div className='item-actions'>
+ <a
+ href='#'
+ onClick={this.handleDeleteClick}
+ >
+ <FormattedMessage
+ id='installed_integrations.delete'
+ defaultMessage='Delete'
+ />
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
new file mode 100644
index 000000000..fe84ae81a
--- /dev/null
+++ b/webapp/components/backstage/installed_integrations.jsx
@@ -0,0 +1,293 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import IntegrationStore from 'stores/integration_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
+import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
+import {Link} from 'react-router';
+
+export default class InstalledIntegrations extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+ this.updateFilter = this.updateFilter.bind(this);
+ this.updateTypeFilter = this.updateTypeFilter.bind(this);
+
+ this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this);
+ this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this);
+ this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this);
+
+ this.state = {
+ incomingWebhooks: [],
+ outgoingWebhooks: [],
+ typeFilter: '',
+ filter: ''
+ };
+ }
+
+ componentWillMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (IntegrationStore.hasReceivedIncomingWebhooks()) {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks()
+ });
+ } else {
+ AsyncClient.listIncomingHooks();
+ }
+ }
+
+ if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (IntegrationStore.hasReceivedOutgoingWebhooks()) {
+ this.setState({
+ outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
+ });
+ } else {
+ AsyncClient.listOutgoingHooks();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleIntegrationChange() {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks(),
+ outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
+ });
+ }
+
+ updateTypeFilter(e, typeFilter) {
+ e.preventDefault();
+
+ this.setState({
+ typeFilter
+ });
+ }
+
+ updateFilter(e) {
+ this.setState({
+ filter: e.target.value
+ });
+ }
+
+ deleteIncomingWebhook(incomingWebhook) {
+ AsyncClient.deleteIncomingHook(incomingWebhook.id);
+ }
+
+ regenOutgoingWebhookToken(outgoingWebhook) {
+ AsyncClient.regenOutgoingHookToken(outgoingWebhook.id);
+ }
+
+ deleteOutgoingWebhook(outgoingWebhook) {
+ AsyncClient.deleteOutgoingHook(outgoingWebhook.id);
+ }
+
+ renderTypeFilters(incomingWebhooks, outgoingWebhooks) {
+ const fields = [];
+
+ if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) {
+ let filterClassName = 'filter-sort';
+ if (this.state.typeFilter === '') {
+ filterClassName += ' filter-sort--active';
+ }
+
+ fields.push(
+ <a
+ key='allFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.updateTypeFilter(e, '')}
+ >
+ <FormattedMessage
+ id='installed_integrations.allFilter'
+ defaultMessage='All ({count})'
+ values={{
+ count: incomingWebhooks.length + outgoingWebhooks.length
+ }}
+ />
+ </a>
+ );
+ }
+
+ if (incomingWebhooks.length > 0) {
+ fields.push(
+ <span
+ key='incomingWebhooksDivider'
+ className='divider'
+ >
+ {'|'}
+ </span>
+ );
+
+ let filterClassName = 'filter-sort';
+ if (this.state.typeFilter === 'incomingWebhooks') {
+ filterClassName += ' filter-sort--active';
+ }
+
+ fields.push(
+ <a
+ key='incomingWebhooksFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.updateTypeFilter(e, 'incomingWebhooks')}
+ >
+ <FormattedMessage
+ id='installed_integrations.incomingWebhooksFilter'
+ defaultMessage='Incoming Webhooks ({count})'
+ values={{
+ count: incomingWebhooks.length
+ }}
+ />
+ </a>
+ );
+ }
+
+ if (outgoingWebhooks.length > 0) {
+ fields.push(
+ <span
+ key='outgoingWebhooksDivider'
+ className='divider'
+ >
+ {'|'}
+ </span>
+ );
+
+ let filterClassName = 'filter-sort';
+ if (this.state.typeFilter === 'outgoingWebhooks') {
+ filterClassName += ' filter-sort--active';
+ }
+
+ fields.push(
+ <a
+ key='outgoingWebhooksFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.updateTypeFilter(e, 'outgoingWebhooks')}
+ >
+ <FormattedMessage
+ id='installed_integrations.outgoingWebhooksFilter'
+ defaultMessage='Outgoing Webhooks ({count})'
+ values={{
+ count: outgoingWebhooks.length
+ }}
+ />
+ </a>
+ );
+ }
+
+ return (
+ <div className='backstage-filters__sort'>
+ {fields}
+ </div>
+ );
+ }
+
+ render() {
+ const incomingWebhooks = this.state.incomingWebhooks;
+ const outgoingWebhooks = this.state.outgoingWebhooks;
+
+ const filter = this.state.filter.toLowerCase();
+
+ const integrations = [];
+ if (!this.state.typeFilter || this.state.typeFilter === 'incomingWebhooks') {
+ for (const incomingWebhook of incomingWebhooks) {
+ if (filter) {
+ const channel = ChannelStore.get(incomingWebhook.channel_id);
+
+ if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) {
+ continue;
+ }
+ }
+
+ integrations.push(
+ <InstalledIncomingWebhook
+ key={incomingWebhook.id}
+ incomingWebhook={incomingWebhook}
+ onDeleteClick={this.deleteIncomingWebhook}
+ />
+ );
+ }
+ }
+
+ if (!this.state.typeFilter || this.state.typeFilter === 'outgoingWebhooks') {
+ for (const outgoingWebhook of outgoingWebhooks) {
+ if (filter) {
+ const channel = ChannelStore.get(outgoingWebhook.channel_id);
+
+ if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) {
+ continue;
+ }
+ }
+
+ integrations.push(
+ <InstalledOutgoingWebhook
+ key={outgoingWebhook.id}
+ outgoingWebhook={outgoingWebhook}
+ onRegenToken={this.regenOutgoingWebhookToken}
+ onDelete={this.deleteOutgoingWebhook}
+ />
+ );
+ }
+ }
+
+ return (
+ <div className='backstage-content row'>
+ <div className='installed-integrations'>
+ <div className='backstage-header'>
+ <h1>
+ <FormattedMessage
+ id='installed_integrations.header'
+ defaultMessage='Installed Integrations'
+ />
+ </h1>
+ <Link
+ className='add-integrations-link'
+ to={'/settings/integrations/add'}
+ >
+ <button
+ type='button'
+ className='btn btn-primary'
+ >
+ <span>
+ <FormattedMessage
+ id='installed_integrations.add'
+ defaultMessage='Add Integration'
+ />
+ </span>
+ </button>
+ </Link>
+ </div>
+ <div className='backstage-filters'>
+ {this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)}
+ <div className='backstage-filter__search'>
+ <i className='fa fa-search'></i>
+ <input
+ type='search'
+ className='form-control'
+ placeholder={Utils.localizeMessage('installed_integrations.search', 'Search Integrations')}
+ value={this.state.filter}
+ onChange={this.updateFilter}
+ style={{flexGrow: 0, flexShrink: 0}}
+ />
+ </div>
+ </div>
+ <div className='backstage-list'>
+ {integrations}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx
new file mode 100644
index 000000000..fee427260
--- /dev/null
+++ b/webapp/components/backstage/installed_outgoing_webhook.jsx
@@ -0,0 +1,91 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class InstalledOutgoingWebhook extends React.Component {
+ static get propTypes() {
+ return {
+ outgoingWebhook: React.PropTypes.object.isRequired,
+ onRegenToken: React.PropTypes.func.isRequired,
+ onDelete: React.PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleRegenToken = this.handleRegenToken.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+ }
+
+ handleRegenToken(e) {
+ e.preventDefault();
+
+ this.props.onRegenToken(this.props.outgoingWebhook);
+ }
+
+ handleDelete(e) {
+ e.preventDefault();
+
+ this.props.onDelete(this.props.outgoingWebhook);
+ }
+
+ render() {
+ const outgoingWebhook = this.props.outgoingWebhook;
+
+ const channel = ChannelStore.get(outgoingWebhook.channel_id);
+ const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ return (
+ <div className='backstage-list__item'>
+ <div className='item-details'>
+ <div className='item-details__row'>
+ <span className='item-details__name'>
+ {channelName}
+ </span>
+ <span className='item-details__type'>
+ <FormattedMessage
+ id='installed_integrations.outgoingWebhookType'
+ defaultMessage='(Outgoing Webhook)'
+ />
+ </span>
+ </div>
+ <div className='item-details__row'>
+ <span className='item-details__description'>
+ {Utils.getWindowLocationOrigin() + '/hooks/' + outgoingWebhook.id}
+ {' - '}
+ {outgoingWebhook.token}
+ </span>
+ </div>
+ </div>
+ <div className='actions'>
+ <a
+ href='#'
+ onClick={this.handleRegenToken}
+ >
+ <FormattedMessage
+ id='installed_integrations.regenToken'
+ defaultMessage='Regen Token'
+ />
+ </a>
+ {' - '}
+ <a
+ href='#'
+ onClick={this.handleDelete}
+ >
+ <FormattedMessage
+ id='installed_integrations.delete'
+ defaultMessage='Delete'
+ />
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/center_panel.jsx b/webapp/components/center_panel.jsx
deleted file mode 100644
index 62b12c1d2..000000000
--- a/webapp/components/center_panel.jsx
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
-import CreatePost from './create_post.jsx';
-import PostsViewContainer from './posts_view_container.jsx';
-import PostFocusView from './post_focus_view.jsx';
-import ChannelHeader from './channel_header.jsx';
-import Navbar from './navbar.jsx';
-import FileUploadOverlay from './file_upload_overlay.jsx';
-
-import PreferenceStore from 'stores/preference_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-
-import * as Utils from 'utils/utils.jsx';
-
-import {FormattedMessage} from 'react-intl';
-
-import Constants from 'utils/constants.jsx';
-const TutorialSteps = Constants.TutorialSteps;
-const Preferences = Constants.Preferences;
-
-import React from 'react';
-import {Link} from 'react-router';
-
-export default class CenterPanel extends React.Component {
- constructor(props) {
- super(props);
-
- this.getStateFromStores = this.getStateFromStores.bind(this);
- this.validState = this.validState.bind(this);
- this.onStoresChange = this.onStoresChange.bind(this);
-
- this.state = this.getStateFromStores();
- }
- getStateFromStores() {
- const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
- return {
- showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
- showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
- user: UserStore.getCurrentUser(),
- channel: ChannelStore.getCurrent(),
- profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
- };
- }
- validState() {
- return this.state.user && this.state.channel && this.state.profiles;
- }
- onStoresChange() {
- this.setState(this.getStateFromStores());
- }
- componentDidMount() {
- PreferenceStore.addChangeListener(this.onStoresChange);
- ChannelStore.addChangeListener(this.onStoresChange);
- UserStore.addChangeListener(this.onStoresChange);
- }
- componentWillUnmount() {
- PreferenceStore.removeChangeListener(this.onStoresChange);
- ChannelStore.removeChangeListener(this.onStoresChange);
- UserStore.removeChangeListener(this.onStoresChange);
- }
- render() {
- if (!this.validState()) {
- return null;
- }
- const channel = this.state.channel;
- var handleClick = null;
- let postsContainer;
- let createPost;
- if (this.state.showTutorialScreens) {
- postsContainer = <TutorialIntroScreens/>;
- createPost = null;
- } else if (this.state.showPostFocus) {
- postsContainer = <PostFocusView profiles={this.state.profiles}/>;
-
- handleClick = function clickHandler(e) {
- e.preventDefault();
- Utils.switchChannel(channel);
- };
-
- createPost = (
- <div
- id='archive-link-home'
- onClick={handleClick}
- >
- <Link to=''>
- <FormattedMessage
- id='center_panel.recent'
- defaultMessage='Click here to jump to recent messages. '
- />
- <i className='fa fa-arrow-down'></i>
- </Link>
- </div>
- );
- } else {
- postsContainer = <PostsViewContainer profiles={this.state.profiles}/>;
- createPost = (
- <div
- className='post-create__container'
- id='post-create'
- >
- <CreatePost/>
- </div>
- );
- }
-
- return (
- <div className='inner-wrap channel__wrap'>
- <div className='row header'>
- <div id='navbar'>
- <Navbar/>
- </div>
- </div>
- <div className='row main'>
- <FileUploadOverlay
- id='file_upload_overlay'
- overlayType='center'
- />
- <div
- id='app-content'
- className='app__content'
- >
- <div
- id='channel-header'
- className='channel-header'
- >
- <ChannelHeader
- user={this.state.user}
- />
- </div>
- {postsContainer}
- {createPost}
- </div>
- </div>
- </div>
- );
- }
-}
-
-CenterPanel.defaultProps = {
-};
-
-CenterPanel.propTypes = {
-};
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 369fa2dbb..482aabc01 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -26,6 +26,7 @@ import * as Utils from 'utils/utils.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Client from 'utils/client.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
@@ -53,11 +54,11 @@ export default class ChannelHeader extends React.Component {
this.state = state;
}
getStateFromStores() {
- const extraInfo = ChannelStore.getCurrentExtraInfo();
+ const extraInfo = ChannelStore.getExtraInfo(this.props.channelId);
return {
- channel: ChannelStore.getCurrent(),
- memberChannel: ChannelStore.getCurrentMember(),
+ channel: ChannelStore.get(this.props.channelId),
+ memberChannel: ChannelStore.getMember(this.props.channelId),
users: extraInfo.members,
userCount: extraInfo.member_count,
searchVisible: SearchStore.getSearchResults() !== null,
@@ -105,7 +106,7 @@ export default class ChannelHeader extends React.Component {
});
const townsquare = ChannelStore.getByName('town-square');
- Utils.switchChannel(townsquare);
+ GlobalActions.emitChannelClickEvent(townsquare);
},
(err) => {
AsyncClient.dispatchError(err, 'handleLeave');
@@ -215,9 +216,9 @@ export default class ChannelHeader extends React.Component {
if (!isDirect) {
popoverListMembers = (
<PopoverListMembers
+ channel={channel}
members={this.state.users}
memberCount={this.state.userCount}
- channelId={channel.id}
/>
);
}
@@ -433,7 +434,10 @@ export default class ChannelHeader extends React.Component {
}
return (
- <div>
+ <div
+ id='channel-header'
+ className='channel-header'
+ >
<table className='channel-header alt'>
<tbody>
<tr>
@@ -518,4 +522,5 @@ export default class ChannelHeader extends React.Component {
}
ChannelHeader.propTypes = {
+ channelId: React.PropTypes.string.isRequired
};
diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx
index e4af9f9ce..1fcd461ea 100644
--- a/webapp/components/channel_invite_button.jsx
+++ b/webapp/components/channel_invite_button.jsx
@@ -65,6 +65,7 @@ export default class ChannelInviteButton extends React.Component {
render() {
return (
<SpinnerButton
+ className='btn btn-sm btn-primary'
onClick={this.handleClick}
spinning={this.state.addingUser}
>
diff --git a/webapp/components/channel_notifications_modal.jsx b/webapp/components/channel_notifications_modal.jsx
index cc1162b77..564776876 100644
--- a/webapp/components/channel_notifications_modal.jsx
+++ b/webapp/components/channel_notifications_modal.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import {Modal} from 'react-bootstrap';
import SettingItemMin from './setting_item_min.jsx';
import SettingItemMax from './setting_item_max.jsx';
@@ -33,6 +34,7 @@ export default class ChannelNotificationsModal extends React.Component {
};
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.setState({activeSection: section});
}
componentWillReceiveProps(nextProps) {
diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx
new file mode 100644
index 000000000..8622d1f57
--- /dev/null
+++ b/webapp/components/channel_select.jsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Constants from 'utils/constants.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+export default class ChannelSelect extends React.Component {
+ static get propTypes() {
+ return {
+ onChange: React.PropTypes.func,
+ value: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChannelChange = this.handleChannelChange.bind(this);
+
+ this.state = {
+ channels: []
+ };
+ }
+
+ componentWillMount() {
+ this.setState({
+ channels: ChannelStore.getAll()
+ });
+
+ ChannelStore.addChangeListener(this.handleChannelChange);
+ }
+
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.handleChannelChange);
+ }
+
+ handleChannelChange() {
+ this.setState({
+ channels: ChannelStore.getAll()
+ });
+ }
+
+ render() {
+ const options = [
+ <option
+ key=''
+ value=''
+ >
+ {Utils.localizeMessage('channel_select.placeholder', '--- Select a channel ---')}
+ </option>
+ ];
+
+ this.state.channels.forEach((channel) => {
+ if (channel.type !== Constants.DM_CHANNEL) {
+ options.push(
+ <option
+ key={channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ }
+ });
+
+ return (
+ <select
+ className='form-control'
+ value={this.props.value}
+ onChange={this.props.onChange}
+ >
+ {options}
+ </select>
+ );
+ }
+}
diff --git a/webapp/components/channel_view.jsx b/webapp/components/channel_view.jsx
index 34e1666d0..4cca5aa98 100644
--- a/webapp/components/channel_view.jsx
+++ b/webapp/components/channel_view.jsx
@@ -1,14 +1,63 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import CenterPanel from 'components/center_panel.jsx';
-
import React from 'react';
+import ChannelHeader from 'components/channel_header.jsx';
+import PostsViewContainer from 'components/posts_view_container.jsx';
+import CreatePost from 'components/create_post.jsx';
+
+import ChannelStore from 'stores/channel_store.jsx';
+
export default class ChannelView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+ this.isStateValid = this.isStateValid.bind(this);
+ this.updateState = this.updateState.bind(this);
+
+ this.state = this.getStateFromStores(props);
+ }
+ getStateFromStores(props) {
+ const channel = ChannelStore.getByName(props.params.channel);
+ const channelId = channel ? channel.id : '';
+ return {
+ channelId
+ };
+ }
+ isStateValid() {
+ return this.state.channelId !== '';
+ }
+ updateState() {
+ this.setState(this.getStateFromStores(this.props));
+ }
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.updateState);
+ }
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.updateState);
+ }
+ componentWillReceiveProps(nextProps) {
+ this.setState(this.getStateFromStores(nextProps));
+ }
render() {
return (
- <CenterPanel/>
+ <div
+ id='app-content'
+ className='app__content'
+ >
+ <ChannelHeader
+ channelId={this.state.channelId}
+ />
+ <PostsViewContainer profiles={this.props.profiles}/>
+ <div
+ className='post-create__container'
+ id='post-create'
+ >
+ <CreatePost/>
+ </div>
+ </div>
);
}
}
@@ -16,5 +65,6 @@ ChannelView.defaultProps = {
};
ChannelView.propTypes = {
- params: React.PropTypes.object
+ params: React.PropTypes.object.isRequired,
+ profiles: React.PropTypes.object
};
diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx
index 1f51f9cd5..1ceb42a27 100644
--- a/webapp/components/claim/components/email_to_ldap.jsx
+++ b/webapp/components/claim/components/email_to_ldap.jsx
@@ -21,7 +21,7 @@ export default class EmailToLDAP extends React.Component {
e.preventDefault();
var state = {};
- const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
+ const password = ReactDOM.findDOMNode(this.refs.emailpassword).value.trim();
if (!password) {
state.error = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.');
this.setState(state);
@@ -105,12 +105,18 @@ export default class EmailToLDAP extends React.Component {
}}
/>
</p>
+ <input
+ type='text'
+ style={{display: 'none'}}
+ name='fakeusernameremembered'
+ />
<div className={formClass}>
<input
type='password'
className='form-control'
- name='password'
- ref='password'
+ name='emailPassword'
+ ref='emailpassword'
+ autoComplete='off'
placeholder={Utils.localizeMessage('claim.email_to_ldap.pwd', 'Password')}
spellCheck='false'
/>
@@ -131,6 +137,7 @@ export default class EmailToLDAP extends React.Component {
className='form-control'
name='ldapId'
ref='ldapid'
+ autoComplete='off'
placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapId', 'LDAP ID')}
spellCheck='false'
/>
@@ -141,6 +148,7 @@ export default class EmailToLDAP extends React.Component {
className='form-control'
name='ldapPassword'
ref='ldappassword'
+ autoComplete='off'
placeholder={Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'LDAP Password')}
spellCheck='false'
/>
diff --git a/webapp/components/error_page.jsx b/webapp/components/error_page.jsx
new file mode 100644
index 000000000..53f0fce82
--- /dev/null
+++ b/webapp/components/error_page.jsx
@@ -0,0 +1,58 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import $ from 'jquery';
+
+import React from 'react';
+import {Link} from 'react-router';
+
+import * as Utils from 'utils/utils.jsx';
+
+export default class ErrorPage extends React.Component {
+ componentDidMount() {
+ $('body').attr('class', 'sticky error');
+ }
+ componentWillUnmount() {
+ $('body').attr('class', '');
+ }
+ render() {
+ let title = this.props.location.query.title;
+ if (!title || title === '') {
+ title = Utils.localizeMessage('error.generic.title', 'Error');
+ }
+
+ let message = this.props.location.query.message;
+ if (!message || message === '') {
+ message = Utils.localizeMessage('error.generic.message', 'An error has occoured.');
+ }
+
+ let link = this.props.location.query.link;
+ if (!link || link === '') {
+ link = '/';
+ }
+
+ let linkMessage = this.props.location.query.linkmessage;
+ if (!linkMessage || linkMessage === '') {
+ linkMessage = Utils.localizeMessage('error.generic.link_message', 'Back to Mattermost');
+ }
+
+ return (
+ <div className='container-fluid'>
+ <div className='error__container'>
+ <div className='error__icon'>
+ <i className='fa fa-exclamation-triangle'/>
+ </div>
+ <h2>{title}</h2>
+ <p>{message}</p>
+ <Link to={link}>{linkMessage}</Link>
+ </div>
+ </div>
+ );
+ }
+}
+
+ErrorPage.defaultProps = {
+};
+ErrorPage.propTypes = {
+ location: React.PropTypes.object
+};
diff --git a/webapp/components/form_error.jsx b/webapp/components/form_error.jsx
new file mode 100644
index 000000000..b7d1de16a
--- /dev/null
+++ b/webapp/components/form_error.jsx
@@ -0,0 +1,50 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+export default class FormError extends React.Component {
+ static get propTypes() {
+ // accepts either a single error or an array of errors
+ return {
+ error: React.PropTypes.node,
+ errors: React.PropTypes.arrayOf(React.PropTypes.node)
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ error: null,
+ errors: []
+ };
+ }
+
+ render() {
+ if (!this.props.error && this.props.errors.length === 0) {
+ return null;
+ }
+
+ // look for the first truthy error to display
+ let message = this.props.error;
+
+ if (!message) {
+ for (const error of this.props.errors) {
+ if (error) {
+ message = error;
+ }
+ }
+ }
+
+ if (!message) {
+ return null;
+ }
+
+ return (
+ <div className='form-group has-error'>
+ <label className='control-label'>
+ {message}
+ </label>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index 1f8fd6133..81c3a9629 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -50,6 +50,7 @@ class InviteMemberModal extends React.Component {
constructor(props) {
super(props);
+ this.teamChange = this.teamChange.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleHide = this.handleHide.bind(this);
@@ -68,16 +69,27 @@ class InviteMemberModal extends React.Component {
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true',
userCreationEnabled: global.window.mm_config.EnableUserCreation === 'true',
showConfirmModal: false,
- isSendingEmails: false
+ isSendingEmails: false,
+ teamType: null
};
}
+ teamChange() {
+ const team = TeamStore.getCurrent();
+ const teamType = team ? team.type : null;
+ this.setState({
+ teamType
+ });
+ }
+
componentDidMount() {
ModalStore.addModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle);
+ TeamStore.addChangeListener(this.teamChange);
}
componentWillUnmount() {
ModalStore.removeModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle);
+ TeamStore.removeChangeListener(this.teamChange);
}
handleToggle(value) {
@@ -224,7 +236,7 @@ class InviteMemberModal extends React.Component {
var currentUser = UserStore.getCurrentUser();
const {formatMessage} = this.props.intl;
- if (currentUser != null) {
+ if (currentUser != null && this.state.teamType != null) {
var inviteSections = [];
var inviteIds = this.state.inviteIds;
for (var i = 0; i < inviteIds.length; i++) {
@@ -398,7 +410,7 @@ class InviteMemberModal extends React.Component {
);
} else if (this.state.userCreationEnabled) {
var teamInviteLink = null;
- if (currentUser && TeamStore.getCurrent().type === 'O') {
+ if (currentUser && this.state.teamType === 'O') {
var link = (
<a
href='#'
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index c6f7b50b1..0c4571083 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -10,13 +10,17 @@ import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
+const TutorialSteps = Constants.TutorialSteps;
+const Preferences = Constants.Preferences;
import ErrorBar from 'components/error_bar.jsx';
import * as Websockets from 'action_creators/websocket_actions.jsx';
+import LoadingScreen from 'components/loading_screen.jsx';
import {browserHistory} from 'react-router';
import SidebarRight from 'components/sidebar_right.jsx';
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
+import Navbar from 'components/navbar.jsx';
// Modals
import GetPostLinkModal from 'components/get_post_link_modal.jsx';
@@ -41,6 +45,14 @@ export default class LoggedIn extends React.Component {
super(params);
this.onUserChanged = this.onUserChanged.bind(this);
+
+ this.state = {
+ user: null,
+ profiles: null
+ };
+ }
+ isValidState() {
+ return this.state.user != null && this.state.profiles != null;
}
onUserChanged() {
// Grab the current user
@@ -66,6 +78,20 @@ export default class LoggedIn extends React.Component {
Utils.applyTheme(Constants.THEMES.default);
}
}
+
+ // Go to tutorial if we are first arrivign
+ const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
+ if (tutorialStep <= TutorialSteps.INTRO_SCREENS) {
+ browserHistory.push(Utils.getTeamURLFromAddressBar() + '/tutorial');
+ }
+
+ // Get profiles
+ const profiles = UserStore.getProfiles();
+
+ this.setState({
+ user,
+ profiles
+ });
}
componentWillMount() {
// Emit view action
@@ -177,6 +203,8 @@ export default class LoggedIn extends React.Component {
Websockets.close();
UserStore.removeChangeListener(this.onUserChanged);
+ Utils.resetTheme();
+
$('body').off('click.userpopover');
$('body').off('mouseenter mouseleave', '.post');
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
@@ -186,14 +214,46 @@ export default class LoggedIn extends React.Component {
$(window).off('keydown.preventBackspace');
}
render() {
+ if (!this.isValidState()) {
+ return <LoadingScreen/>;
+ }
+
+ let content = [];
+ if (this.props.children) {
+ content = this.props.children;
+ } else {
+ content.push(
+ this.props.navbar
+ );
+ content.push(
+ this.props.sidebar
+ );
+ content.push(
+ <div
+ key='inner-wrap'
+ className='inner-wrap channel__wrap'
+ >
+ <div className='row header'>
+ <div id='navbar'>
+ <Navbar/>
+ </div>
+ </div>
+ <div className='row main'>
+ {React.cloneElement(this.props.center, {
+ user: this.state.user,
+ profiles: this.state.profiles
+ })}
+ </div>
+ </div>
+ );
+ }
return (
<div className='channel-view'>
<ErrorBar/>
<div className='container-fluid'>
<SidebarRight/>
<SidebarRightMenu/>
- {this.props.sidebar}
- {this.props.center}
+ {content}
<GetPostLinkModal/>
<GetTeamInviteLinkModal/>
@@ -216,8 +276,12 @@ LoggedIn.defaultProps = {
};
LoggedIn.propTypes = {
- children: React.PropTypes.object,
- sidebar: React.PropTypes.object,
- center: React.PropTypes.object,
+ children: React.PropTypes.oneOfType([
+ React.PropTypes.arrayOf(React.PropTypes.element),
+ React.PropTypes.element
+ ]),
+ navbar: React.PropTypes.element,
+ sidebar: React.PropTypes.element,
+ center: React.PropTypes.element,
params: React.PropTypes.object
};
diff --git a/webapp/components/login_email.jsx b/webapp/components/login/components/login_email.jsx
index d54c32ff9..b1f484c08 100644
--- a/webapp/components/login_email.jsx
+++ b/webapp/components/login/components/login_email.jsx
@@ -2,69 +2,40 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
-import {browserHistory} from 'react-router';
+import Constants from 'utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-
-var holders = defineMessages({
- badTeam: {
- id: 'login_email.badTeam',
- defaultMessage: 'Bad team name'
- },
- emailReq: {
- id: 'login_email.emailReq',
- defaultMessage: 'An email is required'
- },
- pwdReq: {
- id: 'login_email.pwdReq',
- defaultMessage: 'A password is required'
- },
- email: {
- id: 'login_email.email',
- defaultMessage: 'Email'
- },
- pwd: {
- id: 'login_email.pwd',
- defaultMessage: 'Password'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
-class LoginEmail extends React.Component {
+export default class LoginEmail extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
- serverError: ''
+ serverError: props.serverError
};
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({serverError: nextProps.serverError});
+ }
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
var state = {};
- const name = this.props.teamName;
- if (!name) {
- state.serverError = formatMessage(holders.badTeam);
- this.setState(state);
- return;
- }
-
const email = this.refs.email.value.trim();
if (!email) {
- state.serverError = formatMessage(holders.emailReq);
+ state.serverError = Utils.localizeMessage('login_email.emailReq', 'An email is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
- state.serverError = formatMessage(holders.pwdReq);
+ state.serverError = Utils.localizeMessage('login_email.pwdReq', 'A password is required');
this.setState(state);
return;
}
@@ -72,21 +43,7 @@ class LoginEmail extends React.Component {
state.serverError = '';
this.setState(state);
- Client.loginByEmail(name, email, password,
- () => {
- UserStore.setLastEmail(email);
- browserHistory.push('/' + name + '/channels/town-square');
- },
- (err) => {
- if (err.id === 'api.user.login.not_verified.app_error') {
- browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email));
- return;
- }
- state.serverError = err.message;
- this.valid = false;
- this.setState(state);
- }
- );
+ this.props.submit(Constants.EMAIL_SERVICE, email, password);
}
render() {
let serverError;
@@ -110,7 +67,6 @@ class LoginEmail extends React.Component {
priorEmail = decodeURIComponent(emailParam);
}
- const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@@ -125,7 +81,7 @@ class LoginEmail extends React.Component {
name='email'
defaultValue={priorEmail}
ref='email'
- placeholder={formatMessage(holders.email)}
+ placeholder={Utils.localizeMessage('login_email.email', 'Email')}
spellCheck='false'
/>
</div>
@@ -136,7 +92,7 @@ class LoginEmail extends React.Component {
className='form-control'
name='password'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage('login_email.pwd', 'Password')}
spellCheck='false'
/>
</div>
@@ -160,8 +116,6 @@ LoginEmail.defaultProps = {
};
LoginEmail.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string.isRequired
+ submit: React.PropTypes.func.isRequired,
+ serverError: React.PropTypes.string
};
-
-export default injectIntl(LoginEmail);
diff --git a/webapp/components/login_ldap.jsx b/webapp/components/login/components/login_ldap.jsx
index 59ff973dc..a2013710f 100644
--- a/webapp/components/login_ldap.jsx
+++ b/webapp/components/login/components/login_ldap.jsx
@@ -2,68 +2,39 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
+import Constants from 'utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router';
-
-const holders = defineMessages({
- badTeam: {
- id: 'login_ldap.badTeam',
- defaultMessage: 'Bad team name'
- },
- idReq: {
- id: 'login_ldap.idlReq',
- defaultMessage: 'An LDAP ID is required'
- },
- pwdReq: {
- id: 'login_ldap.pwdReq',
- defaultMessage: 'An LDAP password is required'
- },
- username: {
- id: 'login_ldap.username',
- defaultMessage: 'LDAP Username'
- },
- pwd: {
- id: 'login_ldap.pwd',
- defaultMessage: 'LDAP Password'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
-class LoginLdap extends React.Component {
+export default class LoginLdap extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
- serverError: ''
+ serverError: props.serverError
};
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({serverError: nextProps.serverError});
+ }
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
- var state = {};
-
- const teamName = this.props.teamName;
- if (!teamName) {
- state.serverError = formatMessage(holders.badTeam);
- this.setState(state);
- return;
- }
+ const state = {};
const id = this.refs.id.value.trim();
if (!id) {
- state.serverError = formatMessage(holders.idReq);
+ state.serverError = Utils.localizeMessage('login_ldap.idlReq', 'An LDAP ID is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
- state.serverError = formatMessage(holders.pwdReq);
+ state.serverError = Utils.localizeMessage('login_ldap.pwdReq', 'An LDAP password is required');
this.setState(state);
return;
}
@@ -71,20 +42,7 @@ class LoginLdap extends React.Component {
state.serverError = '';
this.setState(state);
- Client.loginByLdap(teamName, id, password,
- () => {
- const redirect = Utils.getUrlParameter('redirect');
- if (redirect) {
- browserHistory.push(decodeURIComponent(redirect));
- } else {
- browserHistory.push('/' + teamName + '/channels/town-square');
- }
- },
- (err) => {
- state.serverError = err.message;
- this.setState(state);
- }
- );
+ this.props.submit(Constants.LDAP_SERVICE, id, password);
}
render() {
let serverError;
@@ -93,7 +51,7 @@ class LoginLdap extends React.Component {
serverError = <label className='control-label'>{this.state.serverError}</label>;
errorClass = ' has-error';
}
- const {formatMessage} = this.props.intl;
+
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@@ -105,7 +63,7 @@ class LoginLdap extends React.Component {
autoFocus={true}
className='form-control'
ref='id'
- placeholder={formatMessage(holders.username)}
+ placeholder={Utils.localizeMessage('login_ldap.username', 'LDAP Username')}
spellCheck='false'
/>
</div>
@@ -114,7 +72,7 @@ class LoginLdap extends React.Component {
type='password'
className='form-control'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage('login_ldap.pwd', 'LDAP Password')}
spellCheck='false'
/>
</div>
@@ -138,8 +96,6 @@ LoginLdap.defaultProps = {
};
LoginLdap.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string.isRequired
+ serverError: React.PropTypes.string,
+ submit: React.PropTypes.func.isRequired
};
-
-export default injectIntl(LoginLdap);
diff --git a/webapp/components/login/components/login_mfa.jsx b/webapp/components/login/components/login_mfa.jsx
new file mode 100644
index 000000000..f8ebf1e82
--- /dev/null
+++ b/webapp/components/login/components/login_mfa.jsx
@@ -0,0 +1,92 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+import React from 'react';
+
+export default class LoginMfa extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+ const state = {};
+
+ const token = this.refs.token.value.trim();
+ if (!token) {
+ state.serverError = Utils.localizeMessage('login_mfa.tokenReq', 'Please enter an MFA token');
+ this.setState(state);
+ return;
+ }
+
+ state.serverError = '';
+ this.setState(state);
+
+ this.props.submit(this.props.method, this.props.loginId, this.props.password, token);
+ }
+ render() {
+ let serverError;
+ let errorClass = '';
+ if (this.state.serverError) {
+ serverError = <label className='control-label'>{this.state.serverError}</label>;
+ errorClass = ' has-error';
+ }
+
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <div className='signup__email-container'>
+ <p>
+ <FormattedMessage
+ id='login_mfa.enterToken'
+ defaultMessage="To complete the sign in process, please enter a token from your smartphone's authenticator"
+ />
+ </p>
+ <div className={'form-group' + errorClass}>
+ {serverError}
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ type='text'
+ className='form-control'
+ name='token'
+ ref='token'
+ placeholder={Utils.localizeMessage('login_mfa.token', 'MFA Token')}
+ spellCheck='false'
+ autoComplete='off'
+ autoFocus={true}
+ />
+ </div>
+ <div className='form-group'>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ >
+ <FormattedMessage
+ id='login_mfa.submit'
+ defaultMessage='Submit'
+ />
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+}
+LoginMfa.defaultProps = {
+};
+
+LoginMfa.propTypes = {
+ method: React.PropTypes.string.isRequired,
+ loginId: React.PropTypes.string.isRequired,
+ password: React.PropTypes.string.isRequired,
+ submit: React.PropTypes.func.isRequired
+};
diff --git a/webapp/components/login_username.jsx b/webapp/components/login/components/login_username.jsx
index 71874fa1a..3cb213994 100644
--- a/webapp/components/login_username.jsx
+++ b/webapp/components/login/components/login_username.jsx
@@ -2,42 +2,10 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
+import Constants from 'utils/constants.jsx';
-import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router';
-
-var holders = defineMessages({
- badTeam: {
- id: 'login_username.badTeam',
- defaultMessage: 'Bad team name'
- },
- usernameReq: {
- id: 'login_username.usernameReq',
- defaultMessage: 'A username is required'
- },
- pwdReq: {
- id: 'login_username.pwdReq',
- defaultMessage: 'A password is required'
- },
- verifyEmailError: {
- id: 'login_username.verifyEmailError',
- defaultMessage: 'Please verify your email address. Check your inbox for an email.'
- },
- userNotFoundError: {
- id: 'login_username.userNotFoundError',
- defaultMessage: "We couldn't find an existing account matching your username for this team."
- },
- username: {
- id: 'login_username.username',
- defaultMessage: 'Username'
- },
- pwd: {
- id: 'login_username.pwd',
- defaultMessage: 'Password'
- }
-});
+import {FormattedMessage} from 'react-intl';
import React from 'react';
@@ -48,31 +16,26 @@ export default class LoginUsername extends React.Component {
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
- serverError: ''
+ serverError: props.serverError
};
}
+ componentWillReceiveProps(nextProps) {
+ this.setState({serverError: nextProps.serverError});
+ }
handleSubmit(e) {
e.preventDefault();
- const {formatMessage} = this.props.intl;
- var state = {};
-
- const name = this.props.teamName;
- if (!name) {
- state.serverError = formatMessage(holders.badTeam);
- this.setState(state);
- return;
- }
+ const state = {};
const username = this.refs.username.value.trim();
if (!username) {
- state.serverError = formatMessage(holders.usernameReq);
+ state.serverError = Utils.localizeMessage('login_username.usernameReq', 'A username is required');
this.setState(state);
return;
}
const password = this.refs.password.value.trim();
if (!password) {
- state.serverError = formatMessage(holders.pwdReq);
+ state.serverError = Utils.localizeMessage('login_username.pwdReq', 'A password is required');
this.setState(state);
return;
}
@@ -80,30 +43,7 @@ export default class LoginUsername extends React.Component {
state.serverError = '';
this.setState(state);
- Client.loginByUsername(name, username, password,
- () => {
- UserStore.setLastUsername(username);
-
- const redirect = Utils.getUrlParameter('redirect');
- if (redirect) {
- browserHistory.push(decodeURIComponent(redirect));
- } else {
- browserHistory.push('/' + name + '/channels/town-square');
- }
- },
- (err) => {
- if (err.id === 'api.user.login.not_verified.app_error') {
- state.serverError = formatMessage(holders.verifyEmailError);
- } else if (err.id === 'store.sql_user.get_by_username.app_error') {
- state.serverError = formatMessage(holders.userNotFoundError);
- } else {
- state.serverError = err.message;
- }
-
- this.valid = false;
- this.setState(state);
- }
- );
+ this.props.submit(Constants.USERNAME_SERVICE, username, password);
}
render() {
let serverError;
@@ -127,7 +67,6 @@ export default class LoginUsername extends React.Component {
priorUsername = decodeURIComponent(emailParam);
}
- const {formatMessage} = this.props.intl;
return (
<form onSubmit={this.handleSubmit}>
<div className='signup__email-container'>
@@ -142,7 +81,7 @@ export default class LoginUsername extends React.Component {
name='username'
defaultValue={priorUsername}
ref='username'
- placeholder={formatMessage(holders.username)}
+ placeholder={Utils.localizeMessage('login_username.username', 'Username')}
spellCheck='false'
/>
</div>
@@ -153,7 +92,7 @@ export default class LoginUsername extends React.Component {
className='form-control'
name='password'
ref='password'
- placeholder={formatMessage(holders.pwd)}
+ placeholder={Utils.localizeMessage('login_username.pwd', 'Password')}
spellCheck='false'
/>
</div>
@@ -177,8 +116,6 @@ LoginUsername.defaultProps = {
};
LoginUsername.propTypes = {
- intl: intlShape.isRequired,
- teamName: React.PropTypes.string.isRequired
+ serverError: React.PropTypes.string,
+ submit: React.PropTypes.func.isRequired
};
-
-export default injectIntl(LoginUsername);
diff --git a/webapp/components/login.jsx b/webapp/components/login/login.jsx
index e8180895d..e867af47a 100644
--- a/webapp/components/login.jsx
+++ b/webapp/components/login/login.jsx
@@ -1,14 +1,17 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import LoginEmail from './login_email.jsx';
-import LoginUsername from './login_username.jsx';
-import LoginLdap from './login_ldap.jsx';
+import LoginEmail from './components/login_email.jsx';
+import LoginUsername from './components/login_username.jsx';
+import LoginLdap from './components/login_ldap.jsx';
+import LoginMfa from './components/login_mfa.jsx';
+
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
-import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
+import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import TeamStore from 'stores/team_store.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router';
@@ -21,8 +24,12 @@ export default class Login extends React.Component {
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
+ this.preSubmit = this.preSubmit.bind(this);
+ this.submit = this.submit.bind(this);
- this.state = this.getStateFromStores();
+ const state = this.getStateFromStores();
+ state.doneCheckLogin = false;
+ this.state = state;
}
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
@@ -39,61 +46,95 @@ export default class Login extends React.Component {
}
getStateFromStores() {
return {
- currentTeam: TeamStore.getByName(this.props.params.team),
- doneCheckLogin: false
+ currentTeam: TeamStore.getByName(this.props.params.team)
};
}
onTeamChange() {
this.setState(this.getStateFromStores());
}
- render() {
- const currentTeam = this.state.currentTeam;
- if (currentTeam == null || !this.state.doneCheckLogin) {
- return <div/>;
+ preSubmit(method, loginId, password) {
+ if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') {
+ this.submit(method, loginId, password, '');
+ return;
}
- const teamDisplayName = currentTeam.display_name;
- const teamName = currentTeam.name;
- const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
- const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
+ Client.checkMfa(method, this.state.currentTeam.name, loginId,
+ (data) => {
+ if (data.mfa_required === 'true') {
+ this.setState({showMfa: true, method, loginId, password});
+ } else {
+ this.submit(method, loginId, password, '');
+ }
+ },
+ (err) => {
+ if (method === Constants.EMAIL_SERVICE) {
+ this.setState({serverEmailError: err.message});
+ } else if (method === Constants.USERNAME_SERVICE) {
+ this.setState({serverUsernameError: err.message});
+ } else if (method === Constants.LDAP_SERVICE) {
+ this.setState({serverLdapError: err.message});
+ }
+ }
+ );
+ }
+ submit(method, loginId, password, token) {
+ this.setState({showMfa: false, serverEmailError: null, serverUsernameError: null, serverLdapError: null});
- let loginMessage = [];
- if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
- loginMessage.push(
- <Link
- className='btn btn-custom-login gitlab'
- key='gitlab'
- to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='login.gitlab'
- defaultMessage='with GitLab'
- />
- </span>
- </Link>
+ const team = this.state.currentTeam.name;
+
+ if (method === Constants.EMAIL_SERVICE) {
+ Client.loginByEmail(team, loginId, password, token,
+ () => {
+ UserStore.setLastEmail(loginId);
+ browserHistory.push('/' + team + '/channels/town-square');
+ },
+ (err) => {
+ if (err.id === 'api.user.login.not_verified.app_error') {
+ browserHistory.push('/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(loginId));
+ return;
+ }
+ this.setState({serverEmailError: err.message});
+ }
);
- }
+ } else if (method === Constants.USERNAME_SERVICE) {
+ Client.loginByUsername(team, loginId, password, token,
+ () => {
+ UserStore.setLastUsername(loginId);
- if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
- loginMessage.push(
- <Link
- className='btn btn-custom-login google'
- key='google'
- to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
- >
- <span className='icon'/>
- <span>
- <FormattedMessage
- id='login.google'
- defaultMessage='with Google Apps'
- />
- </span>
- </Link>
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ browserHistory.push(decodeURIComponent(redirect));
+ } else {
+ browserHistory.push('/' + team + '/channels/town-square');
+ }
+ },
+ (err) => {
+ if (err.id === 'api.user.login.not_verified.app_error') {
+ this.setState({serverUsernameError: Utils.localizeMessage('login_username.verifyEmailError', 'Please verify your email address. Check your inbox for an email.')});
+ } else if (err.id === 'store.sql_user.get_by_username.app_error') {
+ this.setState({serverUsernameError: Utils.localizeMessage('login_username.userNotFoundError', 'We couldn\'t find an existing account matching your username for this team.')});
+ } else {
+ this.setState({serverUsernameError: err.message});
+ }
+ }
+ );
+ } else if (method === Constants.LDAP_SERVICE) {
+ Client.loginByLdap(team, loginId, password, token,
+ () => {
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ browserHistory.push(decodeURIComponent(redirect));
+ } else {
+ browserHistory.push('/' + team + '/channels/town-square');
+ }
+ },
+ (err) => {
+ this.setState({serverLdapError: err.message});
+ }
);
}
-
+ }
+ createLoginOptions(currentTeam) {
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
@@ -130,44 +171,126 @@ export default class Login extends React.Component {
}
}
- let emailSignup;
- if (global.window.mm_config.EnableSignInWithEmail === 'true') {
- emailSignup = (
- <LoginEmail
- teamName={teamName}
- />
+ const teamName = currentTeam.name;
+ const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
+ const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
+ const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
+ const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
+ const emailSigninEnabled = global.window.mm_config.EnableSignInWithEmail === 'true';
+
+ const oauthLogins = [];
+ if (gitlabSigninEnabled) {
+ oauthLogins.push(
+ <Link
+ className='btn btn-custom-login gitlab'
+ key='gitlab'
+ to={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
+ >
+ <span className='icon'/>
+ <span>
+ <FormattedMessage
+ id='login.gitlab'
+ defaultMessage='with GitLab'
+ />
+ </span>
+ </Link>
);
}
- if (loginMessage.length > 0 && emailSignup) {
- loginMessage = (
- <div>
- {loginMessage}
- <div className='or__container'>
+ if (googleSigninEnabled) {
+ oauthLogins.push(
+ <Link
+ className='btn btn-custom-login google'
+ key='google'
+ to={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
+ >
+ <span className='icon'/>
+ <span>
<FormattedMessage
- id='login.or'
- defaultMessage='or'
+ id='login.google'
+ defaultMessage='with Google Apps'
/>
+ </span>
+ </Link>
+ );
+ }
+
+ let emailLogin;
+ if (emailSigninEnabled) {
+ emailLogin = (
+ <LoginEmail
+ teamName={teamName}
+ serverError={this.state.serverEmailError}
+ submit={this.preSubmit}
+ />
+ );
+
+ if (oauthLogins.length > 0) {
+ emailLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ {emailLogin}
</div>
- </div>
+ );
+ }
+ }
+
+ let usernameLogin;
+ if (usernameSigninEnabled) {
+ usernameLogin = (
+ <LoginUsername
+ teamName={teamName}
+ serverError={this.state.serverUsernameError}
+ submit={this.preSubmit}
+ />
);
+
+ if (emailSigninEnabled || oauthLogins.length > 0) {
+ usernameLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ {usernameLogin}
+ </div>
+ );
+ }
}
- let forgotPassword;
- if (emailSignup) {
- forgotPassword = (
- <div className='form-group'>
- <Link to={'/' + teamName + '/reset_password'}>
- <FormattedMessage
- id='login.forgot'
- defaultMessage='I forgot my password'
- />
- </Link>
- </div>
+ let ldapLogin;
+ if (ldapEnabled) {
+ ldapLogin = (
+ <LoginLdap
+ teamName={teamName}
+ serverError={this.state.serverLdapError}
+ submit={this.preSubmit}
+ />
);
+
+ if (emailSigninEnabled || usernameSigninEnabled || oauthLogins.length > 0) {
+ ldapLogin = (
+ <div>
+ <div className='or__container'>
+ <FormattedMessage
+ id='login.or'
+ defaultMessage='or'
+ />
+ </div>
+ {ldapLogin}
+ </div>
+ );
+ }
}
- let userSignUp = null;
+ let userSignUp;
if (currentTeam.allow_open_invite) {
userSignUp = (
<div>
@@ -190,7 +313,21 @@ export default class Login extends React.Component {
);
}
- let teamSignUp = null;
+ let forgotPassword;
+ if (usernameSigninEnabled || emailSigninEnabled) {
+ forgotPassword = (
+ <div className='form-group'>
+ <Link to={'/' + teamName + '/reset_password'}>
+ <FormattedMessage
+ id='login.forgot'
+ defaultMessage='I forgot my password'
+ />
+ </Link>
+ </div>
+ );
+ }
+
+ let teamSignUp;
if (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp()) {
teamSignUp = (
<div className='margin--extra'>
@@ -207,54 +344,37 @@ export default class Login extends React.Component {
);
}
- let ldapLogin = null;
- if (global.window.mm_config.EnableLdap === 'true') {
- ldapLogin = (
- <LoginLdap
- teamName={teamName}
- />
- );
- }
-
- if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
- ldapLogin = (
- <div>
- <div className='or__container'>
- <FormattedMessage
- id='login.or'
- defaultMessage='or'
- />
- </div>
- <LoginLdap
- teamName={teamName}
- />
- </div>
- );
+ return (
+ <div>
+ {extraBox}
+ {oauthLogins}
+ {emailLogin}
+ {usernameLogin}
+ {ldapLogin}
+ {userSignUp}
+ {forgotPassword}
+ {teamSignUp}
+ </div>
+ );
+ }
+ render() {
+ const currentTeam = this.state.currentTeam;
+ if (currentTeam == null || !this.state.doneCheckLogin) {
+ return <div/>;
}
- let usernameLogin = null;
- if (global.window.mm_config.EnableSignInWithUsername === 'true') {
- usernameLogin = (
- <LoginUsername
- teamName={teamName}
+ let content;
+ if (this.state.showMfa) {
+ content = (
+ <LoginMfa
+ method={this.state.method}
+ loginId={this.state.loginId}
+ password={this.state.password}
+ submit={this.submit}
/>
);
- }
-
- if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
- usernameLogin = (
- <div>
- <div className='or__container'>
- <FormattedMessage
- id='login.or'
- defaultMessage='or'
- />
- </div>
- <LoginUsername
- teamName={teamName}
- />
- </div>
- );
+ } else {
+ content = this.createLoginOptions(currentTeam);
}
return (
@@ -275,7 +395,7 @@ export default class Login extends React.Component {
defaultMessage='Sign in to:'
/>
</h5>
- <h2 className='signup-team__name'>{teamDisplayName}</h2>
+ <h2 className='signup-team__name'>{currentTeam.display_name}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
@@ -285,14 +405,7 @@ export default class Login extends React.Component {
}}
/>
</h2>
- {extraBox}
- {loginMessage}
- {emailSignup}
- {usernameLogin}
- {ldapLogin}
- {userSignUp}
- {forgotPassword}
- {teamSignUp}
+ {content}
</div>
</div>
</div>
diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx
index d0eeec1ef..811bb8101 100644
--- a/webapp/components/more_channels.jsx
+++ b/webapp/components/more_channels.jsx
@@ -9,6 +9,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import LoadingScreen from './loading_screen.jsx';
import NewChannelFlow from './new_channel_flow.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -64,8 +65,7 @@ export default class MoreChannels extends React.Component {
client.joinChannel(channel.id,
() => {
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
- AsyncClient.getChannel(channel.id);
- Utils.switchChannel(channel);
+ GlobalActions.emitChannelClickEvent(channel);
this.setState({joiningChannel: -1});
},
(err) => {
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index d1446059d..29d64517e 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -5,6 +5,7 @@ import {Modal} from 'react-bootstrap';
import FilteredUserList from './filtered_user_list.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
import SpinnerButton from 'components/spinner_button.jsx';
@@ -68,7 +69,7 @@ export default class MoreDirectChannels extends React.Component {
Utils.openDirectChannelToUser(
teammate,
(channel) => {
- Utils.switchChannel(channel);
+ GlobalActions.emitChannelClickEvent(channel);
this.setState({loadingDMChannel: -1});
this.handleHide();
},
@@ -85,6 +86,7 @@ export default class MoreDirectChannels extends React.Component {
createJoinDirectChannelButton({user}) {
return (
<SpinnerButton
+ className='btn btm-sm btn-primary'
spinning={this.state.loadingDMChannel === user.id}
onClick={this.handleShowDirectChannel.bind(this, user)}
>
diff --git a/webapp/components/msg_typing.jsx b/webapp/components/msg_typing.jsx
index b2d414287..631eea78d 100644
--- a/webapp/components/msg_typing.jsx
+++ b/webapp/components/msg_typing.jsx
@@ -40,13 +40,15 @@ class MsgTyping extends React.Component {
}
updateTypingText(typingUsers) {
- if (!typingUsers) {
- return;
+ let text = '';
+ let users = {};
+ let numUsers = 0;
+ if (typingUsers) {
+ users = Object.keys(typingUsers);
+ numUsers = users.length;
}
- const users = Object.keys(typingUsers);
- let text = '';
- switch (users.length) {
+ switch (numUsers) {
case 0:
text = '';
break;
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index e58e142d0..5afd7e683 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -45,6 +45,7 @@ export default class Navbar extends React.Component {
this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this);
this.showRenameChannelModal = this.showRenameChannelModal.bind(this);
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
+ this.isStateValid = this.isStateValid.bind(this);
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
@@ -64,7 +65,7 @@ export default class Navbar extends React.Component {
currentUser: UserStore.getCurrentUser()
};
}
- stateValid() {
+ isStateValid() {
return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
}
componentDidMount() {
@@ -422,7 +423,7 @@ export default class Navbar extends React.Component {
return buttons;
}
render() {
- if (!this.stateValid()) {
+ if (!this.isStateValid()) {
return null;
}
diff --git a/webapp/components/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx
index 7e42a71ea..da1ae237e 100644
--- a/webapp/components/navbar_dropdown.jsx
+++ b/webapp/components/navbar_dropdown.jsx
@@ -59,6 +59,7 @@ export default class NavbarDropdown extends React.Component {
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
+ let integrationsLink = null;
if (currentUser != null) {
isAdmin = Utils.isAdmin(currentUser.roles);
@@ -125,6 +126,21 @@ export default class NavbarDropdown extends React.Component {
);
}
+ if (window.mm_config.EnableIncomingWebhooks === 'true' || window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (isAdmin || window.EnableAdminOnlyIntegrations !== 'true') {
+ integrationsLink = (
+ <li>
+ <Link to={'/settings/integrations'}>
+ <FormattedMessage
+ id='navbar_dropdown.integrations'
+ defaultMessage='Integrations'
+ />
+ </Link>
+ </li>
+ );
+ }
+ }
+
if (isSystemAdmin) {
sysAdminLink = (
<li>
@@ -238,6 +254,7 @@ export default class NavbarDropdown extends React.Component {
</li>
{adminDivider}
{teamSettings}
+ {integrationsLink}
{manageLink}
{sysAdminLink}
{teams}
diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx
index 30035ee5d..8c66ef3ce 100644
--- a/webapp/components/new_channel_flow.jsx
+++ b/webapp/components/new_channel_flow.jsx
@@ -2,9 +2,9 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Client from 'utils/client.jsx';
import UserStore from 'stores/user_store.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import NewChannelModal from './new_channel_modal.jsx';
import ChangeURLModal from './change_url_modal.jsx';
@@ -110,8 +110,7 @@ class NewChannelFlow extends React.Component {
Client.createChannel(channel,
(data) => {
this.props.onModalDismissed();
- AsyncClient.getChannel(data.id);
- Utils.switchChannel(data);
+ GlobalActions.emitChannelClickEvent(data);
},
(err) => {
if (err.id === 'model.channel.is_valid.2_or_more.app_error') {
@@ -247,4 +246,4 @@ NewChannelFlow.propTypes = {
onModalDismissed: React.PropTypes.func.isRequired
};
-export default injectIntl(NewChannelFlow); \ No newline at end of file
+export default injectIntl(NewChannelFlow);
diff --git a/webapp/components/permalink_view.jsx b/webapp/components/permalink_view.jsx
new file mode 100644
index 000000000..2c32d643d
--- /dev/null
+++ b/webapp/components/permalink_view.jsx
@@ -0,0 +1,93 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import ChannelHeader from 'components/channel_header.jsx';
+import PostFocusView from 'components/post_focus_view.jsx';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+
+import {Link} from 'react-router';
+import {FormattedMessage} from 'react-intl';
+
+export default class PermalinkView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getStateFromStores = this.getStateFromStores.bind(this);
+ this.isStateValid = this.isStateValid.bind(this);
+ this.updateState = this.updateState.bind(this);
+
+ this.state = this.getStateFromStores(props);
+ }
+ getStateFromStores(props) {
+ const postId = props.params.postid;
+ const channel = ChannelStore.getCurrent();
+ const channelId = channel ? channel.id : '';
+ const channelName = channel ? channel.name : '';
+ const team = TeamStore.getCurrent();
+ const teamName = team ? team.name : '';
+ return {
+ channelId,
+ channelName,
+ teamName,
+ postId
+ };
+ }
+ isStateValid() {
+ return this.state.channelId !== '' && this.state.teamName;
+ }
+ updateState() {
+ this.setState(this.getStateFromStores(this.props));
+ }
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.updateState);
+ TeamStore.addChangeListener(this.updateState);
+ }
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.updateState);
+ TeamStore.removeChangeListener(this.updateState);
+ }
+ componentWillReceiveProps(nextProps) {
+ this.setState(this.getStateFromStores(nextProps));
+ }
+ render() {
+ if (!this.isStateValid()) {
+ return null;
+ }
+ return (
+ <div
+ id='app-content'
+ className='app__content'
+ >
+ <ChannelHeader
+ channelId={this.state.channelId}
+ />
+ <PostFocusView profiles={this.props.profiles}/>
+ <div
+ id='archive-link-home'
+ >
+ <Link
+ to={'/' + this.state.teamName + '/channels/' + this.state.channelName}
+ >
+ <FormattedMessage
+ id='center_panel.recent'
+ defaultMessage='Click here to jump to recent messages. '
+ />
+ <i className='fa fa-arrow-down'></i>
+ </Link>
+ </div>
+ </div>
+ );
+ }
+}
+
+PermalinkView.defaultProps = {
+};
+
+PermalinkView.propTypes = {
+ params: React.PropTypes.object.isRequired,
+ profiles: React.PropTypes.object
+};
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 819c7f590..226a1889c 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -6,10 +6,9 @@ import $ from 'jquery';
import UserStore from 'stores/user_store.jsx';
import {Popover, Overlay} from 'react-bootstrap';
import * as Utils from 'utils/utils.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import Constants from 'utils/constants.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-
import {FormattedMessage} from 'react-intl';
import React from 'react';
@@ -36,7 +35,7 @@ export default class PopoverListMembers extends React.Component {
Utils.openDirectChannelToUser(
teammate,
(channel, channelAlreadyExisted) => {
- Utils.switchChannel(channel);
+ GlobalActions.emitChannelClickEvent(channel);
if (channelAlreadyExisted) {
this.closePopover();
}
@@ -56,7 +55,6 @@ export default class PopoverListMembers extends React.Component {
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
const currentUserId = UserStore.getCurrentId();
- const ch = ChannelStore.getCurrent();
if (members && teamMembers) {
members.sort((a, b) => {
@@ -68,7 +66,7 @@ export default class PopoverListMembers extends React.Component {
members.forEach((m, i) => {
let button = '';
- if (currentUserId !== m.id && ch.type !== 'D') {
+ if (currentUserId !== m.id && this.props.channel.type !== 'D') {
button = (
<a
href='#'
@@ -176,7 +174,7 @@ export default class PopoverListMembers extends React.Component {
}
PopoverListMembers.propTypes = {
+ channel: React.PropTypes.object.isRequired,
members: React.PropTypes.array.isRequired,
- memberCount: React.PropTypes.number,
- channelId: React.PropTypes.string.isRequired
+ memberCount: React.PropTypes.number
};
diff --git a/webapp/components/post.jsx b/webapp/components/post.jsx
index f2818188a..bbf8d9bf6 100644
--- a/webapp/components/post.jsx
+++ b/webapp/components/post.jsx
@@ -129,6 +129,7 @@ export default class Post extends React.Component {
const post = this.props.post;
const parentPost = this.props.parentPost;
const posts = this.props.posts;
+ const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
if (!post.props) {
post.props = {};
@@ -188,9 +189,9 @@ export default class Post extends React.Component {
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
+ } else {
+ src = Constants.DEFAULT_WEBHOOK_LOGO;
}
- } else if (Utils.isSystemMessage(post)) {
- src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
}
profilePic = (
@@ -200,6 +201,15 @@ export default class Post extends React.Component {
width='36'
/>
);
+
+ if (Utils.isSystemMessage(post)) {
+ profilePic = (
+ <span
+ className='icon'
+ dangerouslySetInnerHTML={{__html: mattermostLogo}}
+ />
+ );
+ }
}
return (
diff --git a/webapp/components/post_body_additional_content.jsx b/webapp/components/post_body_additional_content.jsx
index 2cd82f213..452597dde 100644
--- a/webapp/components/post_body_additional_content.jsx
+++ b/webapp/components/post_body_additional_content.jsx
@@ -70,7 +70,7 @@ export default class PostBodyAdditionalContent extends React.Component {
return this.getSlackAttachment();
}
- const link = Utils.extractLinks(this.props.post.message)[0];
+ const link = Utils.extractFirstLink(this.props.post.message);
if (!link) {
return null;
}
diff --git a/webapp/components/post_info.jsx b/webapp/components/post_info.jsx
index 0aa71edd7..2d41b0e54 100644
--- a/webapp/components/post_info.jsx
+++ b/webapp/components/post_info.jsx
@@ -22,7 +22,7 @@ export default class PostInfo extends React.Component {
}
dropdownPosition(e) {
var position = $('#post-list').height() - $(e.target).offset().top;
- var dropdown = $(e.target).next('.dropdown-menu');
+ var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu');
if (position < dropdown.height()) {
dropdown.addClass('bottom');
}
diff --git a/webapp/components/posts_view.jsx b/webapp/components/posts_view.jsx
index 8b4b0c662..ffe04daa1 100644
--- a/webapp/components/posts_view.jsx
+++ b/webapp/components/posts_view.jsx
@@ -173,24 +173,15 @@ export default class PostsView extends React.Component {
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPost.user_id;
- let prevWebhookName = '';
- if (prevPost.props && prevPost.props.override_username) {
- prevWebhookName = prevPost.props.override_username;
- }
- let curWebhookName = '';
- if (post.props && post.props.override_username) {
- curWebhookName = post.props.override_username;
- }
// consider posts from the same user if:
// the previous post was made by the same user as the current post,
// the previous post was made within 5 minutes of the current post,
- // the previous post and current post are both from webhooks or both not,
- // the previous post and current post have the same webhook usernames
+ // the current post is not from a webhook
+ // the previous post is not from a webhook
if (prevPostUserId === postUserId &&
post.create_at - prevPost.create_at <= 1000 * 60 * 5 &&
- postFromWebhook === prevPostFromWebhook &&
- prevWebhookName === curWebhookName) {
+ !postFromWebhook && !prevPostFromWebhook) {
sameUser = true;
}
@@ -213,13 +204,11 @@ export default class PostsView extends React.Component {
// the previous post was made by the same user as the current post,
// the previous post is not a comment,
// the current post is not a comment,
- // the previous post and current post are both from webhooks or both not,
- // the previous post and current post have the same webhook usernames
+ // the current post is not from a webhook
if (prevPostUserId === postUserId &&
!prevPostIsComment &&
!postIsComment &&
- postFromWebhook === prevPostFromWebhook &&
- prevWebhookName === curWebhookName) {
+ !postFromWebhook) {
hideProfilePic = true;
}
}
@@ -319,7 +308,7 @@ export default class PostsView extends React.Component {
if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
this.scrollToBottom();
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) {
- window.requestAnimationFrame(() => {
+ window.setTimeout(window.requestAnimationFrame(() => {
// If separator exists scroll to it. Otherwise scroll to bottom.
if (this.refs.newMessageSeparator) {
var objDiv = this.refs.postlist;
@@ -327,7 +316,7 @@ export default class PostsView extends React.Component {
} else if (this.refs.postlist) {
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
}
- });
+ }), 0);
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) {
window.requestAnimationFrame(() => {
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
@@ -384,6 +373,8 @@ export default class PostsView extends React.Component {
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
+ this.scrollStopAction.cancel();
+ PreferenceStore.removeChangeListener(this.updateState);
}
componentDidUpdate() {
if (this.props.postList != null) {
@@ -570,7 +561,9 @@ function FloatingTimestamp({isScrolling, post}) {
return (
<div className={className}>
- <span>{dateString}</span>
+ <div>
+ <span>{dateString}</span>
+ </div>
</div>
);
}
diff --git a/webapp/components/posts_view_container.jsx b/webapp/components/posts_view_container.jsx
index 7e334d4b0..a49c77f8d 100644
--- a/webapp/components/posts_view_container.jsx
+++ b/webapp/components/posts_view_container.jsx
@@ -8,7 +8,6 @@ import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import UserStore from 'stores/user_store.jsx';
-import * as Utils from 'utils/utils.jsx';
import * as GlobalActions from 'action_creators/global_actions.jsx';
import Constants from 'utils/constants.jsx';
@@ -158,17 +157,6 @@ export default class PostsViewContainer extends React.Component {
this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
}
}
- shouldComponentUpdate(nextProps, nextState) {
- if (!Utils.areObjectsEqual(this.state, nextState)) {
- return true;
- }
-
- if (!Utils.areObjectsEqual(this.props, nextProps)) {
- return true;
- }
-
- return false;
- }
render() {
const postLists = this.state.postLists;
const channels = this.state.channels;
diff --git a/webapp/components/removed_from_channel_modal.jsx b/webapp/components/removed_from_channel_modal.jsx
index cdd51bd6e..45018ac99 100644
--- a/webapp/components/removed_from_channel_modal.jsx
+++ b/webapp/components/removed_from_channel_modal.jsx
@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
import BrowserStore from 'stores/browser_store.jsx';
-import * as utils from 'utils/utils.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -33,7 +33,7 @@ export default class RemovedFromChannelModal extends React.Component {
}
var townSquare = ChannelStore.getByName('town-square');
- setTimeout(() => utils.switchChannel(townSquare), 1);
+ setTimeout(() => GlobalActions.emitChannelClickEvent(townSquare), 1);
this.setState(newState);
}
diff --git a/webapp/components/rename_channel_modal.jsx b/webapp/components/rename_channel_modal.jsx
index 72828984c..ced3c2d2b 100644
--- a/webapp/components/rename_channel_modal.jsx
+++ b/webapp/components/rename_channel_modal.jsx
@@ -4,7 +4,7 @@
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
@@ -165,8 +165,7 @@ export default class RenameChannelModal extends React.Component {
Client.updateChannel(
channel,
() => {
- AsyncClient.getChannel(channel.id);
- Utils.updateAddressBar(channel.name);
+ GlobalActions.emitChannelClickEvent(channel);
this.handleHide();
},
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 26b392aa1..7a7c5f692 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -217,6 +217,8 @@ export default class RhsRootPost extends React.Component {
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
if (post.props.override_icon_url) {
src = post.props.override_icon_url;
+ } else {
+ src = Constants.DEFAULT_WEBHOOK_LOGO;
}
} else if (Utils.isSystemMessage(post)) {
src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index 9963bc5dd..3b85b23fd 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -69,7 +69,7 @@ export default class Root extends React.Component {
FastClick.attach(document.body);
// Get our localizaiton
- GlobalActions.newLocalizationSelected('en');
+ GlobalActions.loadBrowserLocale();
}
componentWillUnmount() {
LocalizationStore.removeChangeListener(this.localizationChanged);
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 35769d06b..75cbcb2a0 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import $ from 'jquery';
import UserStore from 'stores/user_store.jsx';
import UserProfile from './user_profile.jsx';
import * as GlobalActions from 'action_creators/global_actions.jsx';
@@ -10,28 +9,16 @@ import * as TextFormatting from 'utils/text_formatting.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage, FormattedDate} from 'react-intl';
-
import React from 'react';
+import {Link} from 'react-router';
export default class SearchResultsItem extends React.Component {
constructor(props) {
super(props);
- this.handleClick = this.handleClick.bind(this);
this.handleFocusRHSClick = this.handleFocusRHSClick.bind(this);
}
- handleClick(e) {
- e.preventDefault();
-
- GlobalActions.emitPostFocusEvent(this.props.post.id);
-
- if ($(window).width() < 768) {
- $('.sidebar--right').removeClass('move--left');
- $('.inner-wrap').removeClass('move--left');
- }
- }
-
handleFocusRHSClick(e) {
e.preventDefault();
GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch);
@@ -99,16 +86,15 @@ export default class SearchResultsItem extends React.Component {
</time>
</li>
<li>
- <a
- href='#'
+ <Link
+ to={'/' + window.location.pathname.split('/')[1] + '/pl/' + this.props.post.id}
className='search-item__jump'
- onClick={this.handleClick}
>
<FormattedMessage
id='search_item.jump'
defaultMessage='Jump'
/>
- </a>
+ </Link>
</li>
<li>
<a
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index c0d4755ed..500e73cf2 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -15,7 +15,6 @@ import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
-import * as Client from 'utils/client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
@@ -29,7 +28,7 @@ import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import loadingGif from 'images/load.gif';
import React from 'react';
-import {browserHistory} from 'react-router';
+import {browserHistory, Link} from 'react-router';
import favicon from 'images/favicon/favicon-16x16.png';
import redFavicon from 'images/favicon/redfavicon-16x16.png';
@@ -139,7 +138,9 @@ export default class Sidebar extends React.Component {
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
currentTeam: TeamStore.getCurrent(),
- currentUser: UserStore.getCurrentUser()
+ currentUser: UserStore.getCurrentUser(),
+ townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL),
+ offTopic: ChannelStore.getByName(Constants.OFFTOPIC_CHANNEL)
};
}
@@ -239,7 +240,9 @@ export default class Sidebar extends React.Component {
});
}
- handleLeaveDirectChannel(channel) {
+ handleLeaveDirectChannel(e, channel) {
+ e.preventDefault();
+
if (!this.isLeaving.get(channel.id)) {
this.isLeaving.set(channel.id, true);
@@ -259,7 +262,7 @@ export default class Sidebar extends React.Component {
}
if (channel.id === this.state.activeId) {
- Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+ browserHistory.push('/' + this.state.currentTeam.name + '/channels/town-square');
}
}
@@ -289,6 +292,16 @@ export default class Sidebar extends React.Component {
createTutorialTip() {
const screens = [];
+ let townSquareDisplayName = Constants.DEFAULT_CHANNEL_UI_NAME;
+ if (this.state.townSquare) {
+ townSquareDisplayName = this.state.townSquare.display_name;
+ }
+
+ let offTopicDisplayName = Constants.OFFTOPIC_CHANNEL_UI_NAME;
+ if (this.state.offTopic) {
+ offTopicDisplayName = this.state.offTopic.display_name;
+ }
+
screens.push(
<div>
<FormattedHTMLMessage
@@ -302,10 +315,14 @@ export default class Sidebar extends React.Component {
<div>
<FormattedHTMLMessage
id='sidebar.tutorialScreen2'
- defaultMessage='<h4>"Town Square" and "Off-Topic" channels</h4>
+ defaultMessage='<h4>"{townsquare}" and "{offtopic}" channels</h4>
<p>Here are two public channels to start:</p>
- <p><strong>Town Square</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p>
- <p><strong>Off-Topic</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>'
+ <p><strong>{townsquare}</strong> is a place for team-wide communication. Everyone in your team is a member of this channel.</p>
+ <p><strong>{offtopic}</strong> is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.</p>'
+ values={{
+ townsquare: townSquareDisplayName,
+ offtopic: offTopicDisplayName
+ }}
/>
</div>
);
@@ -406,48 +423,6 @@ export default class Sidebar extends React.Component {
icon = <div className='status'><i className='fa fa-lock'></i></div>;
}
- // set up click handler to switch channels (or create a new channel for non-existant ones)
- var handleClick = null;
-
- if (!channel.fake) {
- handleClick = function clickHandler(e) {
- if (e.target.attributes.getNamedItem('data-close')) {
- handleClose(channel);
- } else {
- Utils.switchChannel(channel);
- }
-
- e.preventDefault();
- };
- } else if (channel.fake) {
- // It's a direct message channel that doesn't exist yet so let's create it now
- var otherUserId = Utils.getUserIdFromChannelName(channel);
-
- if (this.state.loadingDMChannel === -1) {
- handleClick = function clickHandler(e) {
- e.preventDefault();
-
- if (e.target.attributes.getNamedItem('data-close')) {
- handleClose(channel);
- } else {
- this.setState({loadingDMChannel: index});
-
- Client.createDirectChannel(channel, otherUserId,
- (data) => {
- this.setState({loadingDMChannel: -1});
- AsyncClient.getChannel(data.id);
- Utils.switchChannel(data);
- },
- () => {
- this.setState({loadingDMChannel: -1});
- browserHistory('/' + this.state.currentTeam.name);
- }
- );
- }
- }.bind(this);
- }
- }
-
let closeButton = null;
const removeTooltip = (
<Tooltip id='remove-dm-tooltip'>
@@ -464,12 +439,12 @@ export default class Sidebar extends React.Component {
placement='top'
overlay={removeTooltip}
>
- <span
- className='btn-close'
- data-close='true'
- >
- {'×'}
- </span>
+ <span
+ onClick={(e) => handleClose(e, channel)}
+ className='btn-close'
+ >
+ {'×'}
+ </span>
</OverlayTrigger>
);
@@ -481,23 +456,29 @@ export default class Sidebar extends React.Component {
tutorialTip = this.createTutorialTip();
}
+ let link = '';
+ if (channel.fake) {
+ link = '/' + this.state.currentTeam.name + '/channels/' + channel.name + '?fakechannel=' + encodeURIComponent(JSON.stringify(channel));
+ } else {
+ link = '/' + this.state.currentTeam.name + '/channels/' + channel.name;
+ }
+
return (
<li
key={channel.name}
ref={channel.name}
className={linkClass}
>
- <a
+ <Link
+ to={link}
className={rowClass}
- href={'#'}
- onClick={handleClick}
>
{icon}
{status}
{channel.display_name}
{badge}
{closeButton}
- </a>
+ </Link>
{tutorialTip}
</li>
);
@@ -600,6 +581,7 @@ export default class Sidebar extends React.Component {
<div
className='sidebar--left'
id='sidebar-left'
+ key='sidebar-left'
>
<NewChannelFlow
show={showChannelModal}
diff --git a/webapp/components/sidebar_right.jsx b/webapp/components/sidebar_right.jsx
index a2e3914f3..594674929 100644
--- a/webapp/components/sidebar_right.jsx
+++ b/webapp/components/sidebar_right.jsx
@@ -29,7 +29,7 @@ export default class SidebarRight extends React.Component {
this.doStrangeThings = this.doStrangeThings.bind(this);
this.state = {
- searchVisible: !!SearchStore.getSearchResults(),
+ searchVisible: SearchStore.getSearchResults() !== null,
isMentionSearch: SearchStore.getIsMentionSearch(),
postRightVisible: !!PostStore.getSelectedPost(),
fromSearch: false,
@@ -111,7 +111,7 @@ export default class SidebarRight extends React.Component {
}
onSearchChange() {
this.setState({
- searchVisible: !!SearchStore.getSearchResults(),
+ searchVisible: SearchStore.getSearchResults() !== null,
isMentionSearch: SearchStore.getIsMentionSearch()
});
}
diff --git a/webapp/components/signup_team.jsx b/webapp/components/signup_team.jsx
index e6b27e745..3ad47500d 100644
--- a/webapp/components/signup_team.jsx
+++ b/webapp/components/signup_team.jsx
@@ -138,6 +138,24 @@ export default class TeamSignUp extends React.Component {
}
let signupMethod = null;
+ let goBack = (
+ <div className='signup-header'>
+ <a
+ href='#'
+ onClick={
+ (e) => {
+ e.preventDefault();
+ this.updatePage('choose');
+ }
+ }
+ >
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.back'
+ />
+ </a>
+ </div>
+ );
if (global.window.mm_config.EnableTeamCreation !== 'true') {
if (teamListing == null) {
@@ -154,9 +172,12 @@ export default class TeamSignUp extends React.Component {
updatePage={this.updatePage}
/>
);
+ goBack = null;
} else if (this.state.page === 'email') {
signupMethod = (
- <EmailSignUpPage/>
+ <div>
+ <EmailSignUpPage/>
+ </div>
);
} else if (this.state.page === 'ldap') {
return (
@@ -180,24 +201,28 @@ export default class TeamSignUp extends React.Component {
defaultMessage='No team creation method has been enabled. Please contact an administrator for access.'
/>
);
+ goBack = null;
}
return (
- <div className='col-sm-12'>
- <div className='signup-team__container'>
- <img
- className='signup-team-logo'
- src={logoImage}
- />
- <h1>{global.window.mm_config.SiteName}</h1>
- <h4 className='color--light'>
- <FormattedMessage
- id='web.root.singup_info'
+ <div>
+ {goBack}
+ <div className='col-sm-12'>
+ <div className='signup-team__container'>
+ <img
+ className='signup-team-logo'
+ src={logoImage}
/>
- </h4>
- <div id='signup-team'>
- {teamListing}
- {signupMethod}
+ <h1>{global.window.mm_config.SiteName}</h1>
+ <h4 className='color--light'>
+ <FormattedMessage
+ id='web.root.singup_info'
+ />
+ </h4>
+ <div id='signup-team'>
+ {teamListing}
+ {signupMethod}
+ </div>
</div>
</div>
</div>
diff --git a/webapp/components/signup_team_complete/components/signup_team_complete.jsx b/webapp/components/signup_team_complete/components/signup_team_complete.jsx
index 8b2096499..e5d310151 100644
--- a/webapp/components/signup_team_complete/components/signup_team_complete.jsx
+++ b/webapp/components/signup_team_complete/components/signup_team_complete.jsx
@@ -8,7 +8,7 @@ import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
import React from 'react';
-import Link from 'react-router';
+import {Link} from 'react-router';
export default class SignupTeamComplete extends React.Component {
constructor(props) {
@@ -55,7 +55,7 @@ export default class SignupTeamComplete extends React.Component {
<div>
<div className='signup-header'>
<Link to='/'>
- <span classNameName='fa fa-chevron-left'/>
+ <span className='fa fa-chevron-left'/>
<FormattedMessage id='web.header.back'/>
</Link>
</div>
diff --git a/webapp/components/signup_user_complete.jsx b/webapp/components/signup_user_complete.jsx
index 5460daf29..e9f9d9d88 100644
--- a/webapp/components/signup_user_complete.jsx
+++ b/webapp/components/signup_user_complete.jsx
@@ -1,18 +1,21 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import ReactDOM from 'react-dom';
+import LoadingScreen from 'components/loading_screen.jsx';
+import LoginLdap from 'components/login/components/login_ldap.jsx';
+
+import BrowserStore from 'stores/browser_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
import * as Utils from 'utils/utils.jsx';
import * as Client from 'utils/client.jsx';
-import UserStore from 'stores/user_store.jsx';
-import BrowserStore from 'stores/browser_store.jsx';
import Constants from 'utils/constants.jsx';
-import LoadingScreen from 'components/loading_screen.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router';
import React from 'react';
+import ReactDOM from 'react-dom';
import logoImage from 'images/logo.png';
@@ -314,13 +317,13 @@ class SignupUserComplete extends React.Component {
</div>
);
- var signupMessage = [];
+ let signupMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
signupMessage.push(
- <Link
+ <a
className='btn btn-custom-login gitlab'
key='gitlab'
- to={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
+ href={'/api/v1/oauth/gitlab/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
>
<span className='icon'/>
<span>
@@ -329,16 +332,16 @@ class SignupUserComplete extends React.Component {
defaultMessage='with GitLab'
/>
</span>
- </Link>
+ </a>
);
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
signupMessage.push(
- <Link
+ <a
className='btn btn-custom-login google'
key='google'
- to={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
+ href={'/api/v1/oauth/google/signup' + window.location.search + '&team=' + encodeURIComponent(this.state.teamName)}
>
<span className='icon'/>
<span>
@@ -347,11 +350,26 @@ class SignupUserComplete extends React.Component {
defaultMessage='with Google'
/>
</span>
- </Link>
+ </a>
);
}
- var emailSignup;
+ let ldapSignup;
+ if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) {
+ ldapSignup = (
+ <div className='inner__content'>
+ <h5><strong>
+ <FormattedMessage
+ id='signup_user_completed.withLdap'
+ defaultMessage='With your LDAP credentials'
+ />
+ </strong></h5>
+ <LoginLdap teamName={this.state.teamName}/>
+ </div>
+ );
+ }
+
+ let emailSignup;
if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
emailSignup = (
<div>
@@ -397,24 +415,24 @@ class SignupUserComplete extends React.Component {
{passwordError}
</div>
</div>
+ <p className='margin--extra'>
+ <button
+ type='submit'
+ onClick={this.handleSubmit}
+ className='btn-primary btn'
+ >
+ <FormattedMessage
+ id='signup_user_completed.create'
+ defaultMessage='Create Account'
+ />
+ </button>
+ </p>
</div>
- <p className='margin--extra'>
- <button
- type='submit'
- onClick={this.handleSubmit}
- className='btn-primary btn'
- >
- <FormattedMessage
- id='signup_user_completed.create'
- defaultMessage='Create Account'
- />
- </button>
- </p>
</div>
);
}
- if (signupMessage.length > 0 && emailSignup) {
+ if (signupMessage.length > 0 && (emailSignup || ldapSignup)) {
signupMessage = (
<div>
{signupMessage}
@@ -428,7 +446,21 @@ class SignupUserComplete extends React.Component {
);
}
- if (signupMessage.length === 0 && !emailSignup) {
+ if (ldapSignup && emailSignup) {
+ ldapSignup = (
+ <div>
+ {ldapSignup}
+ <div className='or__container'>
+ <FormattedMessage
+ id='signup_user_completed.or'
+ defaultMessage='or'
+ />
+ </div>
+ </div>
+ );
+ }
+
+ if (signupMessage.length === 0 && !emailSignup && !ldapSignup) {
emailSignup = (
<div>
<FormattedMessage
@@ -449,7 +481,7 @@ class SignupUserComplete extends React.Component {
</div>
<div className='col-sm-12'>
<div className='signup-team__container padding--less'>
- <form>
+ <div>
<img
className='signup-team-logo'
src={logoImage}
@@ -477,9 +509,10 @@ class SignupUserComplete extends React.Component {
/>
</h4>
{signupMessage}
+ {ldapSignup}
{emailSignup}
{serverError}
- </form>
+ </div>
</div>
</div>
</div>
diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx
index fcc9af8cd..becf395c5 100644
--- a/webapp/components/spinner_button.jsx
+++ b/webapp/components/spinner_button.jsx
@@ -14,20 +14,10 @@ export default class SpinnerButton extends React.Component {
};
}
- constructor(props) {
- super(props);
-
- this.handleClick = this.handleClick.bind(this);
- }
-
- handleClick(e) {
- if (this.props.onClick) {
- this.props.onClick(e);
- }
- }
-
render() {
- if (this.props.spinning) {
+ const {spinning, children, ...props} = this.props; // eslint-disable-line no-use-before-define
+
+ if (spinning) {
return (
<img
className='spinner-button__gif'
@@ -38,10 +28,10 @@ export default class SpinnerButton extends React.Component {
return (
<button
- onClick={this.handleClick}
- className='btn btn-sm btn-primary'
+ className='btn btn-primary'
+ {...props}
>
- {this.props.children}
+ {children}
</button>
);
}
diff --git a/webapp/components/suggestion/search_suggestion_list.jsx b/webapp/components/suggestion/search_suggestion_list.jsx
index b15cc4243..57aaee8ff 100644
--- a/webapp/components/suggestion/search_suggestion_list.jsx
+++ b/webapp/components/suggestion/search_suggestion_list.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import $ from 'jquery';
+import React from 'react';
import ReactDOM from 'react-dom';
import Constants from 'utils/constants.jsx';
import SuggestionList from './suggestion_list.jsx';
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 776c84b48..c27e8ca59 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -84,6 +84,7 @@ class GeneralTab extends React.Component {
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.setState(this.setupInitialState(this.props));
this.props.updateSection(section);
}
diff --git a/webapp/components/team_settings_modal.jsx b/webapp/components/team_settings_modal.jsx
index c19787993..657643367 100644
--- a/webapp/components/team_settings_modal.jsx
+++ b/webapp/components/team_settings_modal.jsx
@@ -62,6 +62,7 @@ class TeamSettingsModal extends React.Component {
}
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.setState({activeSection: section});
}
render() {
diff --git a/webapp/components/team_signup_with_sso.jsx b/webapp/components/team_signup_with_sso.jsx
index 9a46b2d6b..78396eea8 100644
--- a/webapp/components/team_signup_with_sso.jsx
+++ b/webapp/components/team_signup_with_sso.jsx
@@ -64,7 +64,7 @@ class SSOSignUpPage extends React.Component {
this.props.service,
(data) => {
if (data.follow_link) {
- browserHistory.push(data.follow_link);
+ window.location.href = data.follow_link;
} else {
browserHistory.push('/' + team.name + '/channels/town-square');
}
diff --git a/webapp/components/textbox.jsx b/webapp/components/textbox.jsx
index 371c581e5..c77e1f9a3 100644
--- a/webapp/components/textbox.jsx
+++ b/webapp/components/textbox.jsx
@@ -130,8 +130,8 @@ export default class Textbox extends React.Component {
const helpText = (
<div
- style={{visibility: hasText ? 'visible' : 'hidden', opacity: hasText ? '0.5' : '0'}}
- className='help_format_text'
+ style={{visibility: hasText ? 'visible' : 'hidden', opacity: hasText ? '0.3' : '0'}}
+ className='help__format-text'
>
<b>
<FormattedMessage
@@ -208,8 +208,8 @@ export default class Textbox extends React.Component {
dangerouslySetInnerHTML={{__html: this.state.preview ? TextFormatting.formatText(this.props.messageText) : ''}}
>
</div>
- {helpText}
<div className='help__text'>
+ {helpText}
{previewLink}
<a
target='_blank'
diff --git a/webapp/components/tutorial/tutorial_intro_screens.jsx b/webapp/components/tutorial/tutorial_intro_screens.jsx
index 913a30483..0358a6a65 100644
--- a/webapp/components/tutorial/tutorial_intro_screens.jsx
+++ b/webapp/components/tutorial/tutorial_intro_screens.jsx
@@ -2,7 +2,6 @@
// See License.txt for license information.
import UserStore from 'stores/user_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -11,6 +10,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {browserHistory} from 'react-router';
const Preferences = Constants.Preferences;
@@ -19,6 +19,12 @@ const NUM_SCREENS = 3;
import React from 'react';
export default class TutorialIntroScreens extends React.Component {
+ static get propTypes() {
+ return {
+ townSquare: React.PropTypes.object,
+ offTopic: React.PropTypes.object
+ };
+ }
constructor(props) {
super(props);
@@ -34,7 +40,7 @@ export default class TutorialIntroScreens extends React.Component {
return;
}
- Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL));
+ browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
const step = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 0);
@@ -52,6 +58,8 @@ export default class TutorialIntroScreens extends React.Component {
UserStore.getCurrentId(),
'999'
);
+
+ browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
}
createScreen() {
switch (this.state.currentScreen) {
@@ -151,6 +159,11 @@ export default class TutorialIntroScreens extends React.Component {
);
}
+ let townSquareDisplayName = Constants.DEFAULT_CHANNEL_UI_NAME;
+ if (this.props.townSquare) {
+ townSquareDisplayName = this.props.townSquare.display_name;
+ }
+
return (
<div>
<h3>
@@ -169,7 +182,10 @@ export default class TutorialIntroScreens extends React.Component {
{supportInfo}
<FormattedMessage
id='tutorial_intro.end'
- defaultMessage='Click “Next” to enter Town Square. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.'
+ defaultMessage='Click “Next” to enter {channel}. This is the first channel teammates see when they sign up. Use it for posting updates everyone needs to know.'
+ values={{
+ channel: townSquareDisplayName
+ }}
/>
{circles}
</div>
diff --git a/webapp/components/tutorial/tutorial_tip.jsx b/webapp/components/tutorial/tutorial_tip.jsx
index 3508e29a2..deca70794 100644
--- a/webapp/components/tutorial/tutorial_tip.jsx
+++ b/webapp/components/tutorial/tutorial_tip.jsx
@@ -15,6 +15,9 @@ import {Overlay} from 'react-bootstrap';
import React from 'react';
+import tutorialGif from 'images/tutorialTip.gif';
+import tutorialGifWhite from 'images/tutorialTipWhite.gif';
+
export default class TutorialTip extends React.Component {
constructor(props) {
super(props);
@@ -90,16 +93,16 @@ export default class TutorialTip extends React.Component {
}
}
- var tipColor = '';
+ var tutorialGifImage = tutorialGif;
if (this.props.overlayClass === 'tip-overlay--header' || this.props.overlayClass === 'tip-overlay--sidebar') {
- tipColor = 'White';
+ tutorialGifImage = tutorialGifWhite;
}
return (
<div className={'tip-div ' + this.props.overlayClass}>
<img
className='tip-button'
- src={'/static/images/tutorialTip' + tipColor + '.gif'}
+ src={tutorialGifImage}
width='35'
onClick={this.toggle}
ref='target'
diff --git a/webapp/components/tutorial/tutorial_view.jsx b/webapp/components/tutorial/tutorial_view.jsx
new file mode 100644
index 000000000..5f2c1a257
--- /dev/null
+++ b/webapp/components/tutorial/tutorial_view.jsx
@@ -0,0 +1,44 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import TutorialIntroScreens from './tutorial_intro_screens.jsx';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import Constants from 'utils/constants.jsx';
+
+import React from 'react';
+
+export default class TutorialView extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChannelChange = this.handleChannelChange.bind(this);
+
+ this.state = {
+ townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL)
+ };
+ }
+ componentDidMount() {
+ ChannelStore.addChangeListener(this.handleChannelChange);
+ }
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.handleChannelChange);
+ }
+ handleChannelChange() {
+ this.setState({
+ townSquare: ChannelStore.getByName(Constants.DEFAULT_CHANNEL)
+ });
+ }
+ render() {
+ return (
+ <div
+ id='app-content'
+ className='app__content'
+ >
+ <TutorialIntroScreens
+ townSquare={this.state.townSquare}
+ />
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/user_settings/manage_incoming_hooks.jsx b/webapp/components/user_settings/manage_incoming_hooks.jsx
deleted file mode 100644
index b61b331ce..000000000
--- a/webapp/components/user_settings/manage_incoming_hooks.jsx
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as Client from 'utils/client.jsx';
-import * as Utils from 'utils/utils.jsx';
-import Constants from 'utils/constants.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-import LoadingScreen from '../loading_screen.jsx';
-
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-import React from 'react';
-
-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() {
- const hook = {};
- 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) {
- const data = {};
- data.id = id;
-
- Client.deleteIncomingHook(
- data,
- () => {
- const hooks = this.state.hooks;
- 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) => {
- const state = this.state;
-
- 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();
- const options = [];
- channels.forEach((channel) => {
- if (channel.type !== Constants.DM_CHANNEL) {
- options.push(
- <option
- key={'incoming-hook' + channel.id}
- value={channel.id}
- >
- {channel.display_name}
- </option>
- );
- }
- });
-
- let disableButton = '';
- if (this.state.channelId === '') {
- disableButton = ' disable';
- }
-
- const hooks = [];
- this.state.hooks.forEach((hook) => {
- const c = ChannelStore.get(hook.channel_id);
- if (c) {
- hooks.push(
- <div
- key={hook.id}
- className='webhook__item'
- >
- <div className='padding-top x2 webhook__url'>
- <strong>{'URL: '}</strong>
- <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
- </div>
- <div className='padding-top'>
- <strong>
- <FormattedMessage
- id='user.settings.hooks_in.channel'
- defaultMessage='Channel: '
- />
- </strong>{c.display_name}
- </div>
- <a
- className={'webhook__remove'}
- href='#'
- onClick={this.removeHook.bind(this, hook.id)}
- >
- <span aria-hidden='true'>{'×'}</span>
- </a>
- <div className='padding-top x2 divider-light'></div>
- </div>
- );
- }
- });
-
- let displayHooks;
- if (!this.state.getHooksComplete) {
- displayHooks = <LoadingScreen/>;
- } else if (hooks.length > 0) {
- displayHooks = hooks;
- } else {
- displayHooks = (
- <div className='padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_in.none'
- defaultMessage='None'
- />
- </div>
- );
- }
-
- const existingHooks = (
- <div className='webhooks__container'>
- <label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_in.existing'
- defaultMessage='Existing incoming webhooks'
- />
- </label>
- <div className='padding-top divider-light'></div>
- <div className='webhooks__list'>
- {displayHooks}
- </div>
- </div>
- );
-
- return (
- <div key='addIncomingHook'>
- <FormattedHTMLMessage
- id='user.settings.hooks_in.description'
- defaultMessage='Create webhook URLs for use in external integrations. Please see <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">incoming webhooks documentation</a> to learn more. View all incoming webhooks configured on this team below.'
- />
- <div><label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_in.addTitle'
- defaultMessage='Add a new incoming webhook'
- />
- </label></div>
- <div className='row padding-top'>
- <div className='col-sm-10 padding-bottom'>
- <select
- ref='channelName'
- className='form-control'
- value={this.state.channelId}
- onChange={this.updateChannelId}
- >
- {options}
- </select>
- {serverError}
- </div>
- <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'>
- <a
- className={'btn form-control no-padding btn-sm btn-primary' + disableButton}
- href='#'
- onClick={this.addNewHook}
- >
- <FormattedMessage
- id='user.settings.hooks_in.add'
- defaultMessage='Add'
- />
- </a>
- </div>
- </div>
- {existingHooks}
- </div>
- );
- }
-}
diff --git a/webapp/components/user_settings/manage_outgoing_hooks.jsx b/webapp/components/user_settings/manage_outgoing_hooks.jsx
deleted file mode 100644
index 8adec09ce..000000000
--- a/webapp/components/user_settings/manage_outgoing_hooks.jsx
+++ /dev/null
@@ -1,397 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import LoadingScreen from '../loading_screen.jsx';
-
-import ChannelStore from 'stores/channel_store.jsx';
-
-import * as Client from 'utils/client.jsx';
-import Constants from 'utils/constants.jsx';
-
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-const holders = defineMessages({
- optional: {
- id: 'user.settings.hooks_out.optional',
- defaultMessage: 'Optional if channel selected'
- },
- callbackHolder: {
- id: 'user.settings.hooks_out.callbackHolder',
- defaultMessage: 'Each URL must start with http:// or https://'
- },
- select: {
- id: 'user.settings.hooks_out.select',
- defaultMessage: '--- Select a channel ---'
- }
-});
-
-import React from 'react';
-
-class ManageOutgoingHooks extends React.Component {
- constructor() {
- super();
-
- this.getHooks = this.getHooks.bind(this);
- this.addNewHook = this.addNewHook.bind(this);
- this.updateChannelId = this.updateChannelId.bind(this);
- this.updateTriggerWords = this.updateTriggerWords.bind(this);
- this.updateCallbackURLs = this.updateCallbackURLs.bind(this);
-
- this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false};
- }
- componentDidMount() {
- this.getHooks();
- }
- addNewHook(e) {
- e.preventDefault();
-
- if ((this.state.channelId === '' && this.state.triggerWords === '') ||
- this.state.callbackURLs === '') {
- return;
- }
-
- const hook = {};
- hook.channel_id = this.state.channelId;
- if (this.state.triggerWords.length !== 0) {
- hook.trigger_words = this.state.triggerWords.trim().split(',');
- }
- hook.callback_urls = this.state.callbackURLs.split('\n').map((url) => url.trim());
-
- Client.addOutgoingHook(
- hook,
- (data) => {
- let hooks = Object.assign([], this.state.hooks);
- if (!hooks) {
- hooks = [];
- }
- hooks.push(data);
- this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''});
- },
- (err) => {
- this.setState({addError: err.message});
- }
- );
- }
- removeHook(id) {
- const data = {};
- data.id = id;
-
- Client.deleteOutgoingHook(
- data,
- () => {
- const hooks = this.state.hooks;
- 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({editError: err.message});
- }
- );
- }
- regenToken(id) {
- const regenData = {};
- regenData.id = id;
-
- Client.regenOutgoingHookToken(
- regenData,
- (data) => {
- const hooks = Object.assign([], this.state.hooks);
- for (let i = 0; i < hooks.length; i++) {
- if (hooks[i].id === id) {
- hooks[i] = data;
- break;
- }
- }
-
- this.setState({hooks, editError: null});
- },
- (err) => {
- this.setState({editError: err.message});
- }
- );
- }
- getHooks() {
- Client.listOutgoingHooks(
- (data) => {
- if (data) {
- this.setState({hooks: data, getHooksComplete: true, editError: null});
- }
- },
- (err) => {
- this.setState({editError: err.message});
- }
- );
- }
- updateChannelId(e) {
- this.setState({channelId: e.target.value});
- }
- updateTriggerWords(e) {
- this.setState({triggerWords: e.target.value});
- }
- updateCallbackURLs(e) {
- this.setState({callbackURLs: e.target.value});
- }
- render() {
- let addError;
- if (this.state.addError) {
- addError = <label className='has-error'>{this.state.addError}</label>;
- }
- let editError;
- if (this.state.editError) {
- addError = <label className='has-error'>{this.state.editError}</label>;
- }
-
- const channels = ChannelStore.getAll();
- const options = [];
- options.push(
- <option
- key='select-channel'
- value=''
- >
- {this.props.intl.formatMessage(holders.select)}
- </option>
- );
-
- channels.forEach((channel) => {
- if (channel.type === Constants.OPEN_CHANNEL) {
- options.push(
- <option
- key={'outgoing-hook' + channel.id}
- value={channel.id}
- >
- {channel.display_name}
- </option>
- );
- }
- });
-
- const hooks = [];
- this.state.hooks.forEach((hook) => {
- const c = ChannelStore.get(hook.channel_id);
-
- if (!c && hook.channel_id && hook.channel_id.length !== 0) {
- return;
- }
-
- let channelDiv;
- if (c) {
- channelDiv = (
- <div className='padding-top'>
- <strong>
- <FormattedMessage
- id='user.settings.hooks_out.channel'
- defaultMessage='Channel: '
- />
- </strong>{c.display_name}
- </div>
- );
- }
-
- let triggerDiv;
- if (hook.trigger_words && hook.trigger_words.length !== 0) {
- triggerDiv = (
- <div className='padding-top'>
- <strong>
- <FormattedMessage
- id='user.settings.hooks_out.trigger'
- defaultMessage='Trigger Words: '
- />
- </strong>{hook.trigger_words.join(', ')}
- </div>
- );
- }
-
- hooks.push(
- <div
- key={hook.id}
- className='webhook__item'
- >
- <div className='padding-top x2 webhook__url'>
- <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
- </div>
- {channelDiv}
- {triggerDiv}
- <div className='padding-top'>
- <strong>{'Token: '}</strong>{hook.token}
- </div>
- <div className='padding-top'>
- <a
- className='text-danger'
- href='#'
- onClick={this.regenToken.bind(this, hook.id)}
- >
- <FormattedMessage
- id='user.settings.hooks_out.regen'
- defaultMessage='Regen Token'
- />
- </a>
- <a
- className='webhook__remove'
- href='#'
- onClick={this.removeHook.bind(this, hook.id)}
- >
- <span aria-hidden='true'>{'×'}</span>
- </a>
- </div>
- <div className='padding-top x2 divider-light'></div>
- </div>
- );
- });
-
- let displayHooks;
- if (!this.state.getHooksComplete) {
- displayHooks = <LoadingScreen/>;
- } else if (hooks.length > 0) {
- displayHooks = hooks;
- } else {
- displayHooks = (
- <div className='padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_out.none'
- defaultMessage='None'
- />
- </div>
- );
- }
-
- const existingHooks = (
- <div className='webhooks__container'>
- <label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_out.existing'
- defaultMessage='Existing outgoing webhooks'
- />
- </label>
- <div className='padding-top divider-light'></div>
- <div className='webhooks__list'>
- {displayHooks}
- </div>
- </div>
- );
-
- const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === '';
-
- return (
- <div key='addOutgoingHook'>
- <FormattedHTMLMessage
- id='user.settings.hooks_out.addDescription'
- defaultMessage='Create webhooks to send new message events to an external integration. Please see <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">outgoing webhooks documentation</a> to learn more. View all outgoing webhooks configured on this team below.'
- />
- <div><label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_out.addTitle'
- defaultMessage='Add a new outgoing webhook'
- />
- </label></div>
- <div className='padding-top divider-light'></div>
- <div className='padding-top'>
- <div>
- <label className='control-label'>
- <FormattedMessage
- id='user.settings.hooks_out.channel'
- defaultMessage='Channel: '
- />
- </label>
- <div className='padding-top'>
- <select
- ref='channelName'
- className='form-control'
- value={this.state.channelId}
- onChange={this.updateChannelId}
- >
- {options}
- </select>
- </div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.hooks_out.only'
- defaultMessage='Only public channels can be used'
- />
- </div>
- </div>
- <div className='padding-top x2'>
- <label className='control-label'>
- <FormattedMessage
- id='user.settings.hooks_out.trigger'
- defaultMessage='Trigger Words: '
- />
- </label>
- <div className='padding-top'>
- <input
- ref='triggerWords'
- className='form-control'
- value={this.state.triggerWords}
- onChange={this.updateTriggerWords}
- placeholder={this.props.intl.formatMessage(holders.optional)}
- />
- </div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.hooks_out.comma'
- defaultMessage='Comma separated words to trigger on'
- />
- </div>
- </div>
- <div className='padding-top x2'>
- <label className='control-label'>
- <FormattedMessage
- id='user.settings.hooks_out.callback'
- defaultMessage='Callback URLs: '
- />
- </label>
- <div className='padding-top'>
- <textarea
- ref='callbackURLs'
- className='form-control no-resize'
- value={this.state.callbackURLs}
- resize={false}
- rows={3}
- onChange={this.updateCallbackURLs}
- placeholder={this.props.intl.formatMessage(holders.callbackHolder)}
- />
- </div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.hooks_out.callbackDesc'
- defaultMessage='New line separated URLs that will receive the HTTP POST event'
- />
- </div>
- {addError}
- </div>
- <div className='padding-top padding-bottom'>
- <a
- className={'btn btn-sm btn-primary'}
- href='#'
- disabled={disableButton}
- onClick={this.addNewHook}
- >
- <FormattedMessage
- id='user.settings.hooks_out.add'
- defaultMessage='Add'
- />
- </a>
- </div>
- </div>
- {existingHooks}
- {editError}
- </div>
- );
- }
-}
-
-ManageOutgoingHooks.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(ManageOutgoingHooks);
diff --git a/webapp/components/user_settings/premade_theme_chooser.jsx b/webapp/components/user_settings/premade_theme_chooser.jsx
index c35748b41..326120957 100644
--- a/webapp/components/user_settings/premade_theme_chooser.jsx
+++ b/webapp/components/user_settings/premade_theme_chooser.jsx
@@ -7,6 +7,8 @@ import Constants from 'utils/constants.jsx';
import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
export default class PremadeThemeChooser extends React.Component {
constructor(props) {
super(props);
@@ -50,6 +52,17 @@ export default class PremadeThemeChooser extends React.Component {
return (
<div className='row appearance-section'>
{premadeThemes}
+ <div className='col-sm-12 padding-bottom x2'>
+ <a
+ href='http://docs.mattermost.com/help/settings/theme-colors.html#custom-themes'
+ target='_blank'
+ >
+ <FormattedMessage
+ id='user.settings.display.theme.otherThemes'
+ defaultMessage='See other themes'
+ />
+ </a>
+ </div>
</div>
);
}
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 4fcdc9a41..61e0e1dad 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import * as AsyncClient from 'utils/async_client.jsx';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
@@ -151,6 +152,7 @@ class AdvancedSettingsDisplay extends React.Component {
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.props.updateSection(section);
}
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index e56156049..d169e01b5 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import ManageLanguages from './manage_languages.jsx';
@@ -83,6 +84,7 @@ export default class UserSettingsDisplay extends React.Component {
this.setState({selectedFont});
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.updateState();
this.props.updateSection(section);
}
@@ -302,7 +304,7 @@ export default class UserSettingsDisplay extends React.Component {
describe = (
<FormattedMessage
id='user.settings.display.showUsername'
- defaultMessage='Show username (team default)'
+ defaultMessage='Show username (default)'
/>
);
} else if (this.state.nameFormat === 'full_name') {
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index 2129847aa..eddbc1efe 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import SettingPicture from '../setting_picture.jsx';
@@ -13,7 +14,7 @@ import Constants from 'utils/constants.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'react-intl';
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
const holders = defineMessages({
usernameReserved: {
@@ -36,18 +37,6 @@ const holders = defineMessages({
id: 'user.settings.general.checkEmail',
defaultMessage: 'Check your email at {email} to verify the address.'
},
- newAddress: {
- id: 'user.settings.general.newAddress',
- defaultMessage: 'New Address: {email}<br />Check your email to verify the above address.'
- },
- checkEmailNoAddress: {
- id: 'user.settings.general.checkEmailNoAddress',
- defaultMessage: 'Check your email to verify your new address'
- },
- loginGitlab: {
- id: 'user.settings.general.loginGitlab',
- defaultMessage: 'Log in done through GitLab'
- },
validImage: {
id: 'user.settings.general.validImage',
defaultMessage: 'Only JPG or PNG images may be used for profile pictures'
@@ -72,10 +61,6 @@ const holders = defineMessages({
id: 'user.settings.general.username',
defaultMessage: 'Username'
},
- email: {
- id: 'user.settings.general.email',
- defaultMessage: 'Email'
- },
profilePicture: {
id: 'user.settings.general.profilePicture',
defaultMessage: 'Profile Picture'
@@ -286,6 +271,7 @@ class UserSettingsGeneralTab extends React.Component {
}
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
const emailChangeInProgress = this.state.emailChangeInProgress;
this.setState(Object.assign({}, this.setupInitialState(this.props), {emailChangeInProgress, clientError: '', serverError: '', emailError: ''}));
this.submitActive = false;
@@ -297,9 +283,224 @@ class UserSettingsGeneralTab extends React.Component {
return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname,
email: user.email, confirmEmail: '', picture: null, loadingPicture: false, emailChangeInProgress: false};
}
+ createEmailSection() {
+ let emailSection;
+
+ if (this.props.activeSection === 'email') {
+ const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true';
+ const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true';
+ const inputs = [];
+
+ let helpText = (
+ <FormattedMessage
+ id='user.settings.general.emailHelp1'
+ defaultMessage='Email is used for sign-in, notifications, and password reset. Email requires verification if changed.'
+ />
+ );
+
+ if (!emailEnabled) {
+ helpText = (
+ <div className='setting-list__hint text-danger'>
+ <FormattedMessage
+ id='user.settings.general.emailHelp2'
+ defaultMessage='Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'
+ />
+ </div>
+ );
+ } else if (!emailVerificationEnabled) {
+ helpText = (
+ <FormattedMessage
+ id='user.settings.general.emailHelp3'
+ defaultMessage='Email is used for sign-in, notifications, and password reset.'
+ />
+ );
+ } else if (this.state.emailChangeInProgress) {
+ const newEmail = UserStore.getCurrentUser().email;
+ if (newEmail) {
+ helpText = (
+ <FormattedMessage
+ id='user.settings.general.emailHelp4'
+ defaultMessage='A verification email was sent to {email}.'
+ values={{
+ email: newEmail
+ }}
+ />
+ );
+ }
+ }
+
+ let submit = null;
+
+ if (this.props.user.auth_service === '') {
+ inputs.push(
+ <div key='emailSetting'>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.general.primaryEmail'
+ defaultMessage='Primary Email'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateEmail}
+ value={this.state.email}
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ inputs.push(
+ <div key='confirmEmailSetting'>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.general.confirmEmail'
+ defaultMessage='Confirm Email'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateConfirmEmail}
+ value={this.state.confirmEmail}
+ />
+ </div>
+ </div>
+ {helpText}
+ </div>
+ );
+
+ submit = this.submitEmail;
+ } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.general.emailGitlabCantUpdate'
+ defaultMessage='Login occurs through GitLab. Email cannot be updated. Email address used for notifications is {email}.'
+ values={{
+ email: this.state.email
+ }}
+ />
+ </div>
+ {helpText}
+ </div>
+ );
+ } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.general.emailLdapCantUpdate'
+ defaultMessage='Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.'
+ values={{
+ email: this.state.email
+ }}
+ />
+ </div>
+ {helpText}
+ </div>
+ );
+ }
+
+ emailSection = (
+ <SettingItemMax
+ title={
+ <FormattedMessage
+ id='user.settings.general.email'
+ defaultMessage='Email'
+ />
+ }
+ inputs={inputs}
+ submit={submit}
+ server_error={this.state.serverError}
+ client_error={this.state.emailError}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ let describe = '';
+ if (this.props.user.auth_service === '') {
+ if (this.state.emailChangeInProgress) {
+ const newEmail = UserStore.getCurrentUser().email;
+ if (newEmail) {
+ describe = (
+ <FormattedHTMLMessage
+ id='user.settings.general.newAddress'
+ defaultMessage='New Address: {email}<br />Check your email to verify the above address.'
+ values={{
+ email: newEmail
+ }}
+ />
+ );
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.general.checkEmailNoAddress'
+ defaultMessage='Check your email to verify your new address'
+ />
+ );
+ }
+ } else {
+ describe = UserStore.getCurrentUser().email;
+ }
+ } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.general.loginGitlab'
+ defaultMessage='Login done through GitLab ({email})'
+ values={{
+ email: this.state.email
+ }}
+ />
+ );
+ } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.general.loginLdap'
+ defaultMessage='Login done through LDAP ({email})'
+ values={{
+ email: this.state.email
+ }}
+ />
+ );
+ }
+
+ emailSection = (
+ <SettingItemMin
+ title={
+ <FormattedMessage
+ id='user.settings.general.email'
+ defaultMessage='Email'
+ />
+ }
+ describe={describe}
+ updateSection={() => {
+ this.updateSection('email');
+ }}
+ />
+ );
+ }
+
+ return emailSection;
+ }
render() {
const user = this.props.user;
- const {formatMessage, formatHTMLMessage} = this.props.intl;
+ const {formatMessage} = this.props.intl;
let clientError = null;
if (this.state.clientError) {
@@ -309,10 +510,6 @@ class UserSettingsGeneralTab extends React.Component {
if (this.state.serverError) {
serverError = this.state.serverError;
}
- let emailError = null;
- if (this.state.emailError) {
- emailError = this.state.emailError;
- }
let nameSection;
const inputs = [];
@@ -407,20 +604,27 @@ class UserSettingsGeneralTab extends React.Component {
/>
);
} else {
- let fullName = '';
+ let describe = '';
if (user.first_name && user.last_name) {
- fullName = user.first_name + ' ' + user.last_name;
+ describe = user.first_name + ' ' + user.last_name;
} else if (user.first_name) {
- fullName = user.first_name;
+ describe = user.first_name;
} else if (user.last_name) {
- fullName = user.last_name;
+ describe = user.last_name;
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.general.emptyName'
+ defaultMessage="Click 'Edit' to add your full name"
+ />
+ );
}
nameSection = (
<SettingItemMin
title={formatMessage(holders.fullName)}
- describe={fullName}
+ describe={describe}
updateSection={() => {
this.updateSection('name');
}}
@@ -481,10 +685,22 @@ class UserSettingsGeneralTab extends React.Component {
/>
);
} else {
+ let describe = '';
+ if (user.nickname) {
+ describe = user.nickname;
+ } else {
+ describe = (
+ <FormattedMessage
+ id='user.settings.general.emptyNickname'
+ defaultMessage="Click 'Edit' to add a nickname"
+ />
+ );
+ }
+
nicknameSection = (
<SettingItemMin
title={formatMessage(holders.nickname)}
- describe={UserStore.getCurrentUser().nickname}
+ describe={describe}
updateSection={() => {
this.updateSection('nickname');
}}
@@ -557,152 +773,7 @@ class UserSettingsGeneralTab extends React.Component {
);
}
- let emailSection;
- if (this.props.activeSection === 'email') {
- const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true';
- const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true';
- let helpText = (
- <FormattedMessage
- id='user.settings.general.emailHelp1'
- defaultMessage='Email is used for sign-in, notifications, and password reset. Email requires verification if changed.'
- />
- );
-
- if (!emailEnabled) {
- helpText = (
- <div className='setting-list__hint text-danger'>
- <FormattedMessage
- id='user.settings.general.emailHelp2'
- defaultMessage='Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'
- />
- </div>
- );
- } else if (!emailVerificationEnabled) {
- helpText = (
- <FormattedMessage
- id='user.settings.general.emailHelp3'
- defaultMessage='Email is used for sign-in, notifications, and password reset.'
- />
- );
- } else if (this.state.emailChangeInProgress) {
- const newEmail = UserStore.getCurrentUser().email;
- if (newEmail) {
- helpText = (
- <FormattedMessage
- id='user.settings.general.emailHelp4'
- defaultMessage='A verification email was sent to {email}.'
- values={{
- email: newEmail
- }}
- />
- );
- }
- }
-
- let submit = null;
-
- if (this.props.user.auth_service === '') {
- inputs.push(
- <div key='emailSetting'>
- <div className='form-group'>
- <label className='col-sm-5 control-label'>
- <FormattedMessage
- id='user.settings.general.primaryEmail'
- defaultMessage='Primary Email'
- />
- </label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='text'
- onChange={this.updateEmail}
- value={this.state.email}
- />
- </div>
- </div>
- </div>
- );
-
- inputs.push(
- <div key='confirmEmailSetting'>
- <div className='form-group'>
- <label className='col-sm-5 control-label'>
- <FormattedMessage
- id='user.settings.general.confirmEmail'
- defaultMessage='Confirm Email'
- />
- </label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='text'
- onChange={this.updateConfirmEmail}
- value={this.state.confirmEmail}
- />
- </div>
- </div>
- {helpText}
- </div>
- );
-
- submit = this.submitEmail;
- } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
- inputs.push(
- <div
- key='oauthEmailInfo'
- className='form-group'
- >
- <div className='setting-list__hint'>
- <FormattedMessage
- id='user.settings.general.emailCantUpdate'
- defaultMessage='Log in occurs through GitLab. Email cannot be updated.'
- />
- </div>
- {helpText}
- </div>
- );
- }
-
- emailSection = (
- <SettingItemMax
- title='Email'
- inputs={inputs}
- submit={submit}
- server_error={serverError}
- client_error={emailError}
- updateSection={(e) => {
- this.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- let describe = '';
- if (this.props.user.auth_service === '') {
- if (this.state.emailChangeInProgress) {
- const newEmail = UserStore.getCurrentUser().email;
- if (newEmail) {
- describe = formatHTMLMessage(holders.newAddress, {email: newEmail});
- } else {
- describe = formatMessage(holders.checkEmailNoAddress);
- }
- } else {
- describe = UserStore.getCurrentUser().email;
- }
- } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
- describe = formatMessage(holders.loginGitlab);
- }
-
- emailSection = (
- <SettingItemMin
- title={formatMessage(holders.email)}
- describe={describe}
- updateSection={() => {
- this.updateSection('email');
- }}
- />
- );
- }
+ const emailSection = this.createEmailSection();
let pictureSection;
if (this.props.activeSection === 'picture') {
diff --git a/webapp/components/user_settings/user_settings_integrations.jsx b/webapp/components/user_settings/user_settings_integrations.jsx
index 94fc184bd..37081b863 100644
--- a/webapp/components/user_settings/user_settings_integrations.jsx
+++ b/webapp/components/user_settings/user_settings_integrations.jsx
@@ -1,31 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
-import ManageIncomingHooks from './manage_incoming_hooks.jsx';
-import ManageOutgoingHooks from './manage_outgoing_hooks.jsx';
import ManageCommandHooks from './manage_command_hooks.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
const holders = defineMessages({
- inName: {
- id: 'user.settings.integrations.incomingWebhooks',
- defaultMessage: 'Incoming Webhooks'
- },
- inDesc: {
- id: 'user.settings.integrations.incomingWebhooksDescription',
- defaultMessage: 'Manage your incoming webhooks'
- },
- outName: {
- id: 'user.settings.integrations.outWebhooks',
- defaultMessage: 'Outgoing Webhooks'
- },
- outDesc: {
- id: 'user.settings.integrations.outWebhooksDescription',
- defaultMessage: 'Manage your outgoing webhooks'
- },
cmdName: {
id: 'user.settings.integrations.commands',
defaultMessage: 'Slash Commands'
@@ -47,77 +30,14 @@ class UserSettingsIntegrationsTab extends React.Component {
this.state = {};
}
updateSection(section) {
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.props.updateSection(section);
}
render() {
- let incomingHooksSection;
- let outgoingHooksSection;
let commandHooksSection;
var inputs = [];
const {formatMessage} = this.props.intl;
- if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
- if (this.props.activeSection === 'incoming-hooks') {
- inputs.push(
- <ManageIncomingHooks key='incoming-hook-ui'/>
- );
-
- incomingHooksSection = (
- <SettingItemMax
- title={formatMessage(holders.inName)}
- width='medium'
- inputs={inputs}
- updateSection={(e) => {
- this.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- incomingHooksSection = (
- <SettingItemMin
- title={formatMessage(holders.inName)}
- width='medium'
- describe={formatMessage(holders.inDesc)}
- updateSection={() => {
- this.updateSection('incoming-hooks');
- }}
- />
- );
- }
- }
-
- if (global.window.mm_config.EnableOutgoingWebhooks === 'true') {
- if (this.props.activeSection === 'outgoing-hooks') {
- inputs.push(
- <ManageOutgoingHooks key='outgoing-hook-ui'/>
- );
-
- outgoingHooksSection = (
- <SettingItemMax
- title={formatMessage(holders.outName)}
- width='medium'
- inputs={inputs}
- updateSection={(e) => {
- this.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- outgoingHooksSection = (
- <SettingItemMin
- title={formatMessage(holders.outName)}
- width='medium'
- describe={formatMessage(holders.outDesc)}
- updateSection={() => {
- this.updateSection('outgoing-hooks');
- }}
- />
- );
- }
- }
-
if (global.window.mm_config.EnableCommands === 'true') {
if (this.props.activeSection === 'command-hooks') {
inputs.push(
@@ -185,10 +105,6 @@ class UserSettingsIntegrationsTab extends React.Component {
/>
</h3>
<div className='divider-dark first'/>
- {incomingHooksSection}
- <div className='divider-light'/>
- {outgoingHooksSection}
- <div className='divider-dark'/>
{commandHooksSection}
<div className='divider-dark'/>
</div>
@@ -207,4 +123,4 @@ UserSettingsIntegrationsTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired
};
-export default injectIntl(UserSettingsIntegrationsTab); \ No newline at end of file
+export default injectIntl(UserSettingsIntegrationsTab);
diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx
index d1c1f0fe2..b71547baf 100644
--- a/webapp/components/user_settings/user_settings_modal.jsx
+++ b/webapp/components/user_settings/user_settings_modal.jsx
@@ -9,7 +9,6 @@ import SettingsSidebar from '../settings_sidebar.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
-import Constants from 'utils/constants.jsx';
import {Modal} from 'react-bootstrap';
@@ -113,7 +112,6 @@ class UserSettingsModal extends React.Component {
return;
}
- this.resetTheme();
this.deactivateTab();
this.props.onModalDismissed();
return;
@@ -220,22 +218,10 @@ class UserSettingsModal extends React.Component {
if (!skipConfirm && this.requireConfirm) {
this.showConfirmModal(() => this.updateSection(section, true));
} else {
- if (this.state.active_section === 'theme' && section !== 'theme') {
- this.resetTheme();
- }
this.setState({active_section: section});
}
}
- resetTheme() {
- const user = UserStore.getCurrentUser();
- if (user.theme_props == null) {
- Utils.applyTheme(Constants.THEMES.default);
- } else {
- Utils.applyTheme(user.theme_props);
- }
- }
-
render() {
const {formatMessage} = this.props.intl;
if (this.state.currentUser == null) {
diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx
index fe2db6727..b119c42f9 100644
--- a/webapp/components/user_settings/user_settings_notifications.jsx
+++ b/webapp/components/user_settings/user_settings_notifications.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import ReactDOM from 'react-dom';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
@@ -162,6 +163,7 @@ class NotificationsTab extends React.Component {
this.updateState();
this.props.updateSection('');
e.preventDefault();
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
}
updateSection(section) {
this.updateState();
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 283d2c425..ff5a898a9 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -1,6 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import AccessHistoryModal from '../access_history_modal.jsx';
@@ -30,14 +31,6 @@ const holders = defineMessages({
id: 'user.settings.security.passwordMatchError',
defaultMessage: 'The new passwords you entered do not match'
},
- password: {
- id: 'user.settings.security.password',
- defaultMessage: 'Password'
- },
- lastUpdated: {
- id: 'user.settings.security.lastUpdated',
- defaultMessage: 'Last updated {date} at {time}'
- },
method: {
id: 'user.settings.security.method',
defaultMessage: 'Sign-in Method'
@@ -55,12 +48,16 @@ class SecurityTab extends React.Component {
super(props);
this.submitPassword = this.submitPassword.bind(this);
+ this.activateMfa = this.activateMfa.bind(this);
+ this.deactivateMfa = this.deactivateMfa.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
+ this.updateMfaToken = this.updateMfaToken.bind(this);
this.getDefaultState = this.getDefaultState.bind(this);
this.createPasswordSection = this.createPasswordSection.bind(this);
this.createSignInSection = this.createSignInSection.bind(this);
+ this.showQrCode = this.showQrCode.bind(this);
this.state = this.getDefaultState();
}
@@ -69,7 +66,9 @@ class SecurityTab extends React.Component {
currentPassword: '',
newPassword: '',
confirmPassword: '',
- authService: this.props.user.auth_service
+ authService: this.props.user.auth_service,
+ mfaShowQr: false,
+ mfaToken: ''
};
}
submitPassword(e) {
@@ -120,6 +119,51 @@ class SecurityTab extends React.Component {
}
);
}
+ activateMfa() {
+ const data = {};
+ data.activate = true;
+ data.token = this.state.mfaToken;
+
+ Client.updateMfa(data,
+ () => {
+ this.props.updateSection('');
+ AsyncClient.getMe();
+ this.setState(this.getDefaultState());
+ },
+ (err) => {
+ const state = this.getDefaultState();
+ if (err.message) {
+ state.serverError = err.message;
+ } else {
+ state.serverError = err;
+ }
+ state.mfaError = '';
+ this.setState(state);
+ }
+ );
+ }
+ deactivateMfa() {
+ const data = {};
+ data.activate = false;
+
+ Client.updateMfa(data,
+ () => {
+ this.props.updateSection('');
+ AsyncClient.getMe();
+ this.setState(this.getDefaultState());
+ },
+ (err) => {
+ const state = this.getDefaultState();
+ if (err.message) {
+ state.serverError = err.message;
+ } else {
+ state.serverError = err;
+ }
+ state.mfaError = '';
+ this.setState(state);
+ }
+ );
+ }
updateCurrentPassword(e) {
this.setState({currentPassword: e.target.value});
}
@@ -129,123 +173,335 @@ class SecurityTab extends React.Component {
updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value});
}
- createPasswordSection() {
+ updateMfaToken(e) {
+ this.setState({mfaToken: e.target.value});
+ }
+ showQrCode(e) {
+ e.preventDefault();
+ this.setState({mfaShowQr: true});
+ }
+ createMfaSection() {
let updateSectionStatus;
- const {formatMessage} = this.props.intl;
-
- if (this.props.activeSection === 'password' && this.props.user.auth_service === '') {
- const inputs = [];
+ let submit;
+
+ if (this.props.activeSection === 'mfa') {
+ let content;
+ let extraInfo;
+ if (this.props.user.mfa_active) {
+ content = (
+ <div key='mfaQrCode'>
+ <a
+ className='btn btn-primary'
+ href='#'
+ onClick={this.deactivateMfa}
+ >
+ <FormattedMessage
+ id='user.settings.mfa.remove'
+ defaultMessage='Remove MFA from your account'
+ />
+ </a>
+ <br/>
+ </div>
+ );
- inputs.push(
- <div
- key='currentPasswordUpdateForm'
- className='form-group'
- >
- <label className='col-sm-5 control-label'>
+ extraInfo = (
+ <span>
<FormattedMessage
- id='user.settings.security.currentPassword'
- defaultMessage='Current Password'
- />
- </label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='password'
- onChange={this.updateCurrentPassword}
- value={this.state.currentPassword}
+ id='user.settings.mfa.removeHelp'
+ defaultMessage='Removing multi-factor authentication will make your account more vulnerable to attacks.'
/>
+ </span>
+ );
+ } else if (this.state.mfaShowQr) {
+ content = (
+ <div key='mfaButton'>
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.mfa.qrCode'
+ defaultMessage='QR Code'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <img
+ className='qr-code-img'
+ src={'/api/v1/users/generate_mfa_qr?time=' + this.props.user.update_at}
+ />
+ </div>
+ <br/>
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.mfa.enterToken'
+ defaultMessage='Token'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ onChange={this.updateMfaToken}
+ value={this.state.mfaToken}
+ />
+ </div>
</div>
- </div>
- );
- inputs.push(
- <div
- key='newPasswordUpdateForm'
- className='form-group'
- >
- <label className='col-sm-5 control-label'>
+ );
+
+ extraInfo = (
+ <span>
<FormattedMessage
- id='user.settings.security.newPassword'
- defaultMessage='New Password'
- />
- </label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='password'
- onChange={this.updateNewPassword}
- value={this.state.newPassword}
+ id='user.settings.mfa.addHelpQr'
+ defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app.'
/>
+ </span>
+ );
+
+ submit = this.activateMfa;
+ } else {
+ content = (
+ <div key='mfaQrCode'>
+ <a
+ className='btn btn-primary'
+ href='#'
+ onClick={this.showQrCode}
+ >
+ <FormattedMessage
+ id='user.settings.mfa.add'
+ defaultMessage='Add MFA to your account'
+ />
+ </a>
+ <br/>
</div>
- </div>
- );
+ );
+
+ extraInfo = (
+ <span>
+ <FormattedMessage
+ id='user.settings.mfa.addHelp'
+ defaultMessage='To add multi-factor authentication to your account you must have a smartphone with Google Authenticator installed.'
+ />
+ </span>
+ );
+ }
+
+ const inputs = [];
inputs.push(
<div
- key='retypeNewPasswordUpdateForm'
+ key='mfaSetting'
className='form-group'
>
- <label className='col-sm-5 control-label'>
- <FormattedMessage
- id='user.settings.security.retypePassword'
- defaultMessage='Retype New Password'
- />
- </label>
- <div className='col-sm-7'>
- <input
- className='form-control'
- type='password'
- onChange={this.updateConfirmPassword}
- value={this.state.confirmPassword}
- />
- </div>
+ {content}
</div>
);
updateSectionStatus = function resetSection(e) {
this.props.updateSection('');
- this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null});
+ this.setState({mfaToken: '', mfaShowQr: false, mfaError: null});
e.preventDefault();
}.bind(this);
return (
<SettingItemMax
- title={formatMessage(holders.password)}
+ title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')}
inputs={inputs}
- submit={this.submitPassword}
+ extraInfo={extraInfo}
+ submit={submit}
server_error={this.state.serverError}
- client_error={this.state.passwordError}
+ client_error={this.state.mfaError}
updateSection={updateSectionStatus}
/>
);
}
- var describe;
- var d = new Date(this.props.user.last_password_update);
+ let describe;
+ if (this.props.user.mfa_active) {
+ describe = Utils.localizeMessage('user.settings.security.active', 'Active');
+ } else {
+ describe = Utils.localizeMessage('user.settings.security.inactive', 'Inactive');
+ }
+
+ updateSectionStatus = function updateSection() {
+ this.props.updateSection('mfa');
+ }.bind(this);
- const hours12 = !Utils.isMilitaryTime();
- describe = (
- <FormattedMessage
- id='user.settings.security.lastUpdated'
- defaultMessage='Last updated {date} at {time}'
- values={{
- date: (
- <FormattedDate
- value={d}
- day='2-digit'
- month='short'
- year='numeric'
- />
- ),
- time: (
- <FormattedTime
- value={d}
- hour12={hours12}
- hour='2-digit'
- minute='2-digit'
- />
- )
- }}
+ return (
+ <SettingItemMin
+ title={Utils.localizeMessage('user.settings.mfa.title', 'Multi-factor Authentication')}
+ describe={describe}
+ updateSection={updateSectionStatus}
/>
);
+ }
+ createPasswordSection() {
+ let updateSectionStatus;
+
+ if (this.props.activeSection === 'password') {
+ const inputs = [];
+ let submit;
+
+ if (this.props.user.auth_service === '') {
+ submit = this.submitPassword;
+
+ inputs.push(
+ <div
+ key='currentPasswordUpdateForm'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.security.currentPassword'
+ defaultMessage='Current Password'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='password'
+ onChange={this.updateCurrentPassword}
+ value={this.state.currentPassword}
+ />
+ </div>
+ </div>
+ );
+ inputs.push(
+ <div
+ key='newPasswordUpdateForm'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.security.newPassword'
+ defaultMessage='New Password'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='password'
+ onChange={this.updateNewPassword}
+ value={this.state.newPassword}
+ />
+ </div>
+ </div>
+ );
+ inputs.push(
+ <div
+ key='retypeNewPasswordUpdateForm'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>
+ <FormattedMessage
+ id='user.settings.security.retypePassword'
+ defaultMessage='Retype New Password'
+ />
+ </label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='password'
+ onChange={this.updateConfirmPassword}
+ value={this.state.confirmPassword}
+ />
+ </div>
+ </div>
+ );
+ } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordGitlabCantUpdate'
+ defaultMessage='Login occurs through GitLab. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
+ } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordLdapCantUpdate'
+ defaultMessage='Login occurs through LDAP. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
+ }
+
+ updateSectionStatus = function resetSection(e) {
+ this.props.updateSection('');
+ this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null});
+ e.preventDefault();
+ $('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
+ }.bind(this);
+
+ return (
+ <SettingItemMax
+ title={
+ <FormattedMessage
+ id='user.settings.security.password'
+ defaultMessage='Password'
+ />
+ }
+ inputs={inputs}
+ submit={submit}
+ server_error={this.state.serverError}
+ client_error={this.state.passwordError}
+ updateSection={updateSectionStatus}
+ />
+ );
+ }
+
+ let describe;
+
+ if (this.props.user.auth_service === '') {
+ const d = new Date(this.props.user.last_password_update);
+ const hours12 = !Utils.isMilitaryTime();
+
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.lastUpdated'
+ defaultMessage='Last updated {date} at {time}'
+ values={{
+ date: (
+ <FormattedDate
+ value={d}
+ day='2-digit'
+ month='short'
+ year='numeric'
+ />
+ ),
+ time: (
+ <FormattedTime
+ value={d}
+ hour12={hours12}
+ hour='2-digit'
+ minute='2-digit'
+ />
+ )
+ }}
+ />
+ );
+ } else if (this.props.user.auth_service === Constants.GITLAB_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginGitlab'
+ defaultMessage='Login done through Gitlab'
+ />
+ );
+ } else if (this.props.user.auth_service === Constants.LDAP_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginLdap'
+ defaultMessage='Login done through LDAP'
+ />
+ );
+ }
updateSectionStatus = function updateSection() {
this.props.updateSection('password');
@@ -253,7 +509,12 @@ class SecurityTab extends React.Component {
return (
<SettingItemMin
- title={formatMessage(holders.password)}
+ title={
+ <FormattedMessage
+ id='user.settings.security.password'
+ defaultMessage='Password'
+ />
+ }
describe={describe}
updateSection={updateSectionStatus}
/>
@@ -264,7 +525,6 @@ class SecurityTab extends React.Component {
const user = this.props.user;
if (this.props.activeSection === 'signin') {
- const inputs = [];
const teamName = TeamStore.getCurrent().name;
let emailOption;
@@ -346,6 +606,7 @@ class SecurityTab extends React.Component {
);
}
+ const inputs = [];
inputs.push(
<div key='userSignInOption'>
{emailOption}
@@ -411,16 +672,22 @@ class SecurityTab extends React.Component {
}
render() {
const passwordSection = this.createPasswordSection();
- let signInSection;
let numMethods = 0;
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
+ numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
- if (global.window.mm_config.EnableSignUpWithEmail && numMethods > 0) {
+ let signInSection;
+ if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
signInSection = this.createSignInSection();
}
+ let mfaSection;
+ if (global.window.mm_config.EnableMultifactorAuthentication === 'true' && global.window.mm_license.IsLicensed === 'true') {
+ mfaSection = this.createMfaSection();
+ }
+
return (
<div>
<div className='modal-header'>
@@ -459,6 +726,8 @@ class SecurityTab extends React.Component {
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-light'/>
+ {mfaSection}
+ <div className='divider-light'/>
{signInSection}
<div className='divider-dark'/>
<br></br>
diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx
index 3414fe2e2..14991037d 100644
--- a/webapp/components/user_settings/user_settings_theme.jsx
+++ b/webapp/components/user_settings/user_settings_theme.jsx
@@ -40,7 +40,6 @@ export default class ThemeSetting extends React.Component {
this.onChange = this.onChange.bind(this);
this.submitTheme = this.submitTheme.bind(this);
this.updateTheme = this.updateTheme.bind(this);
- this.deactivate = this.deactivate.bind(this);
this.resetFields = this.resetFields.bind(this);
this.handleImportModal = this.handleImportModal.bind(this);
@@ -62,12 +61,17 @@ export default class ThemeSetting extends React.Component {
}
}
componentWillReceiveProps(nextProps) {
- if (!this.props.selected && nextProps.selected) {
+ if (this.props.selected && !nextProps.selected) {
this.resetFields();
}
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
+
+ if (this.props.selected) {
+ const state = this.getStateFromStores();
+ Utils.applyTheme(state.theme);
+ }
}
getStateFromStores() {
const user = UserStore.getCurrentUser();
@@ -147,11 +151,6 @@ export default class ThemeSetting extends React.Component {
updateType(type) {
this.setState({type});
}
- deactivate() {
- const state = this.getStateFromStores();
-
- Utils.applyTheme(state.theme);
- }
resetFields() {
const state = this.getStateFromStores();
state.serverError = null;