diff options
Diffstat (limited to 'webapp/components')
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'; |