diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/admin_console/service_settings.jsx | 104 | ||||
-rw-r--r-- | web/react/components/channel_loader.jsx | 10 | ||||
-rw-r--r-- | web/react/components/create_comment.jsx | 5 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 7 | ||||
-rw-r--r-- | web/react/components/file_upload.jsx | 6 | ||||
-rw-r--r-- | web/react/components/search_results_item.jsx | 118 | ||||
-rw-r--r-- | web/react/components/user_settings/manage_command_hooks.jsx | 652 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_display.jsx | 2 | ||||
-rw-r--r-- | web/react/components/user_settings/user_settings_integrations.jsx | 43 | ||||
-rw-r--r-- | web/react/utils/async_client.jsx | 19 | ||||
-rw-r--r-- | web/react/utils/client.jsx | 76 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_headers.scss | 1 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 3 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_search.scss | 24 | ||||
-rw-r--r-- | web/static/i18n/en.json | 45 | ||||
-rw-r--r-- | web/static/i18n/es.json | 8 | ||||
-rw-r--r-- | web/web_test.go | 4 |
17 files changed, 1027 insertions, 100 deletions
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index 7021900eb..2cc68d1ed 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -75,6 +75,8 @@ class ServiceSettings extends React.Component { config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked; config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked; config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked; + config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked; + config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked; //config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked; @@ -389,11 +391,105 @@ class ServiceSettings extends React.Component { <div className='form-group'> <label className='control-label col-sm-4' + htmlFor='EnableCommands' + > + <FormattedMessage + id='admin.service.cmdsTitle' + defaultMessage='Enable Slash Commands: ' + /> + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableCommands' + value='true' + ref='EnableCommands' + defaultChecked={this.props.config.ServiceSettings.EnableCommands} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.true' + defaultMessage='true' + /> + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableCommands' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnableCommands} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.false' + defaultMessage='false' + /> + </label> + <p className='help-text'> + <FormattedMessage + id='admin.service.cmdsDesc' + defaultMessage='When true, user created slash commands will be allowed.' + /> + </p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableOnlyAdminIntegrations' + > + <FormattedMessage + id='admin.service.integrationAdmin' + defaultMessage='Enable Integrations for Admin Only: ' + /> + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableOnlyAdminIntegrations' + value='true' + ref='EnableOnlyAdminIntegrations' + defaultChecked={this.props.config.ServiceSettings.EnableOnlyAdminIntegrations} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.true' + defaultMessage='true' + /> + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableOnlyAdminIntegrations' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnableOnlyAdminIntegrations} + onChange={this.handleChange} + /> + <FormattedMessage + id='admin.service.false' + defaultMessage='false' + /> + </label> + <p className='help-text'> + <FormattedMessage + id='admin.service.integrationAdminDesc' + defaultMessage='When true, user created integrations can only be created by admins.' + /> + </p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' htmlFor='EnablePostUsernameOverride' > <FormattedMessage id='admin.service.overrideTitle' - defaultMessage='Enable Overriding Usernames from Webhooks: ' + defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: ' /> </label> <div className='col-sm-8'> @@ -427,7 +523,7 @@ class ServiceSettings extends React.Component { <p className='help-text'> <FormattedMessage id='admin.service.overrideDescription' - defaultMessage='When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.' + defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.' /> </p> </div> @@ -440,7 +536,7 @@ class ServiceSettings extends React.Component { > <FormattedMessage id='admin.service.iconTitle' - defaultMessage='Enable Overriding Icon from Webhooks: ' + defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: ' /> </label> <div className='col-sm-8'> @@ -474,7 +570,7 @@ class ServiceSettings extends React.Component { <p className='help-text'> <FormattedMessage id='admin.service.iconDescription' - defaultMessage='When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.' + defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.' /> </p> </div> diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 15571ad93..174c8c4e1 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -128,6 +128,16 @@ class ChannelLoader extends React.Component { } }); + $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before'); + } else { + $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before'); + } + }); + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { if (ev.type === 'mouseenter') { $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 1b552838a..8c49315e7 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -202,8 +202,7 @@ class CreateComment extends React.Component { if (e.keyCode === KeyCodes.UP && this.state.messageText === '') { e.preventDefault(); - const channelId = ChannelStore.getCurrentId(); - const lastPost = PostStore.getCurrentUsersLatestPost(channelId, this.props.rootId); + const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId); if (!lastPost) { return; } @@ -402,4 +401,4 @@ CreateComment.propTypes = { rootId: React.PropTypes.string.isRequired }; -export default injectIntl(CreateComment);
\ No newline at end of file +export default injectIntl(CreateComment); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index ed672cd34..20892898e 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -155,15 +155,10 @@ class CreatePost extends React.Component { post.message, false, (data) => { - if (data.response === 'not implemented') { - this.sendMessage(post); - return; - } - PostStore.storeDraft(data.channel_id, null); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); - if (data.goto_location.length > 0) { + if (data.goto_location && data.goto_location.length > 0) { window.location.href = data.goto_location; } }, diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index 626dbc5b3..746289653 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -101,9 +101,9 @@ class FileUpload extends React.Component { } else if (tooLargeFiles.length > 1) { var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', '); - this.props.onUploadError(formatMessage(holders.filesAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), files: tooLargeFilenames})); + this.props.onUploadError(formatMessage(holders.filesAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), filenames: tooLargeFilenames})); } else if (tooLargeFiles.length > 0) { - this.props.onUploadError(formatMessage(holders.fileAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), file: tooLargeFiles[0].name})); + this.props.onUploadError(formatMessage(holders.fileAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), filename: tooLargeFiles[0].name})); } } @@ -329,4 +329,4 @@ FileUpload.propTypes = { postType: React.PropTypes.string }; -export default injectIntl(FileUpload);
\ No newline at end of file +export default injectIntl(FileUpload); diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 0ad091d5b..544ba920a 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -59,64 +59,74 @@ export default class SearchResultsItem extends React.Component { }; return ( - <div - className='search-item-container post' - > - <div className='search-channel__name'>{channelName}</div> - <div className='post__content'> - <div className='post__img'> - <img - src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()} - height='36' - width='36' + <div className='search-item__container'> + <div className='date-separator'> + <hr className='separator__hr' /> + <div className='separator__text'> + <FormattedDate + value={this.props.post.create_at} + day='numeric' + month='long' + year='numeric' /> </div> - <div> - <ul className='post__header'> - <li className='col__name'><strong><UserProfile userId={this.props.post.user_id} /></strong></li> - <li className='col'> - <time className='search-item-time'> - <FormattedDate - value={this.props.post.create_at} - day='numeric' - month='long' - year='numeric' - hour12={true} - hour='2-digit' - minute='2-digit' - /> - </time> - </li> - <li> - <a - href='#' - className='search-item__jump' - onClick={this.handleClick} - > - <FormattedMessage - id='search_item.jump' - defaultMessage='Jump' - /> - </a> - </li> - <li> - <a - href='#' - className='comment-icon__container search-item__comment' - onClick={this.handleFocusRHSClick} - > - <span - className='comment-icon' - dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} - /> - </a> - </li> - </ul> - <div className='search-item-snippet'> - <span - dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}} + </div> + <div + className='post' + > + <div className='search-channel__name'>{channelName}</div> + <div className='post__content'> + <div className='post__img'> + <img + src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()} + height='36' + width='36' /> </div> + <div> + <ul className='post__header'> + <li className='col__name'><strong><UserProfile userId={this.props.post.user_id} /></strong></li> + <li className='col'> + <time className='search-item-time'> + <FormattedDate + value={this.props.post.create_at} + hour12={true} + hour='2-digit' + minute='2-digit' + /> + </time> + </li> + <li> + <a + href='#' + className='search-item__jump' + onClick={this.handleClick} + > + <FormattedMessage + id='search_item.jump' + defaultMessage='Jump' + /> + </a> + </li> + <li> + <a + href='#' + className='comment-icon__container search-item__comment' + onClick={this.handleFocusRHSClick} + > + <span + className='comment-icon' + dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}} + /> + </a> + </li> + </ul> + <div className='search-item-snippet'> + <span + dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, formattingOptions)}} + /> + </div> + </div> </div> </div> </div> diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx new file mode 100644 index 000000000..bcf0a6c82 --- /dev/null +++ b/web/react/components/user_settings/manage_command_hooks.jsx @@ -0,0 +1,652 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import LoadingScreen from '../loading_screen.jsx'; + +import * as Client from '../../utils/client.jsx'; + +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; + +const holders = defineMessages({ + requestTypePost: { + id: 'user.settings.cmds.request_type_post', + defaultMessage: 'POST' + }, + requestTypeGet: { + id: 'user.settings.cmds.request_type_get', + defaultMessage: 'GET' + }, + addDisplayNamePlaceholder: { + id: 'user.settings.cmds.add_display_name.placeholder', + defaultMessage: 'Display Name' + }, + addUsernamePlaceholder: { + id: 'user.settings.cmds.add_username.placeholder', + defaultMessage: 'Username' + }, + addTriggerPlaceholder: { + id: 'user.settings.cmds.add_trigger.placeholder', + defaultMessage: 'Command trigger e.g. "hello" not including the slash' + }, + addAutoCompleteDescPlaceholder: { + id: 'user.settings.cmds.auto_complete_desc.placeholder', + defaultMessage: 'A short description of what this commands does.' + }, + addAutoCompleteHintPlaceholder: { + id: 'user.settings.cmds.auto_complete_hint.placeholder', + defaultMessage: '[zipcode]' + }, + adUrlPlaceholder: { + id: 'user.settings.cmds.url.placeholder', + defaultMessage: 'Must start with http:// or https://' + } +}); + +export default class ManageCommandCmds extends React.Component { + constructor() { + super(); + + this.getCmds = this.getCmds.bind(this); + this.addNewCmd = this.addNewCmd.bind(this); + this.emptyCmd = this.emptyCmd.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.updateDisplayName = this.updateDisplayName.bind(this); + this.updateAutoComplete = this.updateAutoComplete.bind(this); + this.updateAutoCompleteDesc = this.updateAutoCompleteDesc.bind(this); + this.updateAutoCompleteHint = this.updateAutoCompleteHint.bind(this); + + this.state = {cmds: [], cmd: this.emptyCmd(), getCmdsComplete: false}; + } + + static propTypes() { + return { + intl: intlShape.isRequired + }; + } + + emptyCmd() { + var cmd = {}; + cmd.url = ''; + cmd.trigger = ''; + cmd.method = 'P'; + cmd.username = ''; + cmd.icon_url = ''; + cmd.auto_complete = false; + cmd.auto_complete_desc = ''; + cmd.auto_complete_hint = ''; + cmd.display_name = ''; + return cmd; + } + + componentDidMount() { + this.getCmds(); + } + + addNewCmd(e) { + e.preventDefault(); + + if (this.state.cmd.trigger === '' || this.state.cmd.url === '') { + return; + } + + var cmd = this.state.cmd; + if (cmd.trigger.length !== 0) { + cmd.trigger = cmd.trigger.trim(); + } + cmd.url = cmd.url.trim(); + + Client.addCommand( + cmd, + (data) => { + let cmds = Object.assign([], this.state.cmds); + if (!cmds) { + cmds = []; + } + cmds.push(data); + this.setState({cmds, addError: null, cmd: this.emptyCmd()}); + }, + (err) => { + this.setState({addError: err.message}); + } + ); + } + + removeCmd(id) { + const data = {}; + data.id = id; + + Client.deleteCommand( + data, + () => { + const cmds = this.state.cmds; + let index = -1; + for (let i = 0; i < cmds.length; i++) { + if (cmds[i].id === id) { + index = i; + break; + } + } + + if (index !== -1) { + cmds.splice(index, 1); + } + + this.setState({cmds}); + }, + (err) => { + this.setState({editError: err.message}); + } + ); + } + + regenToken(id) { + const regenData = {}; + regenData.id = id; + + Client.regenCommandToken( + regenData, + (data) => { + const cmds = Object.assign([], this.state.cmds); + for (let i = 0; i < cmds.length; i++) { + if (cmds[i].id === id) { + cmds[i] = data; + break; + } + } + + this.setState({cmds, editError: null}); + }, + (err) => { + this.setState({editError: err.message}); + } + ); + } + + getCmds() { + Client.listTeamCommands( + (data) => { + if (data) { + this.setState({cmds: data, getCmdsComplete: true, editError: null}); + } + }, + (err) => { + this.setState({editError: err.message}); + } + ); + } + + updateTrigger(e) { + var cmd = this.state.cmd; + cmd.trigger = e.target.value; + this.setState(cmd); + } + + updateURL(e) { + var cmd = this.state.cmd; + cmd.url = e.target.value; + this.setState(cmd); + } + + updateMethod(e) { + var cmd = this.state.cmd; + cmd.method = e.target.value; + this.setState(cmd); + } + + updateUsername(e) { + var cmd = this.state.cmd; + cmd.username = e.target.value; + this.setState(cmd); + } + + updateIconURL(e) { + var cmd = this.state.cmd; + cmd.icon_url = e.target.value; + this.setState(cmd); + } + + updateDisplayName(e) { + var cmd = this.state.cmd; + cmd.display_name = e.target.value; + this.setState(cmd); + } + + updateAutoComplete(e) { + var cmd = this.state.cmd; + cmd.auto_complete = e.target.checked; + this.setState(cmd); + } + + updateAutoCompleteDesc(e) { + var cmd = this.state.cmd; + cmd.auto_complete_desc = e.target.value; + this.setState(cmd); + } + + updateAutoCompleteHint(e) { + var cmd = this.state.cmd; + cmd.auto_complete_hint = e.target.value; + this.setState(cmd); + } + + render() { + let addError; + if (this.state.addError) { + addError = <label className='has-error'>{this.state.addError}</label>; + } + + let editError; + if (this.state.editError) { + addError = <label className='has-error'>{this.state.editError}</label>; + } + + const cmds = []; + this.state.cmds.forEach((cmd) => { + let triggerDiv; + if (cmd.trigger && cmd.trigger.length !== 0) { + triggerDiv = ( + <div className='padding-top'> + <strong> + <FormattedMessage + id='user.settings.cmds.trigger' + defaultMessage='Trigger: ' + /> + </strong>{cmd.trigger} + </div> + ); + } + + cmds.push( + <div + key={cmd.id} + className='webcmd__item' + > + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.display_name' + defaultMessage='Display Name: ' + /> + </strong><span className='word-break--all'>{cmd.display_name}</span> + </div> + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.username' + defaultMessage='Username: ' + /> + </strong><span className='word-break--all'>{cmd.username}</span> + </div> + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.icon_url' + defaultMessage='Icon URL: ' + /> + </strong><span className='word-break--all'>{cmd.icon_url}</span> + </div> + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.auto_complete' + defaultMessage='Auto Complete: ' + /> + </strong><span className='word-break--all'>{cmd.auto_complete ? 'yes' : 'no'}</span> + </div> + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.auto_complete_desc' + defaultMessage='Auto Complete Description: ' + /> + </strong><span className='word-break--all'>{cmd.auto_complete_desc}</span> + </div> + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.auto_complete_hint' + defaultMessage='Auto Complete Hint: ' + /> + </strong><span className='word-break--all'>{cmd.auto_complete_hint}</span> + </div> + <div className='padding-top x2'> + <strong> + <FormattedMessage + id='user.settings.cmds.request_type' + defaultMessage='Request Type: ' + /> + </strong> + <span className='word-break--all'> + { + cmd.method === 'P' ? + <FormattedMessage + id='user.settings.cmds.request_type_post' + defaultMessage='POST' + /> : + <FormattedMessage + id='user.settings.cmds.request_type_get' + defaultMessage='GET' + /> + } + </span> + </div> + <div className='padding-top x2 webcmd__url'> + <strong> + <FormattedMessage + id='user.settings.cmds.url' + defaultMessage='URL: ' + /> + </strong><span className='word-break--all'>{cmd.url}</span> + </div> + {triggerDiv} + <div className='padding-top'> + <strong> + <FormattedMessage + id='user.settings.cmds.token' + defaultMessage='Token: ' + /> + </strong>{cmd.token} + </div> + <div className='padding-top'> + <a + className='text-danger' + href='#' + onClick={this.regenToken.bind(this, cmd.id)} + > + <FormattedMessage + id='user.settings.cmds.regen' + defaultMessage='Regen Token' + /> + </a> + <a + className='webcmd__remove' + href='#' + onClick={this.removeCmd.bind(this, cmd.id)} + > + <span aria-hidden='true'>{'×'}</span> + </a> + </div> + <div className='padding-top x2 divider-light'></div> + </div> + ); + }); + + let displayCmds; + if (!this.state.getCmdsComplete) { + displayCmds = <LoadingScreen/>; + } else if (cmds.length > 0) { + displayCmds = cmds; + } else { + displayCmds = ( + <div className='padding-top x2'> + <FormattedMessage + id='user.settings.cmds.none' + defaultMessage='None' + /> + </div> + ); + } + + const existingCmds = ( + <div className='webcmds__container'> + <label className='control-label padding-top x2'> + <FormattedMessage + id='user.settings.cmds.existing' + defaultMessage='Existing commands' + /> + </label> + <div className='padding-top divider-light'></div> + <div className='webcmds__list'> + {displayCmds} + </div> + </div> + ); + + const disableButton = this.state.cmd.trigger === '' || this.state.cmd.url === ''; + + return ( + <div key='addCommandCmd'> + <FormattedHTMLMessage + id='user.settings.cmds.add_desc' + defaultMessage='Create commands to send message events to an external integration. Please see <a href="http://mattermost.org/commands">http://mattermost.org/commands</a> to learn more.' + /> + <div><label className='control-label padding-top x2'>{'Add a new command'}</label></div> + <div className='padding-top divider-light'></div> + <div className='padding-top'> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.display_name' + defaultMessage='Display Name: ' + /> + </label> + <div className='padding-top'> + <input + ref='displayName' + className='form-control' + value={this.state.cmd.display_name} + onChange={this.updateDisplayName} + placeholder={this.props.intl.formatMessage(holders.addDisplayNamePlaceholder)} + /> + </div> + <div className='padding-top'>{'Command display name.'}</div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.username' + defaultMessage='Username: ' + /> + </label> + <div className='padding-top'> + <input + ref='username' + className='form-control' + value={this.state.cmd.username} + onChange={this.updateUsername} + placeholder={this.props.intl.formatMessage(holders.addUsernamePlaceholder)} + /> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.username_desc' + defaultMessage='The username to use when overriding the post.' + /> + </div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.icon_url' + defaultMessage='Icon URL: ' + /> + </label> + <div className='padding-top'> + <input + ref='iconURL' + className='form-control' + value={this.state.cmd.icon_url} + onChange={this.updateIconURL} + placeholder='https://www.example.com/myicon.png' + /> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.icon_url_desc' + defaultMessage='URL to an icon' + /> + </div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.trigger' + defaultMessage='Trigger: ' + /> + </label> + <div className='padding-top'> + <input + ref='trigger' + className='form-control' + value={this.state.cmd.trigger} + onChange={this.updateTrigger} + placeholder={this.props.intl.formatMessage(holders.addTriggerPlaceholder)} + /> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.trigger_desc' + defaultMessage='Word to trigger on' + /> + {''}</div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.auto_complete' + defaultMessage='Auto Complete: ' + /> + </label> + <div className='padding-top'> + <label> + <input + type='checkbox' + checked={this.state.cmd.auto_complete} + onChange={this.updateAutoComplete} + /> + <FormattedMessage + id='user.settings.cmds.auto_complete_desc_desc' + defaultMessage='A short description of what this commands does' + /> + </label> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.auto_complete_help' + defaultMessage='Show this command in autocomplete list.' + /> + </div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.auto_complete_desc' + defaultMessage='Auto Complete Description: ' + /> + </label> + <div className='padding-top'> + <input + ref='autoCompleteDesc' + className='form-control' + value={this.state.cmd.auto_complete_desc} + onChange={this.updateAutoCompleteDesc} + placeholder={this.props.intl.formatMessage(holders.addAutoCompleteDescPlaceholder)} + /> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.auto_complete_desc_desc' + defaultMessage='A short description of what this commands does' + /> + </div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.auto_complete_hint' + defaultMessage='Auto Complete Hint: ' + /> + </label> + <div className='padding-top'> + <input + ref='autoCompleteHint' + className='form-control' + value={this.state.cmd.auto_complete_hint} + onChange={this.updateAutoCompleteHint} + placeholder={this.props.intl.formatMessage(holders.addAutoCompleteHintPlaceholder)} + /> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.auto_complete_hint_desc' + defaultMessage='List parameters to be passed to the command.' + /> + </div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.request_type' + defaultMessage='Request Type: ' + /> + </label> + <div className='padding-top'> + <select + ref='method' + className='form-control' + value={this.state.cmd.method} + onChange={this.updateMethod} + > + <option value='P'> + {this.props.intl.formatMessage(holders.requestTypePost)} + </option> + <option value='G'> + {this.props.intl.formatMessage(holders.requestTypeGet)} + </option> + </select> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.request_type_desc' + defaultMessage='Command request type issued to the callback URL.' + /> + </div> + </div> + <div className='padding-top x2'> + <label className='control-label'> + <FormattedMessage + id='user.settings.cmds.url' + defaultMessage='URL: ' + /> + </label> + <div className='padding-top'> + <input + ref='URL' + className='form-control' + value={this.state.cmd.url} + rows={1} + onChange={this.updateURL} + placeholder={this.props.intl.formatMessage(holders.adUrlPlaceholder)} + /> + </div> + <div className='padding-top'> + <FormattedMessage + id='user.settings.cmds.url_desc' + defaultMessage='URL that will receive the HTTP POST or GET event' + /> + </div> + {addError} + </div> + <div className='padding-top padding-bottom'> + <a + className={'btn btn-sm btn-primary'} + href='#' + disabled={disableButton} + onClick={this.addNewCmd} + > + {'Add'} + </a> + </div> + </div> + {existingCmds} + {editError} + </div> + ); + } +} + +export default injectIntl(ManageCommandCmds); diff --git a/web/react/components/user_settings/user_settings_display.jsx b/web/react/components/user_settings/user_settings_display.jsx index 3b2a2065b..776bde442 100644 --- a/web/react/components/user_settings/user_settings_display.jsx +++ b/web/react/components/user_settings/user_settings_display.jsx @@ -297,7 +297,7 @@ class UserSettingsDisplay extends React.Component { if (this.state.nameFormat === 'username') { describe = formatMessage(holders.showUsername); } else if (this.state.nameFormat === 'full_name') { - describe = formatMessage(holders.showFullName); + describe = formatMessage(holders.showFullname); } else { describe = formatMessage(holders.showNickname); } diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index abd04a301..1a9edab03 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -5,6 +5,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; import ManageIncomingHooks from './manage_incoming_hooks.jsx'; import ManageOutgoingHooks from './manage_outgoing_hooks.jsx'; +import ManageCommandHooks from './manage_command_hooks.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -24,6 +25,14 @@ const holders = defineMessages({ outDesc: { id: 'user.settings.integrations.outWebhooksDescription', defaultMessage: 'Manage your outgoing webhooks' + }, + cmdName: { + id: 'user.settings.integrations.commands', + defaultMessage: 'Commands' + }, + cmdDesc: { + id: 'user.settings.integrations.commandsDescription', + defaultMessage: 'Manage your commands' } }); @@ -41,6 +50,7 @@ class UserSettingsIntegrationsTab extends React.Component { render() { let incomingHooksSection; let outgoingHooksSection; + let commandHooksSection; var inputs = []; const {formatMessage} = this.props.intl; @@ -106,6 +116,37 @@ class UserSettingsIntegrationsTab extends React.Component { } } + if (global.window.mm_config.EnableCommands === 'true') { + if (this.props.activeSection === 'command-hooks') { + inputs.push( + <ManageCommandHooks key='command-hook-ui' /> + ); + + commandHooksSection = ( + <SettingItemMax + title={formatMessage(holders.cmdName)} + width='medium' + inputs={inputs} + updateSection={(e) => { + this.updateSection(''); + e.preventDefault(); + }} + /> + ); + } else { + commandHooksSection = ( + <SettingItemMin + title={formatMessage(holders.cmdName)} + width='medium' + describe={formatMessage(holders.cmdDesc)} + updateSection={() => { + this.updateSection('command-hooks'); + }} + /> + ); + } + } + return ( <div> <div className='modal-header'> @@ -144,6 +185,8 @@ class UserSettingsIntegrationsTab extends React.Component { <div className='divider-light'/> {outgoingHooksSection} <div className='divider-dark'/> + {commandHooksSection} + <div className='divider-dark'/> </div> </div> ); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index d615e02c7..328a7a7f2 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -774,20 +774,27 @@ export function savePreferences(preferences, success, error) { } export function getSuggestedCommands(command, suggestionId, component) { - client.executeCommand( - '', - command, - true, + client.listCommands( (data) => { + var matches = []; + data.forEach((cmd) => { + if (('/' + cmd.trigger).indexOf(command) === 0) { + matches.push({ + suggestion: '/' + cmd.trigger + ' ' + cmd.auto_complete_hint, + description: cmd.auto_complete_desc + }); + } + }); + // pull out the suggested commands from the returned data - const terms = data.suggestions.map((suggestion) => suggestion.suggestion); + const terms = matches.map((suggestion) => suggestion.suggestion); AppDispatcher.handleServerAction({ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS, id: suggestionId, matchedPretext: command, terms, - items: data.suggestions, + items: matches, component }); }, diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 33eb4cd47..992337671 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -908,11 +908,11 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { export function executeCommand(channelId, command, suggest, success, error) { $.ajax({ - url: '/api/v1/command', + url: '/api/v1/commands/execute', dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}), + data: JSON.stringify({channelId, command, suggest: '' + suggest}), success, error: function onError(xhr, status, err) { var e = handleError('executeCommand', xhr, status, err); @@ -921,6 +921,78 @@ export function executeCommand(channelId, command, suggest, success, error) { }); } +export function addCommand(cmd, success, error) { + $.ajax({ + url: '/api/v1/commands/create', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(cmd), + success, + error: (xhr, status, err) => { + var e = handleError('addCommand', xhr, status, err); + error(e); + } + }); +} + +export function deleteCommand(data, success, error) { + $.ajax({ + url: '/api/v1/commands/delete', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('deleteCommand', xhr, status, err); + error(e); + } + }); +} + +export function listTeamCommands(success, error) { + $.ajax({ + url: '/api/v1/commands/list_team_commands', + dataType: 'json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('listTeamCommands', xhr, status, err); + error(e); + } + }); +} + +export function regenCommandToken(data, success, error) { + $.ajax({ + url: '/api/v1/commands/regen_token', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(data), + success, + error: (xhr, status, err) => { + var e = handleError('regenCommandToken', xhr, status, err); + error(e); + } + }); +} + +export function listCommands(success, error) { + $.ajax({ + url: '/api/v1/commands/list', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('listCommands', xhr, status, err); + error(e); + } + }); +} + export function getPostsPage(channelId, offset, limit, success, error, complete) { $.ajax({ cache: false, diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index e73680b38..4a4de5c3b 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -173,6 +173,7 @@ .team__name { line-height: 22px; margin-top: -2px; + float: left; } .user__name { @include single-transition(all, 0.1s, linear); diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 73c7bd9cb..2ff49c9b7 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -764,12 +764,13 @@ body.ios { } svg { + height: 17px; width: 17px; } .comment-icon { display: inline-block; - top: 3px; + top: 2px; position: relative; margin-right: 3px; fill: inherit; diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index cb125bff0..693c59a31 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -90,6 +90,7 @@ -webkit-overflow-scrolling: touch; @include flex(1 1 auto); height: calc(100% - 56px); + padding-top: 10px; } .search-results-header { @@ -104,19 +105,22 @@ border-bottom: $border-gray; } -.search-item-container { - border-top: $border-gray; - padding: 10px 1em; - margin: 0; +.search-item__container { - &:first-child { - border: none; - } + .post { + padding: 0 1em 1em; + margin: 0; - .search-channel__name { - font-weight: 600; - margin: 0 0 10px 0; + &:first-child { + border: none; + } + + .search-channel__name { + font-weight: 600; + margin: 0 0 10px 0; + } } + } .search-item__jump { diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index aaffc6ea7..a160aa58f 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -351,10 +351,14 @@ "admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.", "admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ", "admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed.", - "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks: ", - "admin.service.overrideDescription": "When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.", - "admin.service.iconTitle": "Enable Overriding Icon from Webhooks: ", - "admin.service.iconDescription": "When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.", + "admin.service.cmdsTitle": "Enable Slash Commands: ", + "admin.service.cmdsDesc": "When true, user created slash commands will be allowed.", + "admin.service.integrationAdmin": "Enable Integrations for Admin Only: ", + "admin.service.integrationAdminDesc": "When true, user created integrations can only be created by admins.", + "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks and Salsh Commands: ", + "admin.service.overrideDescription": "When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.", + "admin.service.iconTitle": "Enable Overriding Icon from Webhooks and Slash Commands: ", + "admin.service.iconDescription": "When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.", "admin.service.testingTitle": "Enable Testing: ", "admin.service.testingDescription": "(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.", "admin.service.developerTitle": "Enable Developer Mode: ", @@ -1148,7 +1152,40 @@ "user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks", "user.settings.integrations.outWebhooks": "Outgoing Webhooks", "user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks", + "user.settings.integrations.commands": "Commands", + "user.settings.integrations.commandsDescription": "Manage your commands", "user.settings.integrations.title": "Integration Settings", + "user.settings.cmds.trigger": "Trigger: ", + "user.settings.cmds.display_name": "Display Name: ", + "user.settings.cmds.username": "Username: ", + "user.settings.cmds.icon_url": "Icon URL: ", + "user.settings.cmds.auto_complete": "Auto Complete: ", + "user.settings.cmds.auto_complete_desc": "Auto Complete Description: ", + "user.settings.cmds.auto_complete_hint": "Auto Complete Hint: ", + "user.settings.cmds.request_type": "Request Type: ", + "user.settings.cmds.request_type_post": "POST", + "user.settings.cmds.request_type_get": "GET", + "user.settings.cmds.url": "URL: ", + "user.settings.cmds.token": "Token: ", + "user.settings.cmds.regen": "Regen Token", + "user.settings.cmds.none": "None", + "Existing commands": "Existing commands", + "user.settings.cmds.add_desc": "Create commands to send message events to an external integration. Please see <a href=\"http://mattermost.org/commands\">http://mattermost.org/commands</a> to learn more.", + "user.settings.cmds.add_display_name.placeholder": "Display Name", + "user.settings.cmds.existing": "Existing commands", + "user.settings.cmds.add_username.placeholder": "Username", + "user.settings.cmds.username_desc": "The username to use when overriding the post.", + "user.settings.cmds.icon_url_desc": "URL to an icon", + "user.settings.cmds.trigger_desc": "Word to trigger on", + "user.settings.cmds.add_trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash", + "user.settings.cmds.auto_complete_desc_desc": "A short description of what this commands does", + "user.settings.cmds.auto_complete_help": "Show this command in autocomplete list.", + "user.settings.cmds.auto_complete_desc.placeholder": "A short description of what this commands does.", + "user.settings.cmds.auto_complete_hint.placeholder": "[zipcode]", + "user.settings.cmds.auto_complete_hint_desc": "List parameters to be passed to the command.", + "user.settings.cmds.request_type_desc": "Command request type issued to the callback URL.", + "user.settings.cmds.url_desc": "URL that will receive the HTTP POST or GET event", + "user.settings.cmds.url.placeholder": "Must start with http:// or https://", "user.settings.modal.general": "General", "user.settings.modal.security": "Security", "user.settings.modal.notifications": "Notifications", diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index 28d9336e6..0a780c442 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -299,8 +299,6 @@ "admin.service.googleDescription": "Asigna una llave a este campo para habilitar la previsualización de videos de YouTube tomados de los enlaces que aparecen en los mensajes o comentarios. Las instrucciones de como obtener una llave está disponible en <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Al dejar este campo en blanco deshabilita la generación de previsualizaciones de videos de YouTube desde los enlaces.", "admin.service.googleExample": "Ej \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"", "admin.service.googleTitle": "Llave de desarrolador Google:", - "admin.service.iconDescription": "Cuando es verdadero, se le permitirá cambiar el icono del mensaje desde webhooks. Nota, en combinación con permitir el cambio de nombre de usuario, podría exponer a los usuarios a sufrir ataques de phishing.", - "admin.service.iconTitle": "Habilitar el cambio de icono desde los Webhooks: ", "admin.service.listenAddress": "Dirección de escucha:", "admin.service.listenDescription": "La dirección a la que se unirá y escuchará. Ingresar \":8065\" se podrá unir a todas las interfaces o podrá seleccionar una como ej: \"127.0.0.1:8065\". Cambiando este valor es necesario reiniciar el servidor.", "admin.service.listenExample": "Ej \":8065\"", @@ -308,8 +306,6 @@ "admin.service.mobileSessionDaysDesc": "La sesión nativa de los dispositivos moviles expirará luego de transcurrido el numero de días especificado y se solicitará al usuario que inicie sesión nuevamente.", "admin.service.outWebhooksDesc": "Cuando es verdadero, los webhooks de salida serán permitidos.", "admin.service.outWebhooksTitle": "Habilitar Webhooks de Salida: ", - "admin.service.overrideDescription": "Cuando es verdadero, se le permitirá cambiar el nombre de usuario desde webhooks. Nota, en conjunto con cambio de icono, podría exponer a los usuarios a sufrir ataques de phishing.", - "admin.service.overrideTitle": "Habilitar el cambio de nombres de usuario desde los Webhooks: ", "admin.service.save": "Guardar", "admin.service.saving": "Guardando....", "admin.service.securityDesc": "Cuando es verdadero, Los Administradores del Sistema serán notificados por correo electrónico se han anunciado alertas de seguridad relevantes en las últimas 12 horas. Requiere que los correos estén habilitados.", @@ -571,8 +567,8 @@ "claim.email_to_sso.pwd": "Contraseña", "claim.email_to_sso.pwdError": "Por favor introduce tu contraseña.", "claim.email_to_sso.ssoType": "Al reclamar tu cuenta, sólo podrás iniciar sesión con {type} SSO", - "claim.email_to_sso.switchTo": "Cambiar cuenta a ", - "claim.email_to_sso.title": "Cambiar Cuenta de Correo/Contraseña a ", + "claim.email_to_sso.switchTo": "Cambiar cuenta a {uiType}", + "claim.email_to_sso.title": "Cambiar Cuenta de Correo/Contraseña a {uiType}", "claim.sso_to_email.confirm": "Confirmar Contraseña", "claim.sso_to_email.description": "Al cambiar el tipo de cuenta, sólo podrás iniciar sesión con tu correo electrónico y contraseña.", "claim.sso_to_email.enterPwd": "Por favor ingresa una contraseña.", diff --git a/web/web_test.go b/web/web_test.go index cc7d22559..7617ae54a 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -194,6 +194,10 @@ func TestIncomingWebhook(t *testing.T) { user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User) store.Must(api.Srv.Store.User().VerifyEmail(user.Id)) + c := &api.Context{} + c.RequestId = model.NewId() + c.IpAddress = "cmd_line" + api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN) ApiClient.LoginByEmail(team.Name, user.Email, "pwd") channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} |