diff options
-rw-r--r-- | webapp/components/backstage/add_incoming_webhook.jsx | 214 | ||||
-rw-r--r-- | webapp/components/channel_invite_button.jsx | 1 | ||||
-rw-r--r-- | webapp/components/form_error.jsx | 50 | ||||
-rw-r--r-- | webapp/components/more_direct_channels.jsx | 1 | ||||
-rw-r--r-- | webapp/components/spinner_button.jsx | 22 | ||||
-rw-r--r-- | webapp/root.jsx | 3 | ||||
-rw-r--r-- | webapp/stores/integration_store.jsx | 21 | ||||
-rw-r--r-- | webapp/utils/async_client.jsx | 46 |
8 files changed, 338 insertions, 20 deletions
diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx new file mode 100644 index 000000000..962792ed0 --- /dev/null +++ b/webapp/components/backstage/add_incoming_webhook.jsx @@ -0,0 +1,214 @@ +// 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'; +import ChannelStore from 'stores/channel_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddIncomingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelName = this.updateChannelName.bind(this); + + this.state = { + team: TeamStore.getCurrent(), + name: '', + description: '', + channelName: '', + saving: false, + serverError: '', + clientError: null + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.handleChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.handleChange); + } + + handleChange() { + this.setState({ + team: TeamStore.getCurrent() + }); + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + const channel = ChannelStore.getByName(this.state.channelName); + + if (!channel) { + this.setState({ + saving: false, + clientError: ( + <FormattedMessage + id='add_incoming_webhook.channel_name_required' + defaultMessage='A valid channel name (eg. town-square) is required' + /> + ) + }); + + return; + } + + const hook = { + channel_id: channel.id + }; + + AsyncClient.addIncomingHook( + hook, + () => { + browserHistory.push(`/${this.state.team.name}/integrations/installed`); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelName(e) { + this.setState({ + channelName: e.target.value + }); + } + + render() { + const team = TeamStore.getCurrent(); + + if (!team) { + return null; + } + + return ( + <div className='backstage row'> + <div className='add-incoming-webhook'> + <div className='backstage__header'> + <h1 className='text'> + <FormattedMessage + id='add-incoming-webhook.header' + defaultMessage='Add Incoming Webhook' + /> + </h1> + </div> + </div> + <form className='add-incoming-webhook__body'> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='name' + > + <FormattedMessage + id='add-incoming-webhook.name' + defaultMessage='Name' + /> + </label> + <input + id='name' + type='text' + value={this.state.name} + onChange={this.updateName} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='description' + > + <FormattedMessage + id='add-incoming-webhook.description' + defaultMessage='Description' + /> + </label> + <input + id='description' + type='text' + value={this.state.description} + onChange={this.updateDescription} + /> + </div> + <div className='add-integration__row'> + <label + className='add-integration__label' + htmlFor='channelName' + > + <FormattedMessage + id='add-incoming-webhook.channelName' + defaultMessage='Channel Name' + /> + </label> + <input + id='channelName' + type='text' + value={this.state.channelName} + onChange={this.updateChannelName} + /> + </div> + <div className='add-integration__submit-row'> + <Link + className='btn btn-sm' + to={`/${team.name}/integrations/add`} + > + <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> + <FormError errors={[this.state.serverError, this.state.clientError]}/> + </form> + </div> + ); + } +} diff --git a/webapp/components/channel_invite_button.jsx b/webapp/components/channel_invite_button.jsx index e4af9f9ce..1fcd461ea 100644 --- a/webapp/components/channel_invite_button.jsx +++ b/webapp/components/channel_invite_button.jsx @@ -65,6 +65,7 @@ export default class ChannelInviteButton extends React.Component { render() { return ( <SpinnerButton + className='btn btn-sm btn-primary' onClick={this.handleClick} spinning={this.state.addingUser} > diff --git a/webapp/components/form_error.jsx b/webapp/components/form_error.jsx new file mode 100644 index 000000000..b7d1de16a --- /dev/null +++ b/webapp/components/form_error.jsx @@ -0,0 +1,50 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +export default class FormError extends React.Component { + static get propTypes() { + // accepts either a single error or an array of errors + return { + error: React.PropTypes.node, + errors: React.PropTypes.arrayOf(React.PropTypes.node) + }; + } + + static get defaultProps() { + return { + error: null, + errors: [] + }; + } + + render() { + if (!this.props.error && this.props.errors.length === 0) { + return null; + } + + // look for the first truthy error to display + let message = this.props.error; + + if (!message) { + for (const error of this.props.errors) { + if (error) { + message = error; + } + } + } + + if (!message) { + return null; + } + + return ( + <div className='form-group has-error'> + <label className='control-label'> + {message} + </label> + </div> + ); + } +} diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx index feab8c9db..29d64517e 100644 --- a/webapp/components/more_direct_channels.jsx +++ b/webapp/components/more_direct_channels.jsx @@ -86,6 +86,7 @@ export default class MoreDirectChannels extends React.Component { createJoinDirectChannelButton({user}) { return ( <SpinnerButton + className='btn btm-sm btn-primary' spinning={this.state.loadingDMChannel === user.id} onClick={this.handleShowDirectChannel.bind(this, user)} > diff --git a/webapp/components/spinner_button.jsx b/webapp/components/spinner_button.jsx index fcc9af8cd..becf395c5 100644 --- a/webapp/components/spinner_button.jsx +++ b/webapp/components/spinner_button.jsx @@ -14,20 +14,10 @@ export default class SpinnerButton extends React.Component { }; } - constructor(props) { - super(props); - - this.handleClick = this.handleClick.bind(this); - } - - handleClick(e) { - if (this.props.onClick) { - this.props.onClick(e); - } - } - render() { - if (this.props.spinning) { + const {spinning, children, ...props} = this.props; // eslint-disable-line no-use-before-define + + if (spinning) { return ( <img className='spinner-button__gif' @@ -38,10 +28,10 @@ export default class SpinnerButton extends React.Component { return ( <button - onClick={this.handleClick} - className='btn btn-sm btn-primary' + className='btn btn-primary' + {...props} > - {this.props.children} + {children} </button> ); } diff --git a/webapp/root.jsx b/webapp/root.jsx index b6ec954c2..7e6352b11 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -40,6 +40,7 @@ import BackstageNavbar from 'components/backstage/backstage_navbar.jsx'; import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx'; import InstalledIntegrations from 'components/backstage/installed_integrations.jsx'; import AddIntegration from 'components/backstage/add_integration.jsx'; +import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; import SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx'; import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx'; @@ -276,7 +277,7 @@ function renderRootComponent() { components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, - center: null + center: AddIncomingWebhook }} /> <Route diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx index 4e9212bcb..b875c29e6 100644 --- a/webapp/stores/integration_store.jsx +++ b/webapp/stores/integration_store.jsx @@ -4,7 +4,6 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import Constants from 'utils/constants.jsx'; import EventEmitter from 'events'; -import * as Utils from 'utils/utils.jsx'; const ActionTypes = Constants.ActionTypes; @@ -44,10 +43,14 @@ class IntegrationStore extends EventEmitter { } setIncomingWebhooks(incomingWebhooks) { - this.incomingWebhooks = Utils.freezeArray(incomingWebhooks); + this.incomingWebhooks = incomingWebhooks; this.receivedIncomingWebhooks = true; } + addIncomingWebhook(incomingWebhook) { + this.incomingWebhooks.push(incomingWebhook); + } + hasReceivedOutgoingWebhooks() { return this.receivedIncomingWebhooks; } @@ -57,10 +60,14 @@ class IntegrationStore extends EventEmitter { } setOutgoingWebhooks(outgoingWebhooks) { - this.outgoingWebhooks = Utils.freezeArray(outgoingWebhooks); + this.outgoingWebhooks = outgoingWebhooks; this.receivedOutgoingWebhooks = true; } + addOutgoingWebhook(outgoingWebhook) { + this.outgoingWebhooks.push(outgoingWebhook); + } + handleEventPayload(payload) { const action = payload.action; @@ -69,10 +76,18 @@ class IntegrationStore extends EventEmitter { this.setIncomingWebhooks(action.incomingWebhooks); this.emitChange(); break; + case ActionTypes.RECEIVED_INCOMING_WEBHOOK: + this.addIncomingWebhook(action.incomingWebhook); + this.emitChange(); + break; case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS: this.setOutgoingWebhooks(action.outgoingWebhooks); this.emitChange(); break; + case ActionTypes.RECEIVED_OUTGOING_WEBHOOK: + this.addOutgoingWebhook(action.outgoingWebhook); + this.emitChange(); + break; } } } diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 2154fbe43..9ca2bd606 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1167,3 +1167,49 @@ export function listOutgoingHooks() { } ); } + +export function addIncomingHook(hook, success, error) { + client.addIncomingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_INCOMING_WEBHOOK, + incomingWebhook: data + }); + + if (success) { + success(); + } + }, + (err) => { + dispatchError(err, 'addIncomingHook'); + + if (error) { + error(err); + } + } + ); +} + +export function addOutgoingHook(hook, success, error) { + client.addOutgoingHook( + hook, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_OUTGOING_WEBHOOK, + outgoingWebhook: data + }); + + if (success) { + success(); + } + }, + (err) => { + dispatchError(err, 'addOutgoingHook'); + + if (error) { + error(err); + } + } + ); +} |