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 | |
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')
17 files changed, 1163 insertions, 639 deletions
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx index 24eb7eabb..390c07d13 100644 --- a/webapp/client/client.jsx +++ b/webapp/client/client.jsx @@ -1975,6 +1975,18 @@ export default class Client { this.trackEvent('api', 'api_integrations_created', {team_id: this.getTeamId()}); } + updateIncomingHook(hook, success, error) { + request. + post(`${this.getHooksRoute()}/incoming/update`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(hook). + end(this.handleResponse.bind(this, 'updateIncomingHook', success, error)); + + this.trackEvent('api', 'api_integrations_updated', {team_id: this.getTeamId()}); + } + deleteIncomingHook(hookId, success, error) { request. post(`${this.getHooksRoute()}/incoming/delete`). @@ -2008,6 +2020,18 @@ export default class Client { this.trackEvent('api', 'api_integrations_created', {team_id: this.getTeamId()}); } + updateOutgoingHook(hook, success, error) { + request. + post(`${this.getHooksRoute()}/outgoing/update`). + set(this.defaultHeaders). + type('application/json'). + accept('application/json'). + send(hook). + end(this.handleResponse.bind(this, 'updateOutgoingHook', success, error)); + + this.trackEvent('api', 'api_integrations_updated', {team_id: this.getTeamId()}); + } + deleteOutgoingHook(hookId, success, error) { request. post(`${this.getHooksRoute()}/outgoing/delete`). 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} /> ); }); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 46b9c113f..86ab91c3c 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -96,6 +96,7 @@ "add_incoming_webhook.name": "Name", "add_incoming_webhook.save": "Save", "add_incoming_webhook.url": "<b>URL</b>: {url}", + "update_incoming_webhook.update": "Update", "add_oauth_app.callbackUrls.help": "The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.", "add_oauth_app.callbackUrlsRequired": "One or more callback URLs are required", "add_oauth_app.clientId": "<b>Client ID</b>: {id}", @@ -137,6 +138,9 @@ "add_outgoing_webhook.triggerWordsTriggerWhen.help": "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.", "add_outgoing_webhook.triggerWordsTriggerWhenFullWord": "First word matches a trigger word exactly", "add_outgoing_webhook.triggerWordsTriggerWhenStartsWith": "First word starts with a trigger word", + "update_outgoing_webhook.update": "Update", + "update_outgoing_webhook.confirm": "Edit Outgoing Webhook", + "update_outgoing_webhook.question": "Your changes may break the existing outgoing webhook. Are you sure you would like to update it?", "admin.advance.cluster": "High Availability (Beta)", "admin.advance.metrics": "Performance Monitoring (Beta)", "admin.audits.reload": "Reload User Activity Logs", diff --git a/webapp/routes/route_integrations.jsx b/webapp/routes/route_integrations.jsx index 7a4af7e7a..2933ba189 100644 --- a/webapp/routes/route_integrations.jsx +++ b/webapp/routes/route_integrations.jsx @@ -27,6 +27,12 @@ export default { getComponents: (location, callback) => { System.import('components/integrations/components/add_incoming_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); } + }, + { + path: 'edit', + getComponents: (location, callback) => { + System.import('components/integrations/components/edit_incoming_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); + } } ] }, @@ -43,6 +49,12 @@ export default { getComponents: (location, callback) => { System.import('components/integrations/components/add_outgoing_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); } + }, + { + path: 'edit', + getComponents: (location, callback) => { + System.import('components/integrations/components/edit_outgoing_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); + } } ] }, diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx index 33680452b..34da3751a 100644 --- a/webapp/stores/integration_store.jsx +++ b/webapp/stores/integration_store.jsx @@ -57,6 +57,20 @@ class IntegrationStore extends EventEmitter { this.setIncomingWebhooks(teamId, incomingWebhooks); } + updateIncomingWebhook(incomingWebhook) { + const teamId = incomingWebhook.team_id; + const incomingWebhooks = this.getIncomingWebhooks(teamId); + + for (let i = 0; i < incomingWebhooks.length; i++) { + if (incomingWebhooks[i].id === incomingWebhook.id) { + incomingWebhooks[i] = incomingWebhook; + break; + } + } + + this.setIncomingWebhooks(teamId, incomingWebhooks); + } + removeIncomingWebhook(teamId, id) { let incomingWebhooks = this.getIncomingWebhooks(teamId); @@ -200,6 +214,10 @@ class IntegrationStore extends EventEmitter { this.addIncomingWebhook(action.incomingWebhook); this.emitChange(); break; + case ActionTypes.UPDATED_INCOMING_WEBHOOK: + this.updateIncomingWebhook(action.incomingWebhook); + this.emitChange(); + break; case ActionTypes.REMOVED_INCOMING_WEBHOOK: this.removeIncomingWebhook(action.teamId, action.id); this.emitChange(); diff --git a/webapp/tests/client_hooks.test.jsx b/webapp/tests/client_hooks.test.jsx index 8d09802a9..841d87b7a 100644 --- a/webapp/tests/client_hooks.test.jsx +++ b/webapp/tests/client_hooks.test.jsx @@ -22,7 +22,29 @@ describe('Client.Hooks', function() { done(new Error('hooks not enabled')); }, function(err) { - assert.equal(err.id, 'api.webhook.create_incoming.disabled.app_errror'); + assert.equal(err.id, 'api.webhook.create_incoming.disabled.app_error'); + done(); + } + ); + }); + }); + + it('updateIncomingHook', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var hook = {}; + hook.channel_id = TestHelper.basicChannel().id; + hook.description = 'desc'; + hook.display_name = 'Unit Test'; + + TestHelper.basicClient().updateIncomingHook( + hook, + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.update_incoming.disabled.app_error'); done(); } ); @@ -38,7 +60,7 @@ describe('Client.Hooks', function() { done(new Error('hooks not enabled')); }, function(err) { - assert.equal(err.id, 'api.webhook.delete_incoming.disabled.app_errror'); + assert.equal(err.id, 'api.webhook.delete_incoming.disabled.app_error'); done(); } ); @@ -128,5 +150,27 @@ describe('Client.Hooks', function() { ); }); }); + + it('updateOutgoingHook', function(done) { + TestHelper.initBasic(() => { + TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error + + var hook = {}; + hook.channel_id = TestHelper.basicChannel().id; + hook.description = 'desc'; + hook.display_name = 'Unit Test'; + + TestHelper.basicClient().updateOutgoingHook( + hook, + function() { + done(new Error('hooks not enabled')); + }, + function(err) { + assert.equal(err.id, 'api.webhook.update_outgoing.disabled.app_error'); + done(); + } + ); + }); + }); }); diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index d47e45eb9..e1449e3c5 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1324,6 +1324,29 @@ export function addIncomingHook(hook, success, error) { ); } +export function updateIncomingHook(hook, success, error) { + Client.updateIncomingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.UPDATED_INCOMING_WEBHOOK, + incomingWebhook: data + }); + + if (success) { + success(data); + } + }, + (err) => { + if (error) { + error(err); + } else { + dispatchError(err, 'updateIncomingHook'); + } + } + ); +} + export function addOutgoingHook(hook, success, error) { Client.addOutgoingHook( hook, @@ -1347,6 +1370,29 @@ export function addOutgoingHook(hook, success, error) { ); } +export function updateOutgoingHook(hook, success, error) { + Client.updateOutgoingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.UPDATED_OUTGOING_WEBHOOK, + outgoingWebhook: data + }); + + if (success) { + success(data); + } + }, + (err) => { + if (error) { + error(err); + } else { + dispatchError(err, 'updateOutgoingHook'); + } + } + ); +} + export function deleteIncomingHook(id) { Client.deleteIncomingHook( id, diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 130e116a9..68b6f2cc0 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -105,6 +105,7 @@ export const ActionTypes = keyMirror({ RECEIVED_INCOMING_WEBHOOKS: null, RECEIVED_INCOMING_WEBHOOK: null, + UPDATED_INCOMING_WEBHOOK: null, REMOVED_INCOMING_WEBHOOK: null, RECEIVED_OUTGOING_WEBHOOKS: null, RECEIVED_OUTGOING_WEBHOOK: null, |