diff options
author | Poornima <mpoornima@users.noreply.github.com> | 2017-02-27 00:18:20 +0530 |
---|---|---|
committer | Joram Wilander <jwawilander@gmail.com> | 2017-02-26 13:48:20 -0500 |
commit | 19b753467d37209f2227567637e60138d05dd405 (patch) | |
tree | 163ba0878c02267ecbbcb288e11d23e30ec9c8eb /webapp/components/integrations | |
parent | c0bb6f99f89259f6728856ace23d5dd505494b26 (diff) | |
download | chat-19b753467d37209f2227567637e60138d05dd405.tar.gz chat-19b753467d37209f2227567637e60138d05dd405.tar.bz2 chat-19b753467d37209f2227567637e60138d05dd405.zip |
Adding edit of incoming webhook (#5272)
Adding edit of outgoing webhook
Fixing spelling of error
Fixing style
Changing from PUT to POST for updates
Fixing test failures due to merge
Diffstat (limited to 'webapp/components/integrations')
10 files changed, 1012 insertions, 637 deletions
diff --git a/webapp/components/integrations/components/abstract_incoming_webhook.jsx b/webapp/components/integrations/components/abstract_incoming_webhook.jsx new file mode 100644 index 000000000..04322d77e --- /dev/null +++ b/webapp/components/integrations/components/abstract_incoming_webhook.jsx @@ -0,0 +1,242 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import SpinnerButton from 'components/spinner_button.jsx'; +import {Link} from 'react-router/es6'; + +export default class AbstractIncomingWebhook extends React.Component { + static get propTypes() { + return { + team: React.PropTypes.object + }; + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateDisplayName = this.updateDisplayName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + + this.state = { + displayName: '', + description: '', + channelId: '', + saving: false, + serverError: '', + clientError: null + }; + + if (typeof this.performAction === 'undefined') { + throw new TypeError('Subclasses must override performAction'); + } + + if (typeof this.header === 'undefined') { + throw new TypeError('Subclasses must override header'); + } + + if (typeof this.footer === 'undefined') { + throw new TypeError('Subclasses must override footer'); + } + + this.performAction = this.performAction.bind(this); + this.header = this.header.bind(this); + this.footer = this.footer.bind(this); + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + if (!this.state.channelId) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_incoming_webhook.channelRequired' + defaultMessage='A valid channel is required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + display_name: this.state.displayName, + description: this.state.description + }; + + this.performAction(hook); + } + + updateDisplayName(e) { + this.setState({ + displayName: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + render() { + var headerToRender = this.header(); + var footerToRender = this.footer(); + return ( + <div className='backstage-content'> + <BackstageHeader> + <Link to={`/${this.props.team.name}/integrations/incoming_webhooks`}> + <FormattedMessage + id='installed_incoming_webhooks.header' + defaultMessage='Incoming Webhooks' + /> + </Link> + <FormattedMessage + id={headerToRender.id} + defaultMessage={headerToRender.defaultMessage} + /> + </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_incoming_webhook.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_incoming_webhook.displayName.help' + defaultMessage='Display name for your incoming webhook 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_incoming_webhook.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_incoming_webhook.description.help' + defaultMessage='Description for your incoming webhook.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='channelId' + > + <FormattedMessage + id='add_incoming_webhook.channel' + defaultMessage='Channel' + /> + </label> + <div className='col-md-5 col-sm-8'> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + selectOpen={true} + selectPrivate={true} + /> + <div className='form__help'> + <FormattedMessage + id='add_incoming_webhook.channel.help' + defaultMessage='Public channel or private group that receives the webhook payloads. You must belong to the private group when setting up the webhook.' + /> + </div> + </div> + </div> + <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/incoming_webhooks`} + > + <FormattedMessage + id='add_incoming_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id={footerToRender.id} + defaultMessage={footerToRender.defaultMessage} + /> + </SpinnerButton> + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/abstract_outgoing_webhook.jsx b/webapp/components/integrations/components/abstract_outgoing_webhook.jsx new file mode 100644 index 000000000..6033647af --- /dev/null +++ b/webapp/components/integrations/components/abstract_outgoing_webhook.jsx @@ -0,0 +1,460 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {localizeMessage} from 'utils/utils.jsx'; + +import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; +import ChannelSelect from 'components/channel_select.jsx'; +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router/es6'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AbstractOutgoingWebhook extends React.Component { + static get propTypes() { + return { + team: React.PropTypes.object + }; + } + + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateDisplayName = this.updateDisplayName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateContentType = this.updateContentType.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + this.updateTriggerWords = this.updateTriggerWords.bind(this); + this.updateTriggerWhen = this.updateTriggerWhen.bind(this); + this.updateCallbackUrls = this.updateCallbackUrls.bind(this); + + this.state = { + displayName: '', + description: '', + contentType: 'application/x-www-form-urlencoded', + channelId: '', + triggerWords: '', + triggerWhen: 0, + callbackUrls: '', + saving: false, + serverError: '', + clientError: null + }; + + if (typeof this.performAction === 'undefined') { + throw new TypeError('Subclasses must override performAction'); + } + + if (typeof this.header === 'undefined') { + throw new TypeError('Subclasses must override header'); + } + + if (typeof this.footer === 'undefined') { + throw new TypeError('Subclasses must override footer'); + } + + if (typeof this.renderExtra === 'undefined') { + throw new TypeError('Subclasses must override renderExtra'); + } + + this.performAction = this.performAction.bind(this); + this.header = this.header.bind(this); + this.footer = this.footer.bind(this); + this.renderExtra = this.renderExtra.bind(this); + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + const triggerWords = []; + if (this.state.triggerWords) { + for (let triggerWord of this.state.triggerWords.split('\n')) { + triggerWord = triggerWord.trim(); + + if (triggerWord.length > 0) { + triggerWords.push(triggerWord); + } + } + } + + if (!this.state.channelId && triggerWords.length === 0) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.triggerWordsOrChannelRequired' + defaultMessage='A valid channel or a list of trigger words is required' + /> + ) + }); + + return; + } + + const callbackUrls = []; + for (let callbackUrl of this.state.callbackUrls.split('\n')) { + callbackUrl = callbackUrl.trim(); + + if (callbackUrl.length > 0) { + callbackUrls.push(callbackUrl); + } + } + + if (callbackUrls.length === 0) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_outgoing_webhook.callbackUrlsRequired' + defaultMessage='One or more callback URLs are required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + trigger_words: triggerWords, + trigger_when: parseInt(this.state.triggerWhen, 10), + callback_urls: callbackUrls, + display_name: this.state.displayName, + content_type: this.state.contentType, + description: this.state.description + }; + + this.performAction(hook); + } + + updateDisplayName(e) { + this.setState({ + displayName: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateContentType(e) { + this.setState({ + contentType: e.target.value + }); + } + + updateChannelId(e) { + this.setState({ + channelId: e.target.value + }); + } + + updateTriggerWords(e) { + this.setState({ + triggerWords: e.target.value + }); + } + + updateTriggerWhen(e) { + this.setState({ + triggerWhen: e.target.value + }); + } + + updateCallbackUrls(e) { + this.setState({ + callbackUrls: e.target.value + }); + } + + render() { + const contentTypeOption1 = 'application/x-www-form-urlencoded'; + const contentTypeOption2 = 'application/json'; + + var headerToRender = this.header(); + var footerToRender = this.footer(); + var renderExtra = this.renderExtra(); + + return ( + <div className='backstage-content'> + <BackstageHeader> + <Link to={`/${this.props.team.name}/integrations/outgoing_webhooks`}> + <FormattedMessage + id='installed_outgoing_webhooks.header' + defaultMessage='Outgoing Webhooks' + /> + </Link> + <FormattedMessage + id={headerToRender.id} + defaultMessage={headerToRender.defaultMessage} + /> + </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_outgoing_webhook.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_outgoing_webhook.displayName.help' + defaultMessage='Display name for your incoming webhook 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_outgoing_webhook.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_outgoing_webhook.description.help' + defaultMessage='Description for your incoming webhook.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='contentType' + > + <FormattedMessage + id='add_outgoing_webhook.content_Type' + defaultMessage='Content Type' + /> + </label> + <div className='col-md-5 col-sm-8'> + <select + className='form-control' + value={this.state.contentType} + onChange={this.updateContentType} + > + <option + value={contentTypeOption1} + > + {contentTypeOption1} + </option> + <option + value={contentTypeOption2} + > + {contentTypeOption2} + </option> + </select> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.contentType.help1' + defaultMessage='Choose the content type by which the response will be sent.' + /> + </div> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.contentType.help2' + defaultMessage='If application/x-www-form-urlencoded is chosen, the server assumes you will be encoding the parameters in a URL format.' + /> + </div> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.contentType.help3' + defaultMessage='If application/json is chosen, the server assumes you will posting JSON data.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='channelId' + > + <FormattedMessage + id='add_outgoing_webhook.channel' + defaultMessage='Channel' + /> + </label> + <div className='col-md-5 col-sm-8'> + <ChannelSelect + id='channelId' + value={this.state.channelId} + onChange={this.updateChannelId} + selectOpen={true} + /> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.channel.help' + defaultMessage='Public channel to receive webhook payloads. Optional if at least one Trigger Word is specified.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='triggerWords' + > + <FormattedMessage + id='add_outgoing_webhook.triggerWords' + defaultMessage='Trigger Words (One Per Line)' + /> + </label> + <div className='col-md-5 col-sm-8'> + <textarea + id='triggerWords' + rows='3' + maxLength='1000' + className='form-control' + value={this.state.triggerWords} + onChange={this.updateTriggerWords} + /> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.triggerWords.help' + defaultMessage='Messages that start with one of the specified words will trigger the outgoing webhook. Optional if Channel is selected.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='triggerWords' + > + <FormattedMessage + id='add_outgoing_webhook.triggerWordsTriggerWhen' + defaultMessage='Trigger When' + /> + </label> + <div className='col-md-5 col-sm-8'> + <select + className='form-control' + value={this.state.triggerWhen} + onChange={this.updateTriggerWhen} + > + <option + value='0' + > + {localizeMessage('add_outgoing_webhook.triggerWordsTriggerWhenFullWord', 'First word matches a trigger word exactly')} + </option> + <option + value='1' + > + {localizeMessage('add_outgoing_webhook.triggerWordsTriggerWhenStartsWith', 'First word starts with a trigger word')} + </option> + </select> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.triggerWordsTriggerWhen.help' + defaultMessage='Choose when to trigger the outgoing webhook; if the first word of a message matches a Trigger Word exactly, or if it starts with a Trigger Word.' + /> + </div> + </div> + </div> + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='callbackUrls' + > + <FormattedMessage + id='add_outgoing_webhook.callbackUrls' + defaultMessage='Callback URLs (One Per Line)' + /> + </label> + <div className='col-md-5 col-sm-8'> + <textarea + id='callbackUrls' + rows='3' + maxLength='1000' + className='form-control' + value={this.state.callbackUrls} + onChange={this.updateCallbackUrls} + /> + <div className='form__help'> + <FormattedMessage + id='add_outgoing_webhook.callbackUrls.help' + defaultMessage='The URL that messages will be sent to.' + /> + </div> + </div> + </div> + <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/outgoing_webhooks`} + > + <FormattedMessage + id='add_outgoing_webhook.cancel' + defaultMessage='Cancel' + /> + </Link> + <SpinnerButton + className='btn btn-primary' + type='submit' + spinning={this.state.saving} + onClick={this.handleSubmit} + > + <FormattedMessage + id={footerToRender.id} + defaultMessage={footerToRender.defaultMessage} + /> + </SpinnerButton> + {renderExtra} + </div> + </form> + </div> + </div> + ); + } +} diff --git a/webapp/components/integrations/components/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook.jsx index 0372fbbcb..d7b7fb51b 100644 --- a/webapp/components/integrations/components/add_incoming_webhook.jsx +++ b/webapp/components/integrations/components/add_incoming_webhook.jsx @@ -1,80 +1,17 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React from 'react'; - import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router/es6'; -import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; -import ChannelSelect from 'components/channel_select.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'; - -export default class AddIncomingWebhook extends React.Component { - static get propTypes() { - return { - team: React.PropTypes.object - }; - } - - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - - this.updateDisplayName = this.updateDisplayName.bind(this); - this.updateDescription = this.updateDescription.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - - this.state = { - displayName: '', - description: '', - channelId: '', - saving: false, - serverError: '', - clientError: null - }; - } - - handleSubmit(e) { - e.preventDefault(); - - if (this.state.saving) { - return; - } - - this.setState({ - saving: true, - serverError: '', - clientError: '' - }); - - if (!this.state.channelId) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_incoming_webhook.channelRequired' - defaultMessage='A valid channel is required' - /> - ) - }); - - return; - } - - const hook = { - channel_id: this.state.channelId, - display_name: this.state.displayName, - description: this.state.description - }; +import AbstractIncomingWebhook from './abstract_incoming_webhook.jsx'; +export default class AddIncomingWebhook extends AbstractIncomingWebhook { + performAction(hook) { AsyncClient.addIncomingHook( hook, (data) => { - browserHistory.push('/' + this.props.team.name + '/integrations/confirm?type=incoming_webhooks&id=' + data.id); + browserHistory.push(`/${this.props.team.name}/integrations/confirm?type=incoming_webhooks&id=${data.id}`); }, (err) => { this.setState({ @@ -85,153 +22,11 @@ export default class AddIncomingWebhook extends React.Component { ); } - updateDisplayName(e) { - this.setState({ - displayName: e.target.value - }); + header() { + return {id: 'integrations.add', defaultMessage: 'Add'}; } - updateDescription(e) { - this.setState({ - description: e.target.value - }); - } - - updateChannelId(e) { - this.setState({ - channelId: e.target.value - }); - } - - render() { - return ( - <div className='backstage-content'> - <BackstageHeader> - <Link to={'/' + this.props.team.name + '/integrations/incoming_webhooks'}> - <FormattedMessage - id='installed_incoming_webhooks.header' - defaultMessage='Incoming Webhooks' - /> - </Link> - <FormattedMessage - id='integrations.add' - defaultMessage='Add' - /> - </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_incoming_webhook.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_incoming_webhook.displayName.help' - defaultMessage='Display name for your incoming webhook 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_incoming_webhook.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_incoming_webhook.description.help' - defaultMessage='Description for your incoming webhook.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='channelId' - > - <FormattedMessage - id='add_incoming_webhook.channel' - defaultMessage='Channel' - /> - </label> - <div className='col-md-5 col-sm-8'> - <ChannelSelect - id='channelId' - value={this.state.channelId} - onChange={this.updateChannelId} - selectOpen={true} - selectPrivate={true} - /> - <div className='form__help'> - <FormattedMessage - id='add_incoming_webhook.channel.help' - defaultMessage='Public channel or private group that receives the webhook payloads. You must belong to the private group when setting up the webhook.' - /> - </div> - </div> - </div> - <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/incoming_webhooks'} - > - <FormattedMessage - id='add_incoming_webhook.cancel' - defaultMessage='Cancel' - /> - </Link> - <SpinnerButton - className='btn btn-primary' - type='submit' - spinning={this.state.saving} - onClick={this.handleSubmit} - > - <FormattedMessage - id='add_incoming_webhook.save' - defaultMessage='Save' - /> - </SpinnerButton> - </div> - </form> - </div> - </div> - ); + footer() { + return {id: 'add_incoming_webhook.save', defaultMessage: 'Save'}; } } diff --git a/webapp/components/integrations/components/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx index 9e9aaaeb2..24475e176 100644 --- a/webapp/components/integrations/components/add_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/add_outgoing_webhook.jsx @@ -1,127 +1,17 @@ // 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 {localizeMessage} from 'utils/utils.jsx'; - -import BackstageHeader from 'components/backstage/components/backstage_header.jsx'; -import ChannelSelect from 'components/channel_select.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'; - -export default class AddOutgoingWebhook extends React.Component { - static get propTypes() { - return { - team: React.PropTypes.object - }; - } - - constructor(props) { - super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - - this.updateDisplayName = this.updateDisplayName.bind(this); - this.updateDescription = this.updateDescription.bind(this); - this.updateContentType = this.updateContentType.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - this.updateTriggerWords = this.updateTriggerWords.bind(this); - this.updateTriggerWhen = this.updateTriggerWhen.bind(this); - this.updateCallbackUrls = this.updateCallbackUrls.bind(this); - - this.state = { - displayName: '', - description: '', - contentType: 'application/x-www-form-urlencoded', - channelId: '', - triggerWords: '', - triggerWhen: 0, - callbackUrls: '', - saving: false, - serverError: '', - clientError: null - }; - } - - handleSubmit(e) { - e.preventDefault(); - - if (this.state.saving) { - return; - } - - this.setState({ - saving: true, - serverError: '', - clientError: '' - }); - - const triggerWords = []; - if (this.state.triggerWords) { - for (let triggerWord of this.state.triggerWords.split('\n')) { - triggerWord = triggerWord.trim(); - - if (triggerWord.length > 0) { - triggerWords.push(triggerWord); - } - } - } - - if (!this.state.channelId && triggerWords.length === 0) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_outgoing_webhook.triggerWordsOrChannelRequired' - defaultMessage='A valid channel or a list of trigger words is required' - /> - ) - }); - - return; - } +import {browserHistory} from 'react-router/es6'; - const callbackUrls = []; - for (let callbackUrl of this.state.callbackUrls.split('\n')) { - callbackUrl = callbackUrl.trim(); - - if (callbackUrl.length > 0) { - callbackUrls.push(callbackUrl); - } - } - - if (callbackUrls.length === 0) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_outgoing_webhook.callbackUrlsRequired' - defaultMessage='One or more callback URLs are required' - /> - ) - }); - - return; - } - - const hook = { - channel_id: this.state.channelId, - trigger_words: triggerWords, - trigger_when: parseInt(this.state.triggerWhen, 10), - callback_urls: callbackUrls, - display_name: this.state.displayName, - content_type: this.state.contentType, - description: this.state.description - }; +import AbstractOutgoingWebhook from './abstract_outgoing_webhook.jsx'; +export default class AddOutgoingWebhook extends AbstractOutgoingWebhook { + performAction(hook) { AsyncClient.addOutgoingHook( hook, (data) => { - browserHistory.push('/' + this.props.team.name + '/integrations/confirm?type=outgoing_webhooks&id=' + data.id); + browserHistory.push(`/${this.props.team.name}/integrations/confirm?type=outgoing_webhooks&id=${data.id}`); }, (err) => { this.setState({ @@ -132,315 +22,15 @@ export default class AddOutgoingWebhook extends React.Component { ); } - updateDisplayName(e) { - this.setState({ - displayName: e.target.value - }); - } - - updateDescription(e) { - this.setState({ - description: e.target.value - }); - } - - updateContentType(e) { - this.setState({ - contentType: e.target.value - }); + header() { + return {id: 'integrations.add', defaultMessage: 'Add'}; } - updateChannelId(e) { - this.setState({ - channelId: e.target.value - }); + footer() { + return {id: 'add_outgoing_webhook.save', defaultMessage: 'Save'}; } - updateTriggerWords(e) { - this.setState({ - triggerWords: e.target.value - }); - } - - updateTriggerWhen(e) { - this.setState({ - triggerWhen: e.target.value - }); - } - - updateCallbackUrls(e) { - this.setState({ - callbackUrls: e.target.value - }); - } - - render() { - const contentTypeOption1 = 'application/x-www-form-urlencoded'; - const contentTypeOption2 = 'application/json'; - - return ( - <div className='backstage-content'> - <BackstageHeader> - <Link to={'/' + this.props.team.name + '/integrations/outgoing_webhooks'}> - <FormattedMessage - id='installed_outgoing_webhooks.header' - defaultMessage='Outgoing Webhooks' - /> - </Link> - <FormattedMessage - id='integrations.add' - defaultMessage='Add' - /> - </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_outgoing_webhook.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_outgoing_webhook.displayName.help' - defaultMessage='Display name for your incoming webhook 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_outgoing_webhook.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_outgoing_webhook.description.help' - defaultMessage='Description for your incoming webhook.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='contentType' - > - <FormattedMessage - id='add_outgoing_webhook.content_Type' - defaultMessage='Content Type' - /> - </label> - <div className='col-md-5 col-sm-8'> - <select - className='form-control' - value={this.state.contentType} - onChange={this.updateContentType} - > - <option - value={contentTypeOption1} - > - {contentTypeOption1} - </option> - <option - value={contentTypeOption2} - > - {contentTypeOption2} - </option> - </select> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.contentType.help1' - defaultMessage='Choose the content type by which the response will be sent.' - /> - </div> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.contentType.help2' - defaultMessage='If application/x-www-form-urlencoded is chosen, the server assumes you will be encoding the parameters in a URL format.' - /> - </div> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.contentType.help3' - defaultMessage='If application/json is chosen, the server assumes you will posting JSON data.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='channelId' - > - <FormattedMessage - id='add_outgoing_webhook.channel' - defaultMessage='Channel' - /> - </label> - <div className='col-md-5 col-sm-8'> - <ChannelSelect - id='channelId' - value={this.state.channelId} - onChange={this.updateChannelId} - selectOpen={true} - /> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.channel.help' - defaultMessage='Public channel to receive webhook payloads. Optional if at least one Trigger Word is specified.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='triggerWords' - > - <FormattedMessage - id='add_outgoing_webhook.triggerWords' - defaultMessage='Trigger Words (One Per Line)' - /> - </label> - <div className='col-md-5 col-sm-8'> - <textarea - id='triggerWords' - rows='3' - maxLength='1000' - className='form-control' - value={this.state.triggerWords} - onChange={this.updateTriggerWords} - /> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.triggerWords.help' - defaultMessage='Messages that start with one of the specified words will trigger the outgoing webhook. Optional if Channel is selected.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='triggerWords' - > - <FormattedMessage - id='add_outgoing_webhook.triggerWordsTriggerWhen' - defaultMessage='Trigger When' - /> - </label> - <div className='col-md-5 col-sm-8'> - <select - className='form-control' - value={this.state.triggerWhen} - onChange={this.updateTriggerWhen} - > - <option - value='0' - > - {localizeMessage('add_outgoing_webhook.triggerWordsTriggerWhenFullWord', 'First word matches a trigger word exactly')} - </option> - <option - value='1' - > - {localizeMessage('add_outgoing_webhook.triggerWordsTriggerWhenStartsWith', 'First word starts with a trigger word')} - </option> - </select> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.triggerWordsTriggerWhen.help' - defaultMessage='Choose when to trigger the outgoing webhook; if the first word of a message matches a Trigger Word exactly, or if it starts with a Trigger Word.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='callbackUrls' - > - <FormattedMessage - id='add_outgoing_webhook.callbackUrls' - defaultMessage='Callback URLs (One Per Line)' - /> - </label> - <div className='col-md-5 col-sm-8'> - <textarea - id='callbackUrls' - rows='3' - maxLength='1000' - className='form-control' - value={this.state.callbackUrls} - onChange={this.updateCallbackUrls} - /> - <div className='form__help'> - <FormattedMessage - id='add_outgoing_webhook.callbackUrls.help' - defaultMessage='The URL that messages will be sent to.' - /> - </div> - </div> - </div> - <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/outgoing_webhooks'} - > - <FormattedMessage - id='add_outgoing_webhook.cancel' - defaultMessage='Cancel' - /> - </Link> - <SpinnerButton - className='btn btn-primary' - type='submit' - spinning={this.state.saving} - onClick={this.handleSubmit} - > - <FormattedMessage - id='add_outgoing_webhook.save' - defaultMessage='Save' - /> - </SpinnerButton> - </div> - </form> - </div> - </div> - ); + renderExtra() { + return ''; } } diff --git a/webapp/components/integrations/components/edit_incoming_webhook.jsx b/webapp/components/integrations/components/edit_incoming_webhook.jsx new file mode 100644 index 000000000..9e032409a --- /dev/null +++ b/webapp/components/integrations/components/edit_incoming_webhook.jsx @@ -0,0 +1,78 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as AsyncClient from 'utils/async_client.jsx'; + +import {browserHistory} from 'react-router/es6'; +import IntegrationStore from 'stores/integration_store.jsx'; +import {loadIncomingHooks} from 'actions/integration_actions.jsx'; + +import AbstractIncomingWebhook from './abstract_incoming_webhook.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +export default class EditIncomingWebhook extends AbstractIncomingWebhook { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + this.originalIncomingHook = null; + } + + componentDidMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + loadIncomingHooks(); + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + const teamId = TeamStore.getCurrentId(); + + this.setState({ + hooks: IntegrationStore.getIncomingWebhooks(teamId), + loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) + }); + + if (!this.state.loading) { + this.originalIncomingHook = this.state.hooks.filter((hook) => hook.id === this.props.location.query.id)[0]; + + this.setState({ + displayName: this.originalIncomingHook.display_name, + description: this.originalIncomingHook.description, + channelId: this.originalIncomingHook.channel_id + }); + } + } + + performAction(hook) { + if (this.originalIncomingHook.id) { + hook.id = this.originalIncomingHook.id; + } + + AsyncClient.updateIncomingHook( + hook, + () => { + browserHistory.push(`/${this.props.team.name}/integrations/incoming_webhooks`); + }, + (err) => { + this.setState({ + saving: false, + serverError: err.message + }); + } + ); + } + + header() { + return {id: 'integrations.edit', defaultMessage: 'Edit'}; + } + + footer() { + return {id: 'update_incoming_webhook.update', defaultMessage: 'Update'}; + } +} diff --git a/webapp/components/integrations/components/edit_outgoing_webhook.jsx b/webapp/components/integrations/components/edit_outgoing_webhook.jsx new file mode 100644 index 000000000..2268af923 --- /dev/null +++ b/webapp/components/integrations/components/edit_outgoing_webhook.jsx @@ -0,0 +1,190 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; + +import {browserHistory} from 'react-router/es6'; +import IntegrationStore from 'stores/integration_store.jsx'; +import {loadOutgoingHooks} from 'actions/integration_actions.jsx'; + +import AbstractOutgoingWebhook from './abstract_outgoing_webhook.jsx'; +import ConfirmModal from 'components/confirm_modal.jsx'; +import {FormattedMessage} from 'react-intl'; +import TeamStore from 'stores/team_store.jsx'; + +export default class EditOutgoingWebhook extends AbstractOutgoingWebhook { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + this.handleConfirmModal = this.handleConfirmModal.bind(this); + this.handleUpdate = this.handleUpdate.bind(this); + this.submitCommand = this.submitCommand.bind(this); + this.confirmModalDismissed = this.confirmModalDismissed.bind(this); + this.originalOutgoingHook = null; + + this.state = { + showConfirmModal: false + }; + } + + componentDidMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + loadOutgoingHooks(); + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + const teamId = TeamStore.getCurrentId(); + + this.setState({ + hooks: IntegrationStore.getOutgoingWebhooks(teamId), + loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId) + }); + + if (!this.state.loading) { + this.originalOutgoingHook = this.state.hooks.filter((hook) => hook.id === this.props.location.query.id)[0]; + + this.setState({ + displayName: this.originalOutgoingHook.display_name, + description: this.originalOutgoingHook.description, + channelId: this.originalOutgoingHook.channel_id, + contentType: this.originalOutgoingHook.content_type, + triggerWhen: this.originalOutgoingHook.trigger_when + }); + + var triggerWords = ''; + if (this.originalOutgoingHook.trigger_words) { + let i = 0; + for (i = 0; i < this.originalOutgoingHook.trigger_words.length; i++) { + triggerWords += this.originalOutgoingHook.trigger_words[i] + '\n'; + } + } + + var callbackUrls = ''; + if (this.originalOutgoingHook.callback_urls) { + let i = 0; + for (i = 0; i < this.originalOutgoingHook.callback_urls.length; i++) { + callbackUrls += this.originalOutgoingHook.callback_urls[i] + '\n'; + } + } + + this.setState({ + triggerWords, + callbackUrls + }); + } + } + + performAction(hook) { + this.newHook = hook; + + if (this.originalOutgoingHook.id) { + hook.id = this.originalOutgoingHook.id; + } + + if (this.originalOutgoingHook.token) { + hook.token = this.originalOutgoingHook.token; + } + + var triggerWordsSame = (this.originalOutgoingHook.trigger_words.length === hook.trigger_words.length) && + this.originalOutgoingHook.trigger_words.every((v, i) => v === hook.trigger_words[i]); + + var callbackUrlsSame = (this.originalOutgoingHook.callback_urls.length === hook.callback_urls.length) && + this.originalOutgoingHook.callback_urls.every((v, i) => v === hook.callback_urls[i]); + + if (this.originalOutgoingHook.content_type !== hook.content_type || + !triggerWordsSame || !callbackUrlsSame) { + this.handleConfirmModal(); + this.setState({ + saving: false + }); + } else { + this.submitCommand(); + } + } + + handleUpdate() { + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + this.submitCommand(); + } + + handleConfirmModal() { + this.setState({showConfirmModal: true}); + } + + confirmModalDismissed() { + this.setState({showConfirmModal: false}); + } + + submitCommand() { + AsyncClient.updateOutgoingHook( + this.newHook, + () => { + browserHistory.push(`/${this.props.team.name}/integrations/outgoing_webhooks`); + }, + (err) => { + this.setState({ + saving: false, + showConfirmModal: false, + serverError: err.message + }); + } + ); + } + + header() { + return {id: 'integrations.edit', defaultMessage: 'Edit'}; + } + + footer() { + return {id: 'update_outgoing_webhook.update', defaultMessage: 'Update'}; + } + + renderExtra() { + const confirmButton = ( + <FormattedMessage + id='update_outgoing_webhook.update' + defaultMessage='Update' + /> + ); + + const confirmTitle = ( + <FormattedMessage + id='update_outgoing_webhook.confirm' + defaultMessage='Edit Outgoing Webhook' + /> + ); + + const confirmMessage = ( + <FormattedMessage + id='update_outgoing_webhook.question' + defaultMessage='Your changes may break the existing outgoing webhook. Are you sure you would like to update it?' + /> + ); + + return ( + <ConfirmModal + title={confirmTitle} + message={confirmMessage} + confirmButton={confirmButton} + show={this.state.showConfirmModal} + onConfirm={this.handleUpdate} + onCancel={this.confirmModalDismissed} + /> + ); + } +} diff --git a/webapp/components/integrations/components/installed_incoming_webhook.jsx b/webapp/components/integrations/components/installed_incoming_webhook.jsx index 9b4df2393..52d0d7d67 100644 --- a/webapp/components/integrations/components/installed_incoming_webhook.jsx +++ b/webapp/components/integrations/components/installed_incoming_webhook.jsx @@ -7,6 +7,7 @@ import ChannelStore from 'stores/channel_store.jsx'; import {getSiteURL} from 'utils/url.jsx'; import {FormattedMessage} from 'react-intl'; +import {Link} from 'react-router'; export default class InstalledIncomingWebhook extends React.Component { static get propTypes() { @@ -15,7 +16,8 @@ export default class InstalledIncomingWebhook extends React.Component { onDelete: React.PropTypes.func.isRequired, filter: React.PropTypes.string, creator: React.PropTypes.object.isRequired, - canChange: React.PropTypes.bool.isRequired + canChange: React.PropTypes.bool.isRequired, + team: React.PropTypes.object.isRequired }; } @@ -88,6 +90,13 @@ export default class InstalledIncomingWebhook extends React.Component { if (this.props.canChange) { actions = ( <div className='item-actions'> + <Link to={`/${this.props.team.name}/integrations/incoming_webhooks/edit?id=${incomingWebhook.id}`}> + <FormattedMessage + id='installed_integrations.edit' + defaultMessage='Edit' + /> + </Link> + {' - '} <a href='#' onClick={this.handleDelete} diff --git a/webapp/components/integrations/components/installed_incoming_webhooks.jsx b/webapp/components/integrations/components/installed_incoming_webhooks.jsx index df49aa88e..002dbef7f 100644 --- a/webapp/components/integrations/components/installed_incoming_webhooks.jsx +++ b/webapp/components/integrations/components/installed_incoming_webhooks.jsx @@ -110,6 +110,7 @@ export default class InstalledIncomingWebhooks extends React.Component { onDelete={this.deleteIncomingWebhook} creator={this.state.users[incomingWebhook.user_id] || {}} canChange={canChange} + team={this.props.team} /> ); }); diff --git a/webapp/components/integrations/components/installed_outgoing_webhook.jsx b/webapp/components/integrations/components/installed_outgoing_webhook.jsx index 04cc1b033..ebf4f75e1 100644 --- a/webapp/components/integrations/components/installed_outgoing_webhook.jsx +++ b/webapp/components/integrations/components/installed_outgoing_webhook.jsx @@ -6,6 +6,7 @@ import React from 'react'; import ChannelStore from 'stores/channel_store.jsx'; import {FormattedMessage} from 'react-intl'; +import {Link} from 'react-router'; export default class InstalledOutgoingWebhook extends React.Component { static get propTypes() { @@ -15,7 +16,8 @@ export default class InstalledOutgoingWebhook extends React.Component { onDelete: React.PropTypes.func.isRequired, filter: React.PropTypes.string, creator: React.PropTypes.object.isRequired, - canChange: React.PropTypes.bool.isRequired + canChange: React.PropTypes.bool.isRequired, + team: React.PropTypes.object.isRequired }; } @@ -161,6 +163,13 @@ export default class InstalledOutgoingWebhook extends React.Component { /> </a> {' - '} + <Link to={`/${this.props.team.name}/integrations/outgoing_webhooks/edit?id=${outgoingWebhook.id}`}> + <FormattedMessage + id='installed_integrations.edit' + defaultMessage='Edit' + /> + </Link> + {' - '} <a href='#' onClick={this.handleDelete} diff --git a/webapp/components/integrations/components/installed_outgoing_webhooks.jsx b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx index 01da58556..7abacb241 100644 --- a/webapp/components/integrations/components/installed_outgoing_webhooks.jsx +++ b/webapp/components/integrations/components/installed_outgoing_webhooks.jsx @@ -114,6 +114,7 @@ export default class InstalledOutgoingWebhooks extends React.Component { onDelete={this.deleteOutgoingWebhook} creator={this.state.users[outgoingWebhook.creator_id] || {}} canChange={canChange} + team={this.props.team} /> ); }); |