summaryrefslogtreecommitdiffstats
path: root/webapp/components
diff options
context:
space:
mode:
Diffstat (limited to 'webapp/components')
-rw-r--r--webapp/components/admin_console/admin_sidebar.jsx16
-rw-r--r--webapp/components/admin_console/cluster_settings.jsx4
-rw-r--r--webapp/components/admin_console/cluster_table.jsx2
-rw-r--r--webapp/components/admin_console/cluster_table_container.jsx6
-rw-r--r--webapp/components/admin_console/file_upload_setting.jsx2
-rw-r--r--webapp/components/admin_console/logs.jsx11
-rw-r--r--webapp/components/admin_console/metrics_settings.jsx96
-rw-r--r--webapp/components/admin_console/users_and_teams_settings.jsx22
-rw-r--r--webapp/components/admin_console/webrtc_settings.jsx36
-rw-r--r--webapp/components/change_url_modal.jsx2
-rw-r--r--webapp/components/channel_header.jsx49
-rw-r--r--webapp/components/code_preview.jsx6
-rw-r--r--webapp/components/create_team/components/display_name.jsx18
-rw-r--r--webapp/components/create_team/components/team_url.jsx42
-rw-r--r--webapp/components/integrations/components/edit_command.jsx731
-rw-r--r--webapp/components/integrations/components/installed_command.jsx9
-rw-r--r--webapp/components/logged_in.jsx16
-rw-r--r--webapp/components/login/login_controller.jsx9
-rw-r--r--webapp/components/more_direct_channels.jsx4
-rw-r--r--webapp/components/navbar.jsx8
-rw-r--r--webapp/components/new_channel_flow.jsx14
-rw-r--r--webapp/components/new_channel_modal.jsx3
-rw-r--r--webapp/components/password_reset_send_link.jsx6
-rw-r--r--webapp/components/search_results.jsx2
-rw-r--r--webapp/components/search_results_item.jsx215
-rw-r--r--webapp/components/sidebar_right_menu.jsx1
-rw-r--r--webapp/components/signup/components/signup_email.jsx6
-rw-r--r--webapp/components/signup/components/signup_ldap.jsx6
-rw-r--r--webapp/components/suggestion/at_mention_provider.jsx23
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx3
-rw-r--r--webapp/components/team_general_tab.jsx21
-rw-r--r--webapp/components/user_profile.jsx43
-rw-r--r--webapp/components/user_settings/manage_languages.jsx5
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx7
-rw-r--r--webapp/components/user_settings/user_settings_modal.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx25
-rw-r--r--webapp/components/youtube_video.jsx2
37 files changed, 1231 insertions, 242 deletions
diff --git a/webapp/components/admin_console/admin_sidebar.jsx b/webapp/components/admin_console/admin_sidebar.jsx
index f39bb8b6b..25a06cecf 100644
--- a/webapp/components/admin_console/admin_sidebar.jsx
+++ b/webapp/components/admin_console/admin_sidebar.jsx
@@ -192,6 +192,7 @@ export default class AdminSidebar extends React.Component {
let ldapSettings = null;
let samlSettings = null;
let clusterSettings = null;
+ let metricsSettings = null;
let complianceSettings = null;
let license = null;
@@ -241,6 +242,20 @@ export default class AdminSidebar extends React.Component {
);
}
+ if (global.window.mm_license.Metrics === 'true') {
+ metricsSettings = (
+ <AdminSidebarSection
+ name='metrics'
+ title={
+ <FormattedMessage
+ id='admin.sidebar.metrics'
+ defaultMessage='Performance Monitoring (Beta)'
+ />
+ }
+ />
+ );
+ }
+
if (global.window.mm_license.SAML === 'true') {
samlSettings = (
<AdminSidebarSection
@@ -716,6 +731,7 @@ export default class AdminSidebar extends React.Component {
}
/>
{clusterSettings}
+ {metricsSettings}
</AdminSidebarSection>
</AdminSidebarCategory>
{this.renderTeams()}
diff --git a/webapp/components/admin_console/cluster_settings.jsx b/webapp/components/admin_console/cluster_settings.jsx
index 8aab905e4..bbd135e50 100644
--- a/webapp/components/admin_console/cluster_settings.jsx
+++ b/webapp/components/admin_console/cluster_settings.jsx
@@ -60,7 +60,7 @@ export default class ClusterSettings extends AdminSettings {
);
}
- overrideHandleChange = (id, value) => {
+ overrideHandleChange(id, value) {
this.setState({
showWarning: true
});
@@ -185,4 +185,4 @@ export default class ClusterSettings extends AdminSettings {
</SettingsGroup>
);
}
-} \ No newline at end of file
+}
diff --git a/webapp/components/admin_console/cluster_table.jsx b/webapp/components/admin_console/cluster_table.jsx
index 4aca796a0..0a2755c4a 100644
--- a/webapp/components/admin_console/cluster_table.jsx
+++ b/webapp/components/admin_console/cluster_table.jsx
@@ -176,4 +176,4 @@ export default class ClusterTable extends React.Component {
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/webapp/components/admin_console/cluster_table_container.jsx b/webapp/components/admin_console/cluster_table_container.jsx
index 5dad56469..aad5753b7 100644
--- a/webapp/components/admin_console/cluster_table_container.jsx
+++ b/webapp/components/admin_console/cluster_table_container.jsx
@@ -18,7 +18,7 @@ export default class ClusterTableContainer extends React.Component {
};
}
- load = () => {
+ load() {
Client.getClusterStatus(
(data) => {
this.setState({
@@ -44,7 +44,7 @@ export default class ClusterTableContainer extends React.Component {
}
}
- reload = (e) => {
+ reload(e) {
if (e) {
e.preventDefault();
}
@@ -68,4 +68,4 @@ export default class ClusterTableContainer extends React.Component {
/>
);
}
-} \ No newline at end of file
+}
diff --git a/webapp/components/admin_console/file_upload_setting.jsx b/webapp/components/admin_console/file_upload_setting.jsx
index a7df16c0a..0c1efc168 100644
--- a/webapp/components/admin_console/file_upload_setting.jsx
+++ b/webapp/components/admin_console/file_upload_setting.jsx
@@ -108,7 +108,7 @@ export default class FileUploadSetting extends Setting {
disabled={!this.state.fileSelected}
onClick={this.handleSubmit}
ref='upload_button'
- data-loading-text={`<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ${this.props.uploadingText}`}
+ data-loading-text={`<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> ${this.props.uploadingText}`}
>
<FormattedMessage
id='admin.file_upload.uploadFile'
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index ad0277b7f..8dc0c1e2e 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -26,6 +26,12 @@ export default class Logs extends React.Component {
AsyncClient.getLogs();
}
+ componentDidUpdate() {
+ // Scroll Down to get the latest logs
+ var node = this.refs.logPanel;
+ node.scrollTop = node.scrollHeight;
+ }
+
componentWillUnmount() {
AdminStore.removeLogChangeListener(this.onLogListenerChange);
}
@@ -93,7 +99,10 @@ export default class Logs extends React.Component {
defaultMessage='Reload'
/>
</button>
- <div className='log__panel'>
+ <div
+ ref='logPanel'
+ className='log__panel'
+ >
{content}
</div>
</div>
diff --git a/webapp/components/admin_console/metrics_settings.jsx b/webapp/components/admin_console/metrics_settings.jsx
new file mode 100644
index 000000000..dd031047e
--- /dev/null
+++ b/webapp/components/admin_console/metrics_settings.jsx
@@ -0,0 +1,96 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import AdminSettings from './admin_settings.jsx';
+import BooleanSetting from './boolean_setting.jsx';
+import TextSetting from './text_setting.jsx';
+
+import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import SettingsGroup from './settings_group.jsx';
+
+import * as Utils from 'utils/utils.jsx';
+
+export default class MetricsSettings extends AdminSettings {
+ constructor(props) {
+ super(props);
+
+ this.getConfigFromState = this.getConfigFromState.bind(this);
+ this.renderSettings = this.renderSettings.bind(this);
+ }
+
+ getConfigFromState(config) {
+ config.MetricsSettings.Enable = this.state.enable;
+ config.MetricsSettings.ListenAddress = this.state.listenAddress;
+
+ return config;
+ }
+
+ getStateFromConfig(config) {
+ const settings = config.MetricsSettings;
+
+ return {
+ enable: settings.Enable,
+ listenAddress: settings.ListenAddress
+ };
+ }
+
+ renderTitle() {
+ return (
+ <h3>
+ <FormattedMessage
+ id='admin.advance.metrics'
+ defaultMessage='Performance Monitoring (Beta)'
+ />
+ </h3>
+ );
+ }
+
+ renderSettings() {
+ const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.Metrics === 'true';
+ if (!licenseEnabled) {
+ return null;
+ }
+
+ return (
+ <SettingsGroup>
+ <BooleanSetting
+ id='enable'
+ label={
+ <FormattedMessage
+ id='admin.metrics.enableTitle'
+ defaultMessage='Enable Performance Monitoring:'
+ />
+ }
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.metrics.enableDescription'
+ defaultMessage='When true, Mattermost will enable performance monitoring collection and profiling. Please see <a href="http://docs.mattermost.com/deployment/metrics.html" target="_blank">documentation</a> to learn more about configuring performance monitoring for Mattermost.'
+ />
+ }
+ value={this.state.enable}
+ onChange={this.handleChange}
+ />
+ <TextSetting
+ id='listenAddress'
+ label={
+ <FormattedMessage
+ id='admin.metrics.listenAddressTitle'
+ defaultMessage='Listen Address:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.metrics.listenAddressEx', 'Ex ":8067"')}
+ helpText={
+ <FormattedMessage
+ id='admin.metrics.listenAddressDesc'
+ defaultMessage='The address the server will listen on to expose performance metrics.'
+ />
+ }
+ value={this.state.listenAddress}
+ onChange={this.handleChange}
+ />
+ </SettingsGroup>
+ );
+ }
+}
diff --git a/webapp/components/admin_console/users_and_teams_settings.jsx b/webapp/components/admin_console/users_and_teams_settings.jsx
index dd19005c8..2cb5b4e51 100644
--- a/webapp/components/admin_console/users_and_teams_settings.jsx
+++ b/webapp/components/admin_console/users_and_teams_settings.jsx
@@ -32,6 +32,7 @@ export default class UsersAndTeamsSettings extends AdminSettings {
config.TeamSettings.RestrictCreationToDomains = this.state.restrictCreationToDomains;
config.TeamSettings.RestrictDirectMessage = this.state.restrictDirectMessage;
config.TeamSettings.MaxChannelsPerTeam = this.parseIntNonZero(this.state.maxChannelsPerTeam, Constants.DEFAULT_MAX_CHANNELS_PER_TEAM);
+ config.TeamSettings.MaxNotificationsPerChannel = this.parseIntNonZero(this.state.maxNotificationsPerChannel, Constants.DEFAULT_MAX_NOTIFICATIONS_PER_CHANNEL);
return config;
}
@@ -43,7 +44,8 @@ export default class UsersAndTeamsSettings extends AdminSettings {
maxUsersPerTeam: config.TeamSettings.MaxUsersPerTeam,
restrictCreationToDomains: config.TeamSettings.RestrictCreationToDomains,
restrictDirectMessage: config.TeamSettings.RestrictDirectMessage,
- maxChannelsPerTeam: config.TeamSettings.MaxChannelsPerTeam
+ maxChannelsPerTeam: config.TeamSettings.MaxChannelsPerTeam,
+ maxNotificationsPerChannel: config.TeamSettings.MaxNotificationsPerChannel
};
}
@@ -132,6 +134,24 @@ export default class UsersAndTeamsSettings extends AdminSettings {
onChange={this.handleChange}
/>
<TextSetting
+ id='maxNotificationsPerChannel'
+ label={
+ <FormattedMessage
+ id='admin.team.maxNotificationsPerChannelTitle'
+ defaultMessage='Max Notifications Per Channel:'
+ />
+ }
+ placeholder={Utils.localizeMessage('admin.team.maxNotificationsPerChannelExample', 'Ex "1000"')}
+ helpText={
+ <FormattedMessage
+ id='admin.team.maxNotificationsPerChannelDescription'
+ defaultMessage='Maximum total number of users in a channel before users typing messages, @all, @here, and @channel no longer send notifications because of performance.'
+ />
+ }
+ value={this.state.maxNotificationsPerChannel}
+ onChange={this.handleChange}
+ />
+ <TextSetting
id='restrictCreationToDomains'
label={
<FormattedMessage
diff --git a/webapp/components/admin_console/webrtc_settings.jsx b/webapp/components/admin_console/webrtc_settings.jsx
index cea8e2226..995a02a0c 100644
--- a/webapp/components/admin_console/webrtc_settings.jsx
+++ b/webapp/components/admin_console/webrtc_settings.jsx
@@ -15,23 +15,10 @@ export default class WebrtcSettings extends AdminSettings {
constructor(props) {
super(props);
- this.canSave = this.canSave.bind(this);
- this.handleAgreeChange = this.handleAgreeChange.bind(this);
-
this.getConfigFromState = this.getConfigFromState.bind(this);
this.renderSettings = this.renderSettings.bind(this);
}
- canSave() {
- return !this.state.enableWebrtc || this.state.agree;
- }
-
- handleAgreeChange(e) {
- this.setState({
- agree: e.target.checked
- });
- }
-
getConfigFromState(config) {
config.WebrtcSettings.Enable = this.state.enableWebrtc;
config.WebrtcSettings.GatewayWebsocketUrl = this.state.gatewayWebsocketUrl;
@@ -57,8 +44,7 @@ export default class WebrtcSettings extends AdminSettings {
stunURI: settings.StunURI,
turnURI: settings.TurnURI,
turnUsername: settings.TurnUsername,
- turnSharedKey: settings.TurnSharedKey,
- agree: settings.Enable
+ turnSharedKey: settings.TurnSharedKey
};
}
@@ -74,25 +60,6 @@ export default class WebrtcSettings extends AdminSettings {
}
renderSettings() {
- const tosCheckbox = (
- <div className='form-group'>
- <div className='col-sm-4'/>
- <div className='col-sm-8'>
- <input
- type='checkbox'
- ref='agree'
- checked={this.state.agree}
- onChange={this.handleAgreeChange}
- disabled={!this.state.enableWebrtc}
- />
- <FormattedHTMLMessage
- id='admin.webrtc.agree'
- defaultMessage=' I understand and accept the Mattermost Hosted WebRTC Service <a href="https://about.mattermost.com/webrtc-terms/" target="_blank">Terms of Service</a> and <a href="https://about.mattermost.com/webrtc-privacy/" target="_blank">Privacy Policy</a>.'
- />
- </div>
- </div>
- );
-
return (
<SettingsGroup>
<BooleanSetting
@@ -112,7 +79,6 @@ export default class WebrtcSettings extends AdminSettings {
value={this.state.enableWebrtc}
onChange={this.handleChange}
/>
- {tosCheckbox}
<TextSetting
id='gatewayWebsocketUrl'
label={
diff --git a/webapp/components/change_url_modal.jsx b/webapp/components/change_url_modal.jsx
index fa115cf36..c9d2f3245 100644
--- a/webapp/components/change_url_modal.jsx
+++ b/webapp/components/change_url_modal.jsx
@@ -145,6 +145,7 @@ export default class ChangeUrlModal extends React.Component {
<Modal
show={this.props.show}
onHide={this.doCancel}
+ onExited={this.props.onModalExited}
>
<Modal.Header closeButton={true}>
<Modal.Title>{this.props.title}</Modal.Title>
@@ -226,5 +227,6 @@ ChangeUrlModal.propTypes = {
currentURL: React.PropTypes.string,
serverError: React.PropTypes.node,
onModalSubmit: React.PropTypes.func.isRequired,
+ onModalExited: React.PropTypes.func.optional,
onModalDismissed: React.PropTypes.func.isRequired
};
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index 50b860287..d8110aa5a 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -308,6 +308,13 @@ export default class ChannelHeader extends React.Component {
if (isOffline || busy) {
circleClass = 'offline';
+ webrtcMessage = (
+ <FormattedMessage
+ id='channel_header.webrtc.offline'
+ defaultMessage='The user is offline'
+ />
+ );
+
if (busy) {
webrtcMessage = (
<FormattedMessage
@@ -325,6 +332,10 @@ export default class ChannelHeader extends React.Component {
);
}
+ const webrtcTooltip = (
+ <Tooltip id='webrtcTooltip'>{webrtcMessage}</Tooltip>
+ );
+
webrtc = (
<div className='webrtc__header'>
<a
@@ -332,28 +343,18 @@ export default class ChannelHeader extends React.Component {
onClick={() => this.initWebrtc(otherUserId, !isOffline)}
disabled={isOffline}
>
- <svg
- id='webrtc-btn'
- className='webrtc__button'
- xmlns='http://www.w3.org/2000/svg'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='bottom'
+ overlay={webrtcTooltip}
>
- <circle
- className={circleClass}
- cx='16'
- cy='16'
- r='18'
+ <div
+ id='webrtc-btn'
+ className={'webrtc__button ' + circleClass}
>
- <title>
- {webrtcMessage}
- </title>
- </circle>
- <path
- className='off'
- transform='scale(0.4), translate(17,16)'
- d='M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm-4 24l-8-6.4V32H12V16h16v6.4l8-6.4v16z'
- fill='white'
- />
- </svg>
+ <span dangerouslySetInnerHTML={{__html: Constants.VIDEO_ICON}}/>
+ </div>
+ </OverlayTrigger>
</a>
</div>
);
@@ -648,10 +649,10 @@ export default class ChannelHeader extends React.Component {
id='channelHeader.removeFromFavorites'
defaultMessage='Remove from Favorites'
/> :
- <FormattedMessage
- id='channelHeader.addToFavorites'
- defaultMessage='Add to Favorites'
- />}
+ <FormattedMessage
+ id='channelHeader.addToFavorites'
+ defaultMessage='Add to Favorites'
+ />}
</Tooltip>
);
const toggleFavorite = (
diff --git a/webapp/components/code_preview.jsx b/webapp/components/code_preview.jsx
index b06d9855a..6afe45c2e 100644
--- a/webapp/components/code_preview.jsx
+++ b/webapp/components/code_preview.jsx
@@ -58,8 +58,12 @@ export default class CodePreview extends React.Component {
}
handleReceivedCode(data) {
+ let code = data;
+ if (data.nodeName === '#document') {
+ code = new XMLSerializer().serializeToString(data);
+ }
this.setState({
- code: data,
+ code,
loading: false,
success: true
});
diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx
index 50e7b340b..a557a48c5 100644
--- a/webapp/components/create_team/components/display_name.jsx
+++ b/webapp/components/create_team/components/display_name.jsx
@@ -27,10 +27,24 @@ export default class TeamSignupDisplayNamePage extends React.Component {
var displayName = ReactDOM.findDOMNode(this.refs.name).value.trim();
if (!displayName) {
- this.setState({nameError: Utils.localizeMessage('create_team.display_name.required', 'This field is required')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.display_name.required'
+ defaultMessage='This field is required'
+ />)
+ });
return;
} else if (displayName.length < Constants.MIN_TEAMNAME_LENGTH || displayName.length > Constants.MAX_TEAMNAME_LENGTH) {
- this.setState({nameError: Utils.localizeMessage('create_team.display_name.charLength', 'Name must be 2 or more characters up to a maximum of 15')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.display_name.charLength'
+ defaultMessage='Name must be {min} or more characters up to a maximum of {max}'
+ values={{
+ min: Constants.MIN_TEAMNAME_LENGTH,
+ max: Constants.MAX_TEAMNAME_LENGTH
+ }}
+ />)
+ });
return;
}
diff --git a/webapp/components/create_team/components/team_url.jsx b/webapp/components/create_team/components/team_url.jsx
index 4bea240da..cff0002e0 100644
--- a/webapp/components/create_team/components/team_url.jsx
+++ b/webapp/components/create_team/components/team_url.jsx
@@ -42,26 +42,47 @@ export default class TeamUrl extends React.Component {
const urlRegex = /^[a-z]+([a-z\-0-9]+|(__)?)[a-z0-9]+$/g;
if (!name) {
- this.setState({nameError: Utils.localizeMessage('create_team.team_url.required', 'This field is required')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.team_url.required'
+ defaultMessage='This field is required'
+ />)
+ });
return;
}
if (cleanedName.length < Constants.MIN_TEAMNAME_LENGTH || cleanedName.length > Constants.MAX_TEAMNAME_LENGTH) {
- this.setState({nameError: Utils.localizeMessage('create_team.team_url.charLength', 'Name must be 4 or more characters up to a maximum of 15')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.team_url.charLength'
+ defaultMessage='Name must be {min} or more characters up to a maximum of {max}'
+ values={{
+ min: Constants.MIN_TEAMNAME_LENGTH,
+ max: Constants.MAX_TEAMNAME_LENGTH
+ }}
+ />)
+ });
return;
}
if (cleanedName !== name || !urlRegex.test(name)) {
- this.setState({nameError: Utils.localizeMessage('create_team.team_url.regex', "Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash.")});
- return;
- } else if (cleanedName.length < Constants.MIN_TEAMNAME_LENGTH || cleanedName.length > Constants.MAX_TEAMNAME_LENGTH) {
- this.setState({nameError: Utils.localizeMessage('create_team.team_url.charLength', 'Name must be 2 or more characters up to a maximum of 15')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.team_url.regex'
+ defaultMessage="Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."
+ />)
+ });
return;
}
for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) {
if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) {
- this.setState({nameError: Utils.localizeMessage('create_team.team_url.taken', 'URL is taken or contains a reserved word')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.team_url.taken'
+ defaultMessage='URL is taken or contains a reserved word'
+ />)
+ });
return;
}
}
@@ -74,7 +95,12 @@ export default class TeamUrl extends React.Component {
checkIfTeamExists(name,
(foundTeam) => {
if (foundTeam) {
- this.setState({nameError: Utils.localizeMessage('create_team.team_url.unavailable', 'This URL is unavailable. Please try another.')});
+ this.setState({nameError: (
+ <FormattedMessage
+ id='create_team.team_url.unavailable'
+ defaultMessage='This URL is unavailable. Please try another.'
+ />)
+ });
this.setState({isLoading: false});
return;
}
diff --git a/webapp/components/integrations/components/edit_command.jsx b/webapp/components/integrations/components/edit_command.jsx
new file mode 100644
index 000000000..395c977ca
--- /dev/null
+++ b/webapp/components/integrations/components/edit_command.jsx
@@ -0,0 +1,731 @@
+// 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 IntegrationStore from 'stores/integration_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {loadTeamCommands} from 'actions/integration_actions.jsx';
+import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {browserHistory, Link} from 'react-router/es6';
+import SpinnerButton from 'components/spinner_button.jsx';
+import Constants from 'utils/constants.jsx';
+import ConfirmModal from 'components/confirm_modal.jsx';
+
+const REQUEST_POST = 'P';
+const REQUEST_GET = 'G';
+
+export default class EditCommand extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired,
+ location: React.PropTypes.object
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+
+ this.submitCommand = this.submitCommand.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleUpdate = this.handleUpdate.bind(this);
+ this.handleConfirmModal = this.handleConfirmModal.bind(this);
+ this.confirmModalDismissed = this.confirmModalDismissed.bind(this);
+
+ this.updateDisplayName = this.updateDisplayName.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateTrigger = this.updateTrigger.bind(this);
+ this.updateUrl = this.updateUrl.bind(this);
+ this.updateMethod = this.updateMethod.bind(this);
+ this.updateUsername = this.updateUsername.bind(this);
+ this.updateIconUrl = this.updateIconUrl.bind(this);
+ this.updateAutocomplete = this.updateAutocomplete.bind(this);
+ this.updateAutocompleteHint = this.updateAutocompleteHint.bind(this);
+ this.updateAutocompleteDescription = this.updateAutocompleteDescription.bind(this);
+
+ this.originalCommand = null;
+ this.newCommand = null;
+
+ const teamId = TeamStore.getCurrentId();
+
+ this.state = {
+ displayName: '',
+ description: '',
+ trigger: '',
+ url: '',
+ method: REQUEST_POST,
+ username: '',
+ iconUrl: '',
+ autocomplete: false,
+ autocompleteHint: '',
+ autocompleteDescription: '',
+ saving: false,
+ serverError: '',
+ clientError: null,
+ showConfirmModal: false,
+ commands: IntegrationStore.getCommands(teamId),
+ loading: !IntegrationStore.hasReceivedCommands(teamId)
+ };
+ }
+
+ componentDidMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableCommands === 'true') {
+ loadTeamCommands();
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleConfirmModal() {
+ this.setState({showConfirmModal: true});
+ }
+
+ confirmModalDismissed() {
+ this.setState({showConfirmModal: false});
+ }
+
+ submitCommand() {
+ AsyncClient.editCommand(
+ this.newCmd,
+ browserHistory.push('/' + this.props.team.name + '/integrations/commands'),
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ handleUpdate() {
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ this.submitCommand();
+ }
+
+ handleIntegrationChange() {
+ const teamId = TeamStore.getCurrentId();
+
+ this.setState({
+ commands: IntegrationStore.getCommands(teamId),
+ loading: !IntegrationStore.hasReceivedCommands(teamId)
+ });
+
+ if (!this.state.loading) {
+ this.originalCommand = this.state.commands.filter((command) => command.id === this.props.location.query.id)[0];
+
+ this.setState({
+ displayName: this.originalCommand.display_name,
+ description: this.originalCommand.description,
+ trigger: this.originalCommand.trigger,
+ url: this.originalCommand.url,
+ method: this.originalCommand.method,
+ username: this.originalCommand.username,
+ iconUrl: this.originalCommand.icon_url,
+ autocomplete: this.originalCommand.auto_complete,
+ autocompleteHint: this.originalCommand.auto_complete_hint,
+ autocompleteDescription: this.originalCommand.auto_complete_desc
+ });
+ }
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ let triggerWord = this.state.trigger.trim().toLowerCase();
+ if (triggerWord.indexOf('/') === 0) {
+ triggerWord = triggerWord.substr(1);
+ }
+
+ const command = {
+ display_name: this.state.displayName,
+ description: this.state.description,
+ trigger: triggerWord,
+ url: this.state.url.trim(),
+ method: this.state.method,
+ username: this.state.username,
+ icon_url: this.state.iconUrl,
+ auto_complete: this.state.autocomplete
+ };
+
+ if (this.originalCommand.id) {
+ command.id = this.originalCommand.id;
+ }
+
+ if (command.auto_complete) {
+ command.auto_complete_desc = this.state.autocompleteDescription;
+ command.auto_complete_hint = this.state.autocompleteHint;
+ }
+
+ if (!command.trigger) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerRequired'
+ defaultMessage='A trigger word is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (command.trigger.indexOf('/') === 0) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSlash'
+ defaultMessage='A trigger word cannot begin with a /'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (command.trigger.indexOf(' ') !== -1) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSpace'
+ defaultMessage='A trigger word must not contain spaces'
+ />
+ )
+ });
+ return;
+ }
+
+ if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidLength'
+ defaultMessage='A trigger word must contain between {min} and {max} characters'
+ values={{
+ min: Constants.MIN_TRIGGER_LENGTH,
+ max: Constants.MAX_TRIGGER_LENGTH
+ }}
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!command.url) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.urlRequired'
+ defaultMessage='A request URL is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ this.newCmd = command;
+
+ if (this.originalCommand.url !== this.newCmd.url || this.originalCommand.trigger !== this.newCmd.trigger || this.originalCommand.method !== this.newCmd.method) {
+ this.handleConfirmModal();
+ this.setState({
+ saving: false
+ });
+ } else {
+ this.submitCommand();
+ }
+ }
+
+ updateDisplayName(e) {
+ this.setState({
+ displayName: e.target.value
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateTrigger(e) {
+ this.setState({
+ trigger: e.target.value
+ });
+ }
+
+ updateUrl(e) {
+ this.setState({
+ url: e.target.value
+ });
+ }
+
+ updateMethod(e) {
+ this.setState({
+ method: e.target.value
+ });
+ }
+
+ updateUsername(e) {
+ this.setState({
+ username: e.target.value
+ });
+ }
+
+ updateIconUrl(e) {
+ this.setState({
+ iconUrl: e.target.value
+ });
+ }
+
+ updateAutocomplete(e) {
+ this.setState({
+ autocomplete: e.target.checked
+ });
+ }
+
+ updateAutocompleteHint(e) {
+ this.setState({
+ autocompleteHint: e.target.value
+ });
+ }
+
+ updateAutocompleteDescription(e) {
+ this.setState({
+ autocompleteDescription: e.target.value
+ });
+ }
+
+ render() {
+ const confirmButton = (
+ <FormattedMessage
+ id='update_command.update'
+ defaultMessage='Update'
+ />
+ );
+
+ const confirmTitle = (
+ <FormattedMessage
+ id='update_command.confirm'
+ defaultMessage='Edit Slash Command'
+ />
+ );
+
+ const confirmMessage = (
+ <FormattedMessage
+ id='update_command.question'
+ defaultMessage='Your changes may break the existing slash command. Are you sure you would like to update it?'
+ />
+ );
+
+ let autocompleteFields = null;
+ if (this.state.autocomplete) {
+ autocompleteFields = [(
+ <div
+ key='autocompleteHint'
+ className='form-group'
+ >
+ <label
+ className='control-label col-sm-4'
+ htmlFor='autocompleteHint'
+ >
+ <FormattedMessage
+ id='add_command.autocompleteHint'
+ defaultMessage='Autocomplete Hint'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='autocompleteHint'
+ type='text'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.autocompleteHint}
+ onChange={this.updateAutocompleteHint}
+ placeholder={Utils.localizeMessage('add_command.autocompleteHint.placeholder', 'Example: [Patient Name]')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.autocompleteHint.help'
+ defaultMessage='(Optional) Arguments associated with your slash command, displayed as help in the autocomplete list.'
+ />
+ </div>
+ </div>
+ </div>
+ ),
+ (
+ <div
+ key='autocompleteDescription'
+ className='form-group'
+ >
+ <label
+ className='control-label col-sm-4'
+ htmlFor='autocompleteDescription'
+ >
+ <FormattedMessage
+ id='add_command.autocompleteDescription'
+ defaultMessage='Autocomplete Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='description'
+ type='text'
+ maxLength='128'
+ className='form-control'
+ value={this.state.autocompleteDescription}
+ onChange={this.updateAutocompleteDescription}
+ placeholder={Utils.localizeMessage('add_command.autocompleteDescription.placeholder', 'Example: "Returns search results for patient records"')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.autocompleteDescription.help'
+ defaultMessage='(Optional) Short description of slash command for the autocomplete list.'
+ />
+ </div>
+ </div>
+ </div>
+ )];
+ }
+
+ return (
+ <div className='backstage-content row'>
+ <BackstageHeader>
+ <Link to={'/' + this.props.team.name + '/integrations/commands'}>
+ <FormattedMessage
+ id='installed_command.header'
+ defaultMessage='Slash Commands'
+ />
+ </Link>
+ <FormattedMessage
+ id='integrations.edit'
+ defaultMessage='Edit'
+ />
+ </BackstageHeader>
+ <div className='backstage-form'>
+ <form
+ className='form-horizontal'
+ onSubmit={this.handleSubmit}
+ >
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='displayName'
+ >
+ <FormattedMessage
+ id='add_command.displayName'
+ defaultMessage='Display Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='displayName'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.displayName}
+ onChange={this.updateDisplayName}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.displayName.help'
+ defaultMessage='Display name for your slash command made of up to 64 characters.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='add_command.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='description'
+ type='text'
+ maxLength='128'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.description.help'
+ defaultMessage='Description for your incoming webhook.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='trigger'
+ >
+ <FormattedMessage
+ id='add_command.trigger'
+ defaultMessage='Command Trigger Word'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='trigger'
+ type='text'
+ maxLength={Constants.MAX_TRIGGER_LENGTH}
+ className='form-control'
+ value={this.state.trigger}
+ onChange={this.updateTrigger}
+ placeholder={Utils.localizeMessage('add_command.trigger.placeholder', 'Command trigger e.g. "hello" not including the slash')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.trigger.help'
+ defaultMessage='Trigger word must be unique, and cannot begin with a slash or contain any spaces.'
+ />
+ </div>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.trigger.helpExamples'
+ defaultMessage='Examples: client, employee, patient, weather'
+ />
+ </div>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.trigger.helpReserved'
+ defaultMessage='Reserved: {link}'
+ values={{
+ link: (
+ <a
+ href='https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands'
+ target='_blank'
+ rel='noopener noreferrer'
+ >
+ <FormattedMessage
+ id='add_command.trigger.helpReservedLinkText'
+ defaultMessage='see list of built-in slash commands'
+ />
+ </a>
+ )
+ }}
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='url'
+ >
+ <FormattedMessage
+ id='add_command.url'
+ defaultMessage='Request URL'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='url'
+ type='text'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.url}
+ onChange={this.updateUrl}
+ placeholder={Utils.localizeMessage('add_command.url.placeholder', 'Must start with http:// or https://')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.url.help'
+ defaultMessage='The callback URL to receive the HTTP POST or GET event request when the slash command is run.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='method'
+ >
+ <FormattedMessage
+ id='add_command.method'
+ defaultMessage='Request Method'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <select
+ id='method'
+ className='form-control'
+ value={this.state.method}
+ onChange={this.updateMethod}
+ >
+ <option value={REQUEST_POST}>
+ {Utils.localizeMessage('add_command.method.post', 'POST')}
+ </option>
+ <option value={REQUEST_GET}>
+ {Utils.localizeMessage('add_command.method.get', 'GET')}
+ </option>
+ </select>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.method.help'
+ defaultMessage='The type of command request issued to the Request URL.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='username'
+ >
+ <FormattedMessage
+ id='add_command.username'
+ defaultMessage='Response Username'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='username'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.username}
+ onChange={this.updateUsername}
+ placholder={Utils.localizeMessage('add_command.username.placeholder', 'Username')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.username.help'
+ defaultMessage='(Optional) Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols "-", "_", and "." .'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='iconUrl'
+ >
+ <FormattedMessage
+ id='add_command.iconUrl'
+ defaultMessage='Response Icon'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='iconUrl'
+ type='text'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.iconUrl}
+ onChange={this.updateIconUrl}
+ placeholder={Utils.localizeMessage('add_command.iconUrl.placeholder', 'https://www.example.com/myicon.png')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.iconUrl.help'
+ defaultMessage='(Optional) Choose a profile picture override for the post responses to this slash command. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='autocomplete'
+ >
+ <FormattedMessage
+ id='add_command.autocomplete'
+ defaultMessage='Autocomplete'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8 checkbox'>
+ <input
+ id='autocomplete'
+ type='checkbox'
+ checked={this.state.autocomplete}
+ onChange={this.updateAutocomplete}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.autocomplete.help'
+ defaultMessage='(Optional) Show slash command in autocomplete list.'
+ />
+ </div>
+ </div>
+ </div>
+ {autocompleteFields}
+ <div className='backstage-form__footer'>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
+ <Link
+ className='btn btn-sm'
+ to={'/' + this.props.team.name + '/integrations/commands'}
+ >
+ <FormattedMessage
+ id='add_command.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ disabled={this.state.loading}
+ >
+ <FormattedMessage
+ id='edit_command.save'
+ defaultMessage='Update'
+ />
+ </SpinnerButton>
+ <ConfirmModal
+ title={confirmTitle}
+ message={confirmMessage}
+ confirmButton={confirmButton}
+ show={this.state.showConfirmModal}
+ onConfirm={this.handleUpdate}
+ onCancel={this.confirmModalDismissed}
+ />
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/installed_command.jsx b/webapp/components/integrations/components/installed_command.jsx
index f149a21ac..ecd7d9608 100644
--- a/webapp/components/integrations/components/installed_command.jsx
+++ b/webapp/components/integrations/components/installed_command.jsx
@@ -130,6 +130,15 @@ export default class InstalledCommand extends React.Component {
</a>
{' - '}
<a
+ href={'edit?id=' + command.id}
+ >
+ <FormattedMessage
+ id='installed_integrations.edit'
+ defaultMessage='Edit'
+ />
+ </a>
+ {' - '}
+ <a
href='#'
onClick={this.handleDelete}
>
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index ec4ca2a6a..841061d48 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -109,6 +109,10 @@ export default class LoggedIn extends React.Component {
// Listen for user
UserStore.addChangeListener(this.onUserChanged);
+ // Listen for focussed tab/window state
+ window.addEventListener('focus', this.onFocusListener);
+ window.addEventListener('blur', this.onBlurListener);
+
// ???
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
@@ -166,6 +170,10 @@ export default class LoggedIn extends React.Component {
$('.modal').off('show.bs.modal');
$(window).off('keydown.preventBackspace');
+
+ // Listen for focussed tab/window state
+ window.removeEventListener('focus', this.onFocusListener);
+ window.removeEventListener('blur', this.onBlurListener);
}
render() {
@@ -177,6 +185,14 @@ export default class LoggedIn extends React.Component {
user: this.state.user
});
}
+
+ onFocusListener() {
+ GlobalActions.emitBrowserFocus(true);
+ }
+
+ onBlurListener() {
+ GlobalActions.emitBrowserFocus(false);
+ }
}
LoggedIn.propTypes = {
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index fd5413c17..ae33e489f 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -150,8 +150,8 @@ export default class LoginController extends React.Component {
query.d,
query.h,
query.id,
- () => {
- this.finishSignin();
+ (team) => {
+ this.finishSignin(team);
},
() => {
// there's not really a good way to deal with this, so just let the user log in like normal
@@ -167,7 +167,6 @@ export default class LoginController extends React.Component {
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
browserHistory.push('/should_verify_email?&email=' + encodeURIComponent(loginId));
- return;
} else if (err.id === 'store.sql_user.get_for_login.app_error' ||
err.id === 'ent.ldap.do_login.user_not_registered.app_error') {
this.setState({
@@ -196,13 +195,15 @@ export default class LoginController extends React.Component {
);
}
- finishSignin() {
+ finishSignin(team) {
GlobalActions.emitInitialLoad(
() => {
const query = this.props.location.query;
GlobalActions.loadDefaultLocale();
if (query.redirect_to) {
browserHistory.push(query.redirect_to);
+ } else if (team) {
+ browserHistory.push(`/${team.name}`);
} else {
browserHistory.push('/select_team');
}
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index 50ab5224a..f8cf64867 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -105,7 +105,7 @@ export default class MoreDirectChannels extends React.Component {
let users;
if (this.state.listType === 'any') {
- users = UserStore.getProfileList();
+ users = UserStore.getProfileList(true);
} else {
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true);
}
@@ -119,7 +119,7 @@ export default class MoreDirectChannels extends React.Component {
const listType = e.target.value;
let users;
if (listType === 'any') {
- users = UserStore.getProfileList();
+ users = UserStore.getProfileList(true);
} else {
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true);
}
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index d71fec945..0a5f04394 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -501,10 +501,10 @@ export default class Navbar extends React.Component {
id='channelHeader.removeFromFavorites'
defaultMessage='Remove from Favorites'
/> :
- <FormattedMessage
- id='channelHeader.addToFavorites'
- defaultMessage='Add to Favorites'
- />}
+ <FormattedMessage
+ id='channelHeader.addToFavorites'
+ defaultMessage='Add to Favorites'
+ />}
</a>
</li>
);
diff --git a/webapp/components/new_channel_flow.jsx b/webapp/components/new_channel_flow.jsx
index c6c265725..b37e6cf35 100644
--- a/webapp/components/new_channel_flow.jsx
+++ b/webapp/components/new_channel_flow.jsx
@@ -53,6 +53,7 @@ class NewChannelFlow extends React.Component {
super(props);
this.doSubmit = this.doSubmit.bind(this);
+ this.onModalExited = this.onModalExited.bind(this);
this.typeSwitched = this.typeSwitched.bind(this);
this.urlChangeRequested = this.urlChangeRequested.bind(this);
this.urlChangeSubmitted = this.urlChangeSubmitted.bind(this);
@@ -117,8 +118,11 @@ class NewChannelFlow extends React.Component {
member: data2.member
});
+ this.doOnModalExited = () => {
+ browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + data2.channel.name);
+ };
+
this.props.onModalDismissed();
- browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + data2.channel.name);
}
);
},
@@ -143,6 +147,11 @@ class NewChannelFlow extends React.Component {
}
);
}
+ onModalExited() {
+ if (this.doOnModalExited) {
+ this.doOnModalExited();
+ }
+ }
typeSwitched() {
if (this.state.channelType === 'P') {
this.setState({channelType: 'O'});
@@ -223,6 +232,7 @@ class NewChannelFlow extends React.Component {
serverError={this.state.serverError}
onSubmitChannel={this.doSubmit}
onModalDismissed={this.props.onModalDismissed}
+ onModalExited={this.onModalExited}
onTypeSwitched={this.typeSwitched}
onChangeURLPressed={this.urlChangeRequested}
onDataChanged={this.channelDataChanged}
@@ -233,6 +243,7 @@ class NewChannelFlow extends React.Component {
channelData={channelData}
serverError={this.state.serverError}
onSubmitChannel={this.doSubmit}
+ onModalExited={this.onModalExited}
onModalDismissed={this.props.onModalDismissed}
onTypeSwitched={this.typeSwitched}
onChangeURLPressed={this.urlChangeRequested}
@@ -248,6 +259,7 @@ class NewChannelFlow extends React.Component {
serverError={this.state.serverError}
onModalSubmit={this.urlChangeSubmitted}
onModalDismissed={this.urlChangeDismissed}
+ onModalExited={this.onModalExited}
/>
</span>
);
diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx
index 4122c3bfb..fa52c56a7 100644
--- a/webapp/components/new_channel_modal.jsx
+++ b/webapp/components/new_channel_modal.jsx
@@ -206,9 +206,11 @@ class NewChannelModal extends React.Component {
return (
<span>
<Modal
+ dialogClassName='new-channel__modal'
show={this.props.show}
bsSize='large'
onHide={this.props.onModalDismissed}
+ onExited={this.props.onModalExited}
>
<Modal.Header closeButton={true}>
<Modal.Title>
@@ -382,6 +384,7 @@ NewChannelModal.propTypes = {
serverError: React.PropTypes.node,
onSubmitChannel: React.PropTypes.func.isRequired,
onModalDismissed: React.PropTypes.func.isRequired,
+ onModalExited: React.PropTypes.func.optional,
onTypeSwitched: React.PropTypes.func.isRequired,
onChangeURLPressed: React.PropTypes.func.isRequired,
onDataChanged: React.PropTypes.func.isRequired
diff --git a/webapp/components/password_reset_send_link.jsx b/webapp/components/password_reset_send_link.jsx
index 18741b816..1cd532855 100644
--- a/webapp/components/password_reset_send_link.jsx
+++ b/webapp/components/password_reset_send_link.jsx
@@ -52,14 +52,14 @@ class PasswordResetSendLink extends React.Component {
<div className='reset-form alert alert-success'>
<FormattedHTMLMessage
id='password_send.link'
- defaultMessage='<p>A password reset link has been sent to <b>{email}</b></p>'
+ defaultMessage='If the account exists, a password reset email will be sent to: <br/><b>{email}</b><br/><br/>'
values={{
email
}}
/>
<FormattedMessage
- id={'password_send.checkInbox'}
- defaultMessage={'Please check your inbox.'}
+ id='password_send.checkInbox'
+ defaultMessage='Please check your inbox.'
/>
</div>
)
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index f128245e4..8d50338e0 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -19,7 +19,7 @@ import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
function getStateFromStores() {
- const results = SearchStore.getSearchResults();
+ const results = JSON.parse(JSON.stringify(SearchStore.getSearchResults()));
const channels = new Map();
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 698d68bba..d9955a136 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -111,42 +111,132 @@ export default class SearchResultsItem extends React.Component {
compactClass = 'post--compact';
}
- let flag;
- let flagFunc;
- let flagVisible = '';
- let flagTooltip = (
- <Tooltip id='flagTooltip'>
- <FormattedMessage
- id='flag_post.flag'
- defaultMessage='Flag for follow up'
- />
- </Tooltip>
- );
- if (this.props.isFlagged) {
- flagVisible = 'visible';
- flagTooltip = (
+ let message;
+ let flagContent;
+ let rhsControls;
+ if (post.state === Constants.POST_DELETED) {
+ message = (
+ <p>
+ <FormattedMessage
+ id='post_body.deleted'
+ defaultMessage='(message deleted)'
+ />
+ </p>
+ );
+ } else {
+ let flag;
+ let flagFunc;
+ let flagVisible = '';
+ let flagTooltip = (
<Tooltip id='flagTooltip'>
<FormattedMessage
- id='flag_post.unflag'
- defaultMessage='Unflag'
+ id='flag_post.flag'
+ defaultMessage='Flag for follow up'
/>
</Tooltip>
);
- flagFunc = this.unflagPost;
- flag = (
- <span
- className='icon'
- dangerouslySetInnerHTML={{__html: flagIcon}}
- />
+
+ if (this.props.isFlagged) {
+ flagVisible = 'visible';
+ flagTooltip = (
+ <Tooltip id='flagTooltip'>
+ <FormattedMessage
+ id='flag_post.unflag'
+ defaultMessage='Unflag'
+ />
+ </Tooltip>
+ );
+ flagFunc = this.unflagPost;
+ flag = (
+ <span
+ className='icon'
+ dangerouslySetInnerHTML={{__html: flagIcon}}
+ />
+ );
+ } else {
+ flag = (
+ <span
+ className='icon'
+ dangerouslySetInnerHTML={{__html: flagIcon}}
+ />
+ );
+ flagFunc = this.flagPost;
+ }
+
+ flagContent = (
+ <OverlayTrigger
+ delayShow={Constants.OVERLAY_TIME_DELAY}
+ placement='top'
+ overlay={flagTooltip}
+ >
+ <a
+ href='#'
+ className={'flag-icon__container ' + flagVisible}
+ onClick={flagFunc}
+ >
+ {flag}
+ </a>
+ </OverlayTrigger>
);
- } else {
- flag = (
- <span
- className='icon'
- dangerouslySetInnerHTML={{__html: flagIcon}}
+
+ rhsControls = (
+ <li className='col__controls'>
+ <a
+ href='#'
+ className='comment-icon__container search-item__comment'
+ onClick={this.handleFocusRHSClick}
+ >
+ <span
+ className='comment-icon'
+ dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}}
+ />
+ </a>
+ <a
+ onClick={
+ () => {
+ if (Utils.isMobile()) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH,
+ results: null
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH_TERM,
+ term: null,
+ do_search: false,
+ is_mention_search: false
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POST_SELECTED,
+ postId: null
+ });
+
+ this.hideSidebar();
+ }
+ this.shrinkSidebar();
+ browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/pl/' + post.id);
+ }
+ }
+ className='search-item__jump'
+ >
+ <FormattedMessage
+ id='search_item.jump'
+ defaultMessage='Jump'
+ />
+ </a>
+ </li>
+ );
+
+ message = (
+ <PostMessageContainer
+ post={post}
+ options={{
+ searchTerm: this.props.term,
+ mentionHighlight: this.props.isMentionSearch
+ }}
/>
);
- flagFunc = this.flagPost;
}
return (
@@ -187,75 +277,12 @@ export default class SearchResultsItem extends React.Component {
minute='2-digit'
/>
</time>
- <OverlayTrigger
- delayShow={Constants.OVERLAY_TIME_DELAY}
- placement='top'
- overlay={flagTooltip}
- >
- <a
- href='#'
- className={'flag-icon__container ' + flagVisible}
- onClick={flagFunc}
- >
- {flag}
- </a>
- </OverlayTrigger>
- </li>
- <li className='col__controls'>
- <a
- href='#'
- className='comment-icon__container search-item__comment'
- onClick={this.handleFocusRHSClick}
- >
- <span
- className='comment-icon'
- dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}}
- />
- </a>
- <a
- onClick={
- () => {
- if (Utils.isMobile()) {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_SEARCH,
- results: null
- });
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_SEARCH_TERM,
- term: null,
- do_search: false,
- is_mention_search: false
- });
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST_SELECTED,
- postId: null
- });
-
- this.hideSidebar();
- }
- this.shrinkSidebar();
- browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/pl/' + post.id);
- }
- }
- className='search-item__jump'
- >
- <FormattedMessage
- id='search_item.jump'
- defaultMessage='Jump'
- />
- </a>
+ {flagContent}
</li>
+ {rhsControls}
</ul>
<div className='search-item-snippet'>
- <PostMessageContainer
- post={post}
- options={{
- searchTerm: this.props.term,
- mentionHighlight: this.props.isMentionSearch
- }}
- />
+ {message}
</div>
</div>
</div>
diff --git a/webapp/components/sidebar_right_menu.jsx b/webapp/components/sidebar_right_menu.jsx
index 86026967a..f201adfcf 100644
--- a/webapp/components/sidebar_right_menu.jsx
+++ b/webapp/components/sidebar_right_menu.jsx
@@ -68,6 +68,7 @@ export default class SidebarRightMenu extends React.Component {
getFlagged(e) {
e.preventDefault();
getFlaggedPosts();
+ this.hideSidebars();
}
componentDidMount() {
diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx
index 2d4b3f277..b67179604 100644
--- a/webapp/components/signup/components/signup_email.jsx
+++ b/webapp/components/signup/components/signup_email.jsx
@@ -429,9 +429,11 @@ export default class SignupEmail extends React.Component {
<p>
<FormattedHTMLMessage
id='create_team.agreement'
- defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}."
+ defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='{TermsOfServiceLink}'>Terms of Service</a> and <a href='{PrivacyPolicyLink}'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}."
values={{
- siteName: global.window.mm_config.SiteName
+ siteName: global.window.mm_config.SiteName,
+ TermsOfServiceLink: global.window.mm_config.TermsOfServiceLink,
+ PrivacyPolicyLink: global.window.mm_config.PrivacyPolicyLink
}}
/>
</p>
diff --git a/webapp/components/signup/components/signup_ldap.jsx b/webapp/components/signup/components/signup_ldap.jsx
index 8c1b1bafb..bc8c073ad 100644
--- a/webapp/components/signup/components/signup_ldap.jsx
+++ b/webapp/components/signup/components/signup_ldap.jsx
@@ -179,9 +179,11 @@ export default class SignupLdap extends React.Component {
<p>
<FormattedHTMLMessage
id='create_team.agreement'
- defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}."
+ defaultMessage="By proceeding to create your account and use {siteName}, you agree to our <a href='{TermsOfServiceLink}'>Terms of Service</a> and <a href='{PrivacyPolicyLink}'>Privacy Policy</a>. If you do not agree, you cannot use {siteName}."
values={{
- siteName: global.window.mm_config.SiteName
+ siteName: global.window.mm_config.SiteName,
+ TermsOfServiceLink: global.window.mm_config.TermsOfServiceLink,
+ PrivacyPolicyLink: global.window.mm_config.PrivacyPolicyLink
}}
/>
</p>
diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx
index d1a03deb5..6118b8d98 100644
--- a/webapp/components/suggestion/at_mention_provider.jsx
+++ b/webapp/components/suggestion/at_mention_provider.jsx
@@ -4,6 +4,7 @@
import Suggestion from './suggestion.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import SuggestionStore from 'stores/suggestion_store.jsx';
import {autocompleteUsersInChannel} from 'actions/user_actions.jsx';
@@ -106,13 +107,24 @@ export default class AtMentionProvider {
}
componentWillUnmount() {
- clearTimeout(this.timeoutId);
+ this.clearTimeout(this.timeoutId);
+ }
+
+ clearTimeout() {
+ if (this.timeoutId) {
+ clearTimeout(this.timeoutId);
+ this.timeoutId = '';
+
+ return true;
+ }
+
+ return false;
}
handlePretextChanged(suggestionId, pretext) {
- clearTimeout(this.timeoutId);
+ const hadSuggestions = this.clearTimeout(this.timeoutId);
- const captured = (/(?:^|\W)@([a-z0-9\-\._]*)$/i).exec(pretext.toLowerCase());
+ const captured = (/(?:^|\W)@([a-z0-9\-._]*)$/i).exec(pretext.toLowerCase());
if (captured) {
const prefix = captured[1];
@@ -160,5 +172,10 @@ export default class AtMentionProvider {
Constants.AUTOCOMPLETE_TIMEOUT
);
}
+
+ if (hadSuggestions) {
+ // Clear the suggestions since the user has now typed something invalid
+ SuggestionStore.clearSuggestions(suggestionId);
+ }
}
}
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index eeae5ba28..3a8cd65cf 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -39,7 +39,8 @@ export default class SuggestionBox extends React.Component {
componentWillReceiveProps(nextProps) {
// Clear any suggestions when the SuggestionBox is cleared
if (nextProps.value === '' && this.props.value !== nextProps.value) {
- GlobalActions.emitClearSuggestions(this.suggestionId);
+ // TODO - Find a better way to not "dispatch during dispatch"
+ setTimeout(() => GlobalActions.emitClearSuggestions(this.suggestionId), 1);
}
}
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 1d749f480..a5281d238 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -5,12 +5,11 @@ import $ from 'jquery';
import SettingItemMin from './setting_item_min.jsx';
import SettingItemMax from './setting_item_max.jsx';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
-import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {updateTeam} from 'actions/team_actions.jsx';
const holders = defineMessages({
dirDisabled: {
@@ -131,10 +130,8 @@ class GeneralTab extends React.Component {
var data = this.props.team;
data.allow_open_invite = this.state.allow_open_invite;
- Client.updateTeam(data,
- (team) => {
- TeamStore.saveTeam(team);
- TeamStore.emitChange();
+ updateTeam(data,
+ () => {
this.updateSection('');
},
(err) => {
@@ -170,10 +167,8 @@ class GeneralTab extends React.Component {
var data = this.props.team;
data.display_name = this.state.name;
- Client.updateTeam(data,
- (team) => {
- TeamStore.saveTeam(team);
- TeamStore.emitChange();
+ updateTeam(data,
+ () => {
this.updateSection('');
},
(err) => {
@@ -205,10 +200,8 @@ class GeneralTab extends React.Component {
var data = this.props.team;
data.invite_id = this.state.invite_id;
- Client.updateTeam(data,
- (team) => {
- TeamStore.saveTeam(team);
- TeamStore.emitChange();
+ updateTeam(data,
+ () => {
this.updateSection('');
},
(err) => {
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index e69d917a3..21dbf9699 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -11,7 +11,7 @@ import Constants from 'utils/constants.jsx';
const UserStatuses = Constants.UserStatuses;
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
-import {Popover, OverlayTrigger} from 'react-bootstrap';
+import {Popover, OverlayTrigger, Tooltip} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import React from 'react';
@@ -111,8 +111,19 @@ export default class UserProfile extends React.Component {
defaultMessage='New call unavailable until your existing call ends'
/>
);
+ } else {
+ webrtcMessage = (
+ <FormattedMessage
+ id='user_profile.webrtc.offline'
+ defaultMessage='The user is offline'
+ />
+ );
}
+ const webrtcTooltip = (
+ <Tooltip id='webrtcTooltip'>{webrtcMessage}</Tooltip>
+ );
+
webrtc = (
<div
className='webrtc__user-profile'
@@ -123,28 +134,18 @@ export default class UserProfile extends React.Component {
onClick={() => this.initWebrtc()}
disabled={!isOnline}
>
- <svg
- id='webrtc-btn'
- className='webrtc__button'
- xmlns='http://www.w3.org/2000/svg'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={webrtcTooltip}
>
- <circle
- className={circleClass}
- cx='16'
- cy='16'
- r='18'
+ <div
+ id='webrtc-btn'
+ className={'webrtc__button ' + circleClass}
>
- <title>
- {webrtcMessage}
- </title>
- </circle>
- <path
- className='off'
- transform='scale(0.4), translate(17,16)'
- d='M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm-4 24l-8-6.4V32H12V16h16v6.4l8-6.4v16z'
- fill='white'
- />
- </svg>
+ <span dangerouslySetInnerHTML={{__html: Constants.VIDEO_ICON}}/>
+ </div>
+ </OverlayTrigger>
</a>
</div>
);
diff --git a/webapp/components/user_settings/manage_languages.jsx b/webapp/components/user_settings/manage_languages.jsx
index f4ae79088..4f5eb223d 100644
--- a/webapp/components/user_settings/manage_languages.jsx
+++ b/webapp/components/user_settings/manage_languages.jsx
@@ -3,13 +3,12 @@
import SettingItemMax from '../setting_item_max.jsx';
-import Client from 'client/web_client.jsx';
import * as I18n from 'i18n/i18n.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
+import {updateUser} from 'actions/user_actions.jsx';
import React from 'react';
export default class ManageLanguage extends React.Component {
@@ -42,7 +41,7 @@ export default class ManageLanguage extends React.Component {
this.submitUser(user);
}
submitUser(user) {
- Client.updateUser(user, Constants.UserUpdateEvents.LANGUAGE,
+ updateUser(user, Constants.UserUpdateEvents.LANGUAGE,
() => {
GlobalActions.newLocalizationSelected(user.locale);
},
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index e794c4d4b..805650608 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -15,6 +15,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
+import {updateUser} from 'actions/user_actions.jsx';
const holders = defineMessages({
usernameReserved: {
@@ -187,7 +188,7 @@ class UserSettingsGeneralTab extends React.Component {
}
submitUser(user, type, emailUpdated) {
- Client.updateUser(user, type,
+ updateUser(user, type,
() => {
this.updateSection('');
AsyncClient.getMe();
@@ -461,7 +462,7 @@ class UserSettingsGeneralTab extends React.Component {
inputs.push(
<div
key='oauthEmailInfo'
- className='form-group'
+ className='padding-bottom'
>
<div className='setting-list__hint'>
<FormattedMessage
@@ -479,7 +480,7 @@ class UserSettingsGeneralTab extends React.Component {
inputs.push(
<div
key='oauthEmailInfo'
- className='form-group'
+ className='padding-bottom'
>
<div className='setting-list__hint'>
<FormattedMessage
diff --git a/webapp/components/user_settings/user_settings_modal.jsx b/webapp/components/user_settings/user_settings_modal.jsx
index 9112f8711..c1194ed78 100644
--- a/webapp/components/user_settings/user_settings_modal.jsx
+++ b/webapp/components/user_settings/user_settings_modal.jsx
@@ -104,7 +104,6 @@ class UserSettingsModal extends React.Component {
}
this.props.onModalDismissed();
- return;
}
// called after the dialog is fully hidden and faded out
@@ -251,7 +250,6 @@ class UserSettingsModal extends React.Component {
setRequireConfirm={
(requireConfirm) => {
this.requireConfirm = requireConfirm;
- return;
}
}
/>
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 0cee3dfca..5f231e499 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -289,7 +289,7 @@ export default class SecurityTab extends React.Component {
<span>
<FormattedMessage
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. If you are unable to scan the code, you can maunally enter the secret provided.'
+ 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. If you are unable to scan the code, you can manually enter the secret provided.'
/>
</span>
);
@@ -473,6 +473,20 @@ export default class SecurityTab extends React.Component {
</div>
</div>
);
+ } else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordSamlCantUpdate'
+ defaultMessage='This field is handled through your login provider. If you want to change it, you need to do so through your login provider.'
+ />
+ </div>
+ </div>
+ );
}
updateSectionStatus = function resetSection(e) {
@@ -533,7 +547,7 @@ export default class SecurityTab extends React.Component {
describe = (
<FormattedMessage
id='user.settings.security.loginGitlab'
- defaultMessage='Login done through Gitlab'
+ defaultMessage='Login done through GitLab'
/>
);
} else if (this.props.user.auth_service === Constants.LDAP_SERVICE) {
@@ -543,6 +557,13 @@ export default class SecurityTab extends React.Component {
defaultMessage='Login done through AD/LDAP'
/>
);
+ } else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginSaml'
+ defaultMessage='Login done through SAML'
+ />
+ );
}
updateSectionStatus = function updateSection() {
diff --git a/webapp/components/youtube_video.jsx b/webapp/components/youtube_video.jsx
index 93ea4f946..908a2c74e 100644
--- a/webapp/components/youtube_video.jsx
+++ b/webapp/components/youtube_video.jsx
@@ -5,7 +5,7 @@ import ChannelStore from 'stores/channel_store.jsx';
import WebClient from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
-const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^\/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&\?]*)/;
+const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
import React from 'react';