summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2016-03-30 10:05:26 -0400
committerChristopher Speller <crspeller@gmail.com>2016-03-30 10:05:26 -0400
commit2ab4581d5e658b22c4a957ec57bb3530f92ad66b (patch)
tree4202ebdcbc92905873a15d90a6fb68464bb1629f /webapp
parentfcc80818a8afb6f1e2f9974916f02d5fdeb72ec8 (diff)
parent6a101292c74d33e542e47f8e54fff5a5389bf2ef (diff)
downloadchat-2ab4581d5e658b22c4a957ec57bb3530f92ad66b.tar.gz
chat-2ab4581d5e658b22c4a957ec57bb3530f92ad66b.tar.bz2
chat-2ab4581d5e658b22c4a957ec57bb3530f92ad66b.zip
Merge pull request #2561 from hmhealey/plt1736
PLT-1736 Initial Backstage Work
Diffstat (limited to 'webapp')
-rw-r--r--webapp/components/backstage/add_incoming_webhook.jsx188
-rw-r--r--webapp/components/backstage/add_integration.jsx78
-rw-r--r--webapp/components/backstage/add_integration_option.jsx39
-rw-r--r--webapp/components/backstage/add_outgoing_webhook.jsx254
-rw-r--r--webapp/components/backstage/backstage_category.jsx68
-rw-r--r--webapp/components/backstage/backstage_navbar.jsx61
-rw-r--r--webapp/components/backstage/backstage_section.jsx80
-rw-r--r--webapp/components/backstage/backstage_sidebar.jsx68
-rw-r--r--webapp/components/backstage/installed_incoming_webhook.jsx71
-rw-r--r--webapp/components/backstage/installed_integrations.jsx289
-rw-r--r--webapp/components/backstage/installed_outgoing_webhook.jsx91
-rw-r--r--webapp/components/channel_invite_button.jsx1
-rw-r--r--webapp/components/channel_select.jsx79
-rw-r--r--webapp/components/form_error.jsx50
-rw-r--r--webapp/components/logged_in.jsx10
-rw-r--r--webapp/components/more_direct_channels.jsx1
-rw-r--r--webapp/components/navbar_dropdown.jsx17
-rw-r--r--webapp/components/spinner_button.jsx22
-rw-r--r--webapp/components/user_settings/manage_incoming_hooks.jsx225
-rw-r--r--webapp/components/user_settings/manage_outgoing_hooks.jsx397
-rw-r--r--webapp/components/user_settings/user_settings_integrations.jsx88
-rw-r--r--webapp/i18n/en.json65
-rw-r--r--webapp/i18n/es.json25
-rw-r--r--webapp/i18n/fr.json27
-rw-r--r--webapp/i18n/pt.json27
-rw-r--r--webapp/root.jsx42
-rw-r--r--webapp/sass/routes/_backstage.scss212
-rw-r--r--webapp/sass/routes/_module.scss1
-rw-r--r--webapp/stores/file_store.jsx5
-rw-r--r--webapp/stores/integration_store.jsx134
-rw-r--r--webapp/utils/async_client.jsx137
-rw-r--r--webapp/utils/constants.jsx8
-rw-r--r--webapp/webpack.config.js2
33 files changed, 2026 insertions, 836 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..fa7531fc6
--- /dev/null
+++ b/webapp/components/backstage/add_incoming_webhook.jsx
@@ -0,0 +1,188 @@
+// 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 ChannelSelect from 'components/channel_select.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.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+
+ this.state = {
+ name: '',
+ 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
+ };
+
+ AsyncClient.addIncomingHook(
+ hook,
+ () => {
+ browserHistory.push('/settings/integrations/installed');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateChannelId(e) {
+ this.setState({
+ channelId: e.target.value
+ });
+ }
+
+ render() {
+ 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='channelId'
+ >
+ <FormattedMessage
+ id='add_incoming_webhook.channel'
+ defaultMessage='Channel'
+ />
+ </label>
+ <ChannelSelect
+ id='channelId'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ />
+ </div>
+ <div className='add-integration__submit-row'>
+ <Link
+ className='btn btn-sm'
+ to={'/settings/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/backstage/add_integration.jsx b/webapp/components/backstage/add_integration.jsx
new file mode 100644
index 000000000..cebc1e8b0
--- /dev/null
+++ b/webapp/components/backstage/add_integration.jsx
@@ -0,0 +1,78 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {FormattedMessage} from 'react-intl';
+import AddIntegrationOption from './add_integration_option.jsx';
+
+import WebhookIcon from 'images/webhook_icon.jpg';
+
+export default class AddIntegration extends React.Component {
+ render() {
+ const options = [];
+
+ if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ options.push(
+ <AddIntegrationOption
+ key='incomingWebhook'
+ image={WebhookIcon}
+ title={
+ <FormattedMessage
+ id='add_integration.incomingWebhook.title'
+ defaultMessage='Incoming Webhook'
+ />
+ }
+ description={
+ <FormattedMessage
+ id='add_integration.incomingWebhook.description'
+ defaultMessage='Create webhook URLs for use in external integrations.'
+ />
+ }
+ link={'/settings/integrations/add/incoming_webhook'}
+ />
+ );
+ }
+
+ if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ options.push(
+ <AddIntegrationOption
+ key='outgoingWebhook'
+ image={WebhookIcon}
+ title={
+ <FormattedMessage
+ id='add_integration.outgoingWebhook.title'
+ defaultMessage='Outgoing Webhook'
+ />
+ }
+ description={
+ <FormattedMessage
+ id='add_integration.outgoingWebhook.description'
+ defaultMessage='Create webhooks to send new message events to an external integration.'
+ />
+ }
+ link={'/settings/integrations/add/outgoing_webhook'}
+ />
+ );
+ }
+
+ return (
+ <div className='backstage row'>
+ <div className='add-integration'>
+ <div className='backstage__header'>
+ <h1 className='text'>
+ <FormattedMessage
+ id='add_integration.header'
+ defaultMessage='Add Integration'
+ />
+ </h1>
+ </div>
+ </div>
+ <div className='add-integration__options'>
+ {options}
+ </div>
+ </div>
+ );
+ }
+}
+
diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/add_integration_option.jsx
new file mode 100644
index 000000000..3c3caf2f4
--- /dev/null
+++ b/webapp/components/backstage/add_integration_option.jsx
@@ -0,0 +1,39 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class AddIntegrationOption extends React.Component {
+ static get propTypes() {
+ return {
+ image: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ description: React.PropTypes.node.isRequired,
+ link: React.PropTypes.string.isRequired
+ };
+ }
+
+ render() {
+ const {image, title, description, link} = this.props;
+
+ return (
+ <Link
+ to={link}
+ className='add-integration-option'
+ >
+ <img
+ className='add-integration-option__image'
+ src={image}
+ />
+ <div className='add-integration-option__title'>
+ {title}
+ </div>
+ <div className='add-integration-option__description'>
+ {description}
+ </div>
+ </Link>
+ );
+ }
+}
diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx
new file mode 100644
index 000000000..3ae2f8606
--- /dev/null
+++ b/webapp/components/backstage/add_outgoing_webhook.jsx
@@ -0,0 +1,254 @@
+// 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 ChannelSelect from 'components/channel_select.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 AddOutgoingWebhook extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.updateName = this.updateName.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateChannelId = this.updateChannelId.bind(this);
+ this.updateTriggerWords = this.updateTriggerWords.bind(this);
+ this.updateCallbackUrls = this.updateCallbackUrls.bind(this);
+
+ this.state = {
+ name: '',
+ description: '',
+ channelId: '',
+ triggerWords: '',
+ callbackUrls: '',
+ 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.state.triggerWords) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_outgoing_webhook.triggerWordsOrChannelRequired'
+ defaultMessage='A valid channel or a list of trigger words is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!this.state.callbackUrls) {
+ 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: this.state.triggerWords.split('\n').map((word) => word.trim()),
+ callback_urls: this.state.callbackUrls.split('\n').map((url) => url.trim())
+ };
+
+ AsyncClient.addOutgoingHook(
+ hook,
+ () => {
+ browserHistory.push('/settings/integrations/installed');
+ },
+ (err) => {
+ this.setState({
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ updateName(e) {
+ this.setState({
+ name: e.target.value
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateChannelId(e) {
+ this.setState({
+ channelId: e.target.value
+ });
+ }
+
+ updateTriggerWords(e) {
+ this.setState({
+ triggerWords: e.target.value
+ });
+ }
+
+ updateCallbackUrls(e) {
+ this.setState({
+ callbackUrls: e.target.value
+ });
+ }
+
+ render() {
+ return (
+ <div className='backstage row'>
+ <div className='add-outgoing-webhook'>
+ <div className='backstage__header'>
+ <h1 className='text'>
+ <FormattedMessage
+ id='add_outgoing_webhook.header'
+ defaultMessage='Add Outgoing Webhook'
+ />
+ </h1>
+ </div>
+ </div>
+ <form className='add-outgoing-webhook__body'>
+ <div className='add-integration__row'>
+ <label
+ className='add-integration__label'
+ htmlFor='name'
+ >
+ <FormattedMessage
+ id='add_outgoing_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_outgoing_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='channelId'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.channel'
+ defaultMessage='Channel'
+ />
+ </label>
+ <ChannelSelect
+ id='channelId'
+ value={this.state.channelId}
+ onChange={this.updateChannelId}
+ />
+ </div>
+ <div className='add-integration__row'>
+ <label
+ className='add-integration__label'
+ htmlFor='triggerWords'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.triggerWords'
+ defaultMessage='Trigger Words (One Per Line)'
+ />
+ </label>
+ <textarea
+ id='triggerWords'
+ rows='3'
+ value={this.state.triggerWords}
+ onChange={this.updateTriggerWords}
+ />
+ </div>
+ <div className='add-integration__row'>
+ <label
+ className='add-integration__label'
+ htmlFor='callbackUrls'
+ >
+ <FormattedMessage
+ id='add_outgoing_webhook.callbackUrls'
+ defaultMessage='Callback URLs (One Per Line)'
+ />
+ </label>
+ <textarea
+ id='callbackUrls'
+ rows='3'
+ value={this.state.callbackUrls}
+ onChange={this.updateCallbackUrls}
+ />
+ </div>
+ <div className='add-integration__submit-row'>
+ <Link
+ className='btn btn-sm'
+ to={'/settings/integrations/add'}
+ >
+ <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>
+ <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ </form>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/backstage_category.jsx
new file mode 100644
index 000000000..e8b0b57ae
--- /dev/null
+++ b/webapp/components/backstage/backstage_category.jsx
@@ -0,0 +1,68 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class BackstageCategory extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ icon: React.PropTypes.string.isRequired,
+ parentLink: React.PropTypes.string,
+ children: React.PropTypes.arrayOf(React.PropTypes.element)
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: '',
+ children: []
+ };
+ }
+
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
+ render() {
+ const {name, title, icon, parentLink, children} = this.props;
+
+ const link = parentLink + '/' + name;
+
+ let clonedChildren = null;
+ if (children.length > 0 && this.context.router.isActive(link)) {
+ clonedChildren = (
+ <ul className='sections'>
+ {
+ React.Children.map(children, (child) => {
+ return React.cloneElement(child, {
+ parentLink: link
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ return (
+ <li className='backstage__sidebar__category'>
+ <Link
+ to={link}
+ className='category-title'
+ activeClassName='category-title--active'
+ >
+ <i className={'fa ' + icon}/>
+ <span className='category-title__text'>
+ {title}
+ </span>
+ </Link>
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/backstage_navbar.jsx
new file mode 100644
index 000000000..555165791
--- /dev/null
+++ b/webapp/components/backstage/backstage_navbar.jsx
@@ -0,0 +1,61 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import TeamStore from 'stores/team_store.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import {Link} from 'react-router';
+
+export default class BackstageNavbar extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+
+ this.state = {
+ team: TeamStore.getCurrent()
+ };
+ }
+
+ componentDidMount() {
+ TeamStore.addChangeListener(this.handleChange);
+ }
+
+ componentWillUnmount() {
+ TeamStore.removeChangeListener(this.handleChange);
+ }
+
+ handleChange() {
+ this.setState({
+ team: TeamStore.getCurrent()
+ });
+ }
+
+ render() {
+ if (!this.state.team) {
+ return null;
+ }
+
+ return (
+ <div className='backstage__navbar row'>
+ <Link
+ className='backstage__navbar__back'
+ to={`/${this.state.team.display_name}/channels/town-square`}
+ >
+ <i className='fa fa-angle-left'/>
+ <span>
+ <FormattedMessage
+ id='backstage_navbar.backToMattermost'
+ defaultMessage='Back to {siteName}'
+ values={{
+ siteName: global.window.mm_config.SiteName
+ }}
+ />
+ </span>
+ </Link>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx
new file mode 100644
index 000000000..d6ce2b258
--- /dev/null
+++ b/webapp/components/backstage/backstage_section.jsx
@@ -0,0 +1,80 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import {Link} from 'react-router';
+
+export default class BackstageSection extends React.Component {
+ static get propTypes() {
+ return {
+ name: React.PropTypes.string.isRequired,
+ title: React.PropTypes.node.isRequired,
+ parentLink: React.PropTypes.string,
+ subsection: React.PropTypes.bool,
+ children: React.PropTypes.arrayOf(React.PropTypes.element)
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ parentLink: '',
+ subsection: false,
+ children: []
+ };
+ }
+
+ static get contextTypes() {
+ return {
+ router: React.PropTypes.object.isRequired
+ };
+ }
+
+ getLink() {
+ return this.props.parentLink + '/' + this.props.name;
+ }
+
+ render() {
+ const {title, subsection, children} = this.props;
+
+ const link = this.getLink();
+
+ let clonedChildren = null;
+ if (children.length > 0) {
+ clonedChildren = (
+ <ul className='subsections'>
+ {
+ React.Children.map(children, (child) => {
+ return React.cloneElement(child, {
+ parentLink: link,
+ subsection: true
+ });
+ })
+ }
+ </ul>
+ );
+ }
+
+ let className = 'section';
+ if (subsection) {
+ className = 'subsection';
+ }
+
+ return (
+ <li className={className}>
+ <Link
+ className={`${className}-title`}
+ activeClassName={`${className}-title--active`}
+ onlyActiveOnIndex={true}
+ onClick={this.handleClick}
+ to={link}
+ >
+ <span className={`${className}-title__text`}>
+ {title}
+ </span>
+ </Link>
+ {clonedChildren}
+ </li>
+ );
+ }
+}
diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx
new file mode 100644
index 000000000..63a0df5cb
--- /dev/null
+++ b/webapp/components/backstage/backstage_sidebar.jsx
@@ -0,0 +1,68 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import BackstageCategory from './backstage_category.jsx';
+import BackstageSection from './backstage_section.jsx';
+import {FormattedMessage} from 'react-intl';
+
+export default class BackstageSidebar extends React.Component {
+ render() {
+ return (
+ <div className='backstage__sidebar'>
+ <ul>
+ <BackstageCategory
+ name='integrations'
+ parentLink={'/settings'}
+ icon='fa-link'
+ title={
+ <FormattedMessage
+ id='backstage_sidebar.integrations'
+ defaultMessage='Integrations'
+ />
+ }
+ >
+ <BackstageSection
+ name='installed'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.installed'
+ defaultMessage='Installed Integrations'
+ />
+ )}
+ />
+ <BackstageSection
+ name='add'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.add'
+ defaultMessage='Add Integration'
+ />
+ )}
+ >
+ <BackstageSection
+ name='incoming_webhook'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.add.incomingWebhook'
+ defaultMessage='Incoming Webhook'
+ />
+ )}
+ />
+ <BackstageSection
+ name='outgoing_webhook'
+ title={(
+ <FormattedMessage
+ id='backstage_sidebar.integrations.add.outgoingWebhook'
+ defaultMessage='Outgoing Webhook'
+ />
+ )}
+ />
+ </BackstageSection>
+ </BackstageCategory>
+ </ul>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx
new file mode 100644
index 000000000..4ca421a02
--- /dev/null
+++ b/webapp/components/backstage/installed_incoming_webhook.jsx
@@ -0,0 +1,71 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class InstalledIncomingWebhook extends React.Component {
+ static get propTypes() {
+ return {
+ incomingWebhook: React.PropTypes.object.isRequired,
+ onDeleteClick: React.PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleDeleteClick = this.handleDeleteClick.bind(this);
+ }
+
+ handleDeleteClick(e) {
+ e.preventDefault();
+
+ this.props.onDeleteClick(this.props.incomingWebhook);
+ }
+
+ render() {
+ const incomingWebhook = this.props.incomingWebhook;
+
+ const channel = ChannelStore.get(incomingWebhook.channel_id);
+ const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ return (
+ <div className='installed-integrations__item installed-integrations__incoming-webhook'>
+ <div className='details'>
+ <div className='details-row'>
+ <span className='name'>
+ {channelName}
+ </span>
+ <span className='type'>
+ <FormattedMessage
+ id='installed_integrations.incomingWebhookType'
+ defaultMessage='(Incoming Webhook)'
+ />
+ </span>
+ </div>
+ <div className='details-row'>
+ <span className='description'>
+ {Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id}
+ </span>
+ </div>
+ </div>
+ <div className='actions'>
+ <a
+ href='#'
+ onClick={this.handleDeleteClick}
+ >
+ <FormattedMessage
+ id='installed_integrations.delete'
+ defaultMessage='Delete'
+ />
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
new file mode 100644
index 000000000..ff0b6e4ec
--- /dev/null
+++ b/webapp/components/backstage/installed_integrations.jsx
@@ -0,0 +1,289 @@
+// 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 ChannelStore from 'stores/channel_store.jsx';
+import IntegrationStore from 'stores/integration_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
+import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
+import {Link} from 'react-router';
+
+export default class InstalledIntegrations extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
+ this.updateFilter = this.updateFilter.bind(this);
+ this.updateTypeFilter = this.updateTypeFilter.bind(this);
+
+ this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this);
+ this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this);
+ this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this);
+
+ this.state = {
+ incomingWebhooks: [],
+ outgoingWebhooks: [],
+ typeFilter: '',
+ filter: ''
+ };
+ }
+
+ componentWillMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableIncomingWebhooks === 'true') {
+ if (IntegrationStore.hasReceivedIncomingWebhooks()) {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks()
+ });
+ } else {
+ AsyncClient.listIncomingHooks();
+ }
+ }
+
+ if (window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (IntegrationStore.hasReceivedOutgoingWebhooks()) {
+ this.setState({
+ outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
+ });
+ } else {
+ AsyncClient.listOutgoingHooks();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleIntegrationChange() {
+ this.setState({
+ incomingWebhooks: IntegrationStore.getIncomingWebhooks(),
+ outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
+ });
+ }
+
+ updateTypeFilter(e, typeFilter) {
+ e.preventDefault();
+
+ this.setState({
+ typeFilter
+ });
+ }
+
+ updateFilter(e) {
+ this.setState({
+ filter: e.target.value
+ });
+ }
+
+ deleteIncomingWebhook(incomingWebhook) {
+ AsyncClient.deleteIncomingHook(incomingWebhook.id);
+ }
+
+ regenOutgoingWebhookToken(outgoingWebhook) {
+ AsyncClient.regenOutgoingHookToken(outgoingWebhook.id);
+ }
+
+ deleteOutgoingWebhook(outgoingWebhook) {
+ AsyncClient.deleteOutgoingHook(outgoingWebhook.id);
+ }
+
+ renderTypeFilters(incomingWebhooks, outgoingWebhooks) {
+ const fields = [];
+
+ if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) {
+ let filterClassName = 'type-filter';
+ if (this.state.typeFilter === '') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ <a
+ key='allFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.updateTypeFilter(e, '')}
+ >
+ <FormattedMessage
+ id='installed_integrations.allFilter'
+ defaultMessage='All ({count})'
+ values={{
+ count: incomingWebhooks.length + outgoingWebhooks.length
+ }}
+ />
+ </a>
+ );
+ }
+
+ if (incomingWebhooks.length > 0) {
+ fields.push(
+ <span
+ key='incomingWebhooksDivider'
+ className='divider'
+ >
+ {'|'}
+ </span>
+ );
+
+ let filterClassName = 'type-filter';
+ if (this.state.typeFilter === 'incomingWebhooks') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ <a
+ key='incomingWebhooksFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.updateTypeFilter(e, 'incomingWebhooks')}
+ >
+ <FormattedMessage
+ id='installed_integrations.incomingWebhooksFilter'
+ defaultMessage='Incoming Webhooks ({count})'
+ values={{
+ count: incomingWebhooks.length
+ }}
+ />
+ </a>
+ );
+ }
+
+ if (outgoingWebhooks.length > 0) {
+ fields.push(
+ <span
+ key='outgoingWebhooksDivider'
+ className='divider'
+ >
+ {'|'}
+ </span>
+ );
+
+ let filterClassName = 'type-filter';
+ if (this.state.typeFilter === 'outgoingWebhooks') {
+ filterClassName += ' type-filter--selected';
+ }
+
+ fields.push(
+ <a
+ key='outgoingWebhooksFilter'
+ className={filterClassName}
+ href='#'
+ onClick={(e) => this.updateTypeFilter(e, 'outgoingWebhooks')}
+ >
+ <FormattedMessage
+ id='installed_integrations.outgoingWebhooksFilter'
+ defaultMessage='Outgoing Webhooks ({count})'
+ values={{
+ count: outgoingWebhooks.length
+ }}
+ />
+ </a>
+ );
+ }
+
+ return (
+ <div className='type-filters'>
+ {fields}
+ </div>
+ );
+ }
+
+ render() {
+ const incomingWebhooks = this.state.incomingWebhooks;
+ const outgoingWebhooks = this.state.outgoingWebhooks;
+
+ const filter = this.state.filter.toLowerCase();
+
+ const integrations = [];
+ if (!this.state.typeFilter || this.state.typeFilter === 'incomingWebhooks') {
+ for (const incomingWebhook of incomingWebhooks) {
+ if (filter) {
+ const channel = ChannelStore.get(incomingWebhook.channel_id);
+
+ if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) {
+ continue;
+ }
+ }
+
+ integrations.push(
+ <InstalledIncomingWebhook
+ key={incomingWebhook.id}
+ incomingWebhook={incomingWebhook}
+ onDeleteClick={this.deleteIncomingWebhook}
+ />
+ );
+ }
+ }
+
+ if (!this.state.typeFilter || this.state.typeFilter === 'outgoingWebhooks') {
+ for (const outgoingWebhook of outgoingWebhooks) {
+ if (filter) {
+ const channel = ChannelStore.get(outgoingWebhook.channel_id);
+
+ if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) {
+ continue;
+ }
+ }
+
+ integrations.push(
+ <InstalledOutgoingWebhook
+ key={outgoingWebhook.id}
+ outgoingWebhook={outgoingWebhook}
+ onRegenToken={this.regenOutgoingWebhookToken}
+ onDelete={this.deleteOutgoingWebhook}
+ />
+ );
+ }
+ }
+
+ return (
+ <div className='backstage row'>
+ <div className='installed-integrations'>
+ <div className='backstage__header'>
+ <h1 className='text'>
+ <FormattedMessage
+ id='installed_integrations.header'
+ defaultMessage='Installed Integrations'
+ />
+ </h1>
+ <Link
+ className='add-integrations-link'
+ to={'/settings/integrations/add'}
+ >
+ <button
+ type='button'
+ className='btn btn-primary'
+ >
+ <span>
+ <FormattedMessage
+ id='installed_integrations.add'
+ defaultMessage='Add Integration'
+ />
+ </span>
+ </button>
+ </Link>
+ </div>
+ <div className='installed-integrations__filters'>
+ {this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)}
+ <input
+ type='search'
+ placeholder={Utils.localizeMessage('installed_integrations.search', 'Search Integrations')}
+ value={this.state.filter}
+ onChange={this.updateFilter}
+ style={{flexGrow: 0, flexShrink: 0}}
+ />
+ </div>
+ <div className='installed-integrations__list'>
+ {integrations}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx
new file mode 100644
index 000000000..12e1a5c81
--- /dev/null
+++ b/webapp/components/backstage/installed_outgoing_webhook.jsx
@@ -0,0 +1,91 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+
+export default class InstalledOutgoingWebhook extends React.Component {
+ static get propTypes() {
+ return {
+ outgoingWebhook: React.PropTypes.object.isRequired,
+ onRegenToken: React.PropTypes.func.isRequired,
+ onDelete: React.PropTypes.func.isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleRegenToken = this.handleRegenToken.bind(this);
+ this.handleDelete = this.handleDelete.bind(this);
+ }
+
+ handleRegenToken(e) {
+ e.preventDefault();
+
+ this.props.onRegenToken(this.props.outgoingWebhook);
+ }
+
+ handleDelete(e) {
+ e.preventDefault();
+
+ this.props.onDelete(this.props.outgoingWebhook);
+ }
+
+ render() {
+ const outgoingWebhook = this.props.outgoingWebhook;
+
+ const channel = ChannelStore.get(outgoingWebhook.channel_id);
+ const channelName = channel ? channel.display_name : 'cannot find channel';
+
+ return (
+ <div className='installed-integrations__item installed-integrations__outgoing-webhook'>
+ <div className='details'>
+ <div className='details-row'>
+ <span className='name'>
+ {channelName}
+ </span>
+ <span className='type'>
+ <FormattedMessage
+ id='installed_integrations.outgoingWebhookType'
+ defaultMessage='(Outgoing Webhook)'
+ />
+ </span>
+ </div>
+ <div className='details-row'>
+ <span className='description'>
+ {Utils.getWindowLocationOrigin() + '/hooks/' + outgoingWebhook.id}
+ {' - '}
+ {outgoingWebhook.token}
+ </span>
+ </div>
+ </div>
+ <div className='actions'>
+ <a
+ href='#'
+ onClick={this.handleRegenToken}
+ >
+ <FormattedMessage
+ id='installed_integrations.regenToken'
+ defaultMessage='Regen Token'
+ />
+ </a>
+ {' - '}
+ <a
+ href='#'
+ onClick={this.handleDelete}
+ >
+ <FormattedMessage
+ id='installed_integrations.delete'
+ defaultMessage='Delete'
+ />
+ </a>
+ </div>
+ </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/channel_select.jsx b/webapp/components/channel_select.jsx
new file mode 100644
index 000000000..8622d1f57
--- /dev/null
+++ b/webapp/components/channel_select.jsx
@@ -0,0 +1,79 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Constants from 'utils/constants.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+export default class ChannelSelect extends React.Component {
+ static get propTypes() {
+ return {
+ onChange: React.PropTypes.func,
+ value: React.PropTypes.string
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.handleChannelChange = this.handleChannelChange.bind(this);
+
+ this.state = {
+ channels: []
+ };
+ }
+
+ componentWillMount() {
+ this.setState({
+ channels: ChannelStore.getAll()
+ });
+
+ ChannelStore.addChangeListener(this.handleChannelChange);
+ }
+
+ componentWillUnmount() {
+ ChannelStore.removeChangeListener(this.handleChannelChange);
+ }
+
+ handleChannelChange() {
+ this.setState({
+ channels: ChannelStore.getAll()
+ });
+ }
+
+ render() {
+ const options = [
+ <option
+ key=''
+ value=''
+ >
+ {Utils.localizeMessage('channel_select.placeholder', '--- Select a channel ---')}
+ </option>
+ ];
+
+ this.state.channels.forEach((channel) => {
+ if (channel.type !== Constants.DM_CHANNEL) {
+ options.push(
+ <option
+ key={channel.id}
+ value={channel.id}
+ >
+ {channel.display_name}
+ </option>
+ );
+ }
+ });
+
+ return (
+ <select
+ className='form-control'
+ value={this.props.value}
+ onChange={this.props.onChange}
+ >
+ {options}
+ </select>
+ );
+ }
+}
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/logged_in.jsx b/webapp/components/logged_in.jsx
index 53db501bf..fd09aac9e 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -200,6 +200,9 @@ export default class LoggedIn extends React.Component {
content = this.props.children;
} else {
content.push(
+ this.props.navbar
+ );
+ content.push(
this.props.sidebar
);
content.push(
@@ -247,8 +250,9 @@ LoggedIn.defaultProps = {
};
LoggedIn.propTypes = {
- children: React.PropTypes.object,
- sidebar: React.PropTypes.object,
- center: React.PropTypes.object,
+ children: React.PropTypes.arrayOf(React.PropTypes.element),
+ navbar: React.PropTypes.element,
+ sidebar: React.PropTypes.element,
+ center: React.PropTypes.element,
params: React.PropTypes.object
};
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/navbar_dropdown.jsx b/webapp/components/navbar_dropdown.jsx
index 7e42a71ea..da1ae237e 100644
--- a/webapp/components/navbar_dropdown.jsx
+++ b/webapp/components/navbar_dropdown.jsx
@@ -59,6 +59,7 @@ export default class NavbarDropdown extends React.Component {
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
+ let integrationsLink = null;
if (currentUser != null) {
isAdmin = Utils.isAdmin(currentUser.roles);
@@ -125,6 +126,21 @@ export default class NavbarDropdown extends React.Component {
);
}
+ if (window.mm_config.EnableIncomingWebhooks === 'true' || window.mm_config.EnableOutgoingWebhooks === 'true') {
+ if (isAdmin || window.EnableAdminOnlyIntegrations !== 'true') {
+ integrationsLink = (
+ <li>
+ <Link to={'/settings/integrations'}>
+ <FormattedMessage
+ id='navbar_dropdown.integrations'
+ defaultMessage='Integrations'
+ />
+ </Link>
+ </li>
+ );
+ }
+ }
+
if (isSystemAdmin) {
sysAdminLink = (
<li>
@@ -238,6 +254,7 @@ export default class NavbarDropdown extends React.Component {
</li>
{adminDivider}
{teamSettings}
+ {integrationsLink}
{manageLink}
{sysAdminLink}
{teams}
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/components/user_settings/manage_incoming_hooks.jsx b/webapp/components/user_settings/manage_incoming_hooks.jsx
deleted file mode 100644
index b61b331ce..000000000
--- a/webapp/components/user_settings/manage_incoming_hooks.jsx
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import * as Client from 'utils/client.jsx';
-import * as Utils from 'utils/utils.jsx';
-import Constants from 'utils/constants.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-import LoadingScreen from '../loading_screen.jsx';
-
-import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-import React from 'react';
-
-export default class ManageIncomingHooks extends React.Component {
- constructor() {
- super();
-
- this.getHooks = this.getHooks.bind(this);
- this.addNewHook = this.addNewHook.bind(this);
- this.updateChannelId = this.updateChannelId.bind(this);
-
- this.state = {hooks: [], channelId: ChannelStore.getByName(Constants.DEFAULT_CHANNEL).id, getHooksComplete: false};
- }
- componentDidMount() {
- this.getHooks();
- }
- addNewHook() {
- const hook = {};
- hook.channel_id = this.state.channelId;
-
- Client.addIncomingHook(
- hook,
- (data) => {
- let hooks = this.state.hooks;
- if (!hooks) {
- hooks = [];
- }
- hooks.push(data);
- this.setState({hooks});
- },
- (err) => {
- this.setState({serverError: err});
- }
- );
- }
- removeHook(id) {
- const data = {};
- data.id = id;
-
- Client.deleteIncomingHook(
- data,
- () => {
- const hooks = this.state.hooks;
- let index = -1;
- for (let i = 0; i < hooks.length; i++) {
- if (hooks[i].id === id) {
- index = i;
- break;
- }
- }
-
- if (index !== -1) {
- hooks.splice(index, 1);
- }
-
- this.setState({hooks});
- },
- (err) => {
- this.setState({serverError: err});
- }
- );
- }
- getHooks() {
- Client.listIncomingHooks(
- (data) => {
- const state = this.state;
-
- if (data) {
- state.hooks = data;
- }
-
- state.getHooksComplete = true;
- this.setState(state);
- },
- (err) => {
- this.setState({serverError: err});
- }
- );
- }
- updateChannelId(e) {
- this.setState({channelId: e.target.value});
- }
- render() {
- let serverError;
- if (this.state.serverError) {
- serverError = <label className='has-error'>{this.state.serverError}</label>;
- }
-
- const channels = ChannelStore.getAll();
- const options = [];
- channels.forEach((channel) => {
- if (channel.type !== Constants.DM_CHANNEL) {
- options.push(
- <option
- key={'incoming-hook' + channel.id}
- value={channel.id}
- >
- {channel.display_name}
- </option>
- );
- }
- });
-
- let disableButton = '';
- if (this.state.channelId === '') {
- disableButton = ' disable';
- }
-
- const hooks = [];
- this.state.hooks.forEach((hook) => {
- const c = ChannelStore.get(hook.channel_id);
- if (c) {
- hooks.push(
- <div
- key={hook.id}
- className='webhook__item'
- >
- <div className='padding-top x2 webhook__url'>
- <strong>{'URL: '}</strong>
- <span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
- </div>
- <div className='padding-top'>
- <strong>
- <FormattedMessage
- id='user.settings.hooks_in.channel'
- defaultMessage='Channel: '
- />
- </strong>{c.display_name}
- </div>
- <a
- className={'webhook__remove'}
- href='#'
- onClick={this.removeHook.bind(this, hook.id)}
- >
- <span aria-hidden='true'>{'×'}</span>
- </a>
- <div className='padding-top x2 divider-light'></div>
- </div>
- );
- }
- });
-
- let displayHooks;
- if (!this.state.getHooksComplete) {
- displayHooks = <LoadingScreen/>;
- } else if (hooks.length > 0) {
- displayHooks = hooks;
- } else {
- displayHooks = (
- <div className='padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_in.none'
- defaultMessage='None'
- />
- </div>
- );
- }
-
- const existingHooks = (
- <div className='webhooks__container'>
- <label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_in.existing'
- defaultMessage='Existing incoming webhooks'
- />
- </label>
- <div className='padding-top divider-light'></div>
- <div className='webhooks__list'>
- {displayHooks}
- </div>
- </div>
- );
-
- return (
- <div key='addIncomingHook'>
- <FormattedHTMLMessage
- id='user.settings.hooks_in.description'
- defaultMessage='Create webhook URLs for use in external integrations. Please see <a href="http://docs.mattermost.com/developer/webhooks-incoming.html" target="_blank">incoming webhooks documentation</a> to learn more. View all incoming webhooks configured on this team below.'
- />
- <div><label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_in.addTitle'
- defaultMessage='Add a new incoming webhook'
- />
- </label></div>
- <div className='row padding-top'>
- <div className='col-sm-10 padding-bottom'>
- <select
- ref='channelName'
- className='form-control'
- value={this.state.channelId}
- onChange={this.updateChannelId}
- >
- {options}
- </select>
- {serverError}
- </div>
- <div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'>
- <a
- className={'btn form-control no-padding btn-sm btn-primary' + disableButton}
- href='#'
- onClick={this.addNewHook}
- >
- <FormattedMessage
- id='user.settings.hooks_in.add'
- defaultMessage='Add'
- />
- </a>
- </div>
- </div>
- {existingHooks}
- </div>
- );
- }
-}
diff --git a/webapp/components/user_settings/manage_outgoing_hooks.jsx b/webapp/components/user_settings/manage_outgoing_hooks.jsx
deleted file mode 100644
index 8adec09ce..000000000
--- a/webapp/components/user_settings/manage_outgoing_hooks.jsx
+++ /dev/null
@@ -1,397 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import LoadingScreen from '../loading_screen.jsx';
-
-import ChannelStore from 'stores/channel_store.jsx';
-
-import * as Client from 'utils/client.jsx';
-import Constants from 'utils/constants.jsx';
-
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
-
-const holders = defineMessages({
- optional: {
- id: 'user.settings.hooks_out.optional',
- defaultMessage: 'Optional if channel selected'
- },
- callbackHolder: {
- id: 'user.settings.hooks_out.callbackHolder',
- defaultMessage: 'Each URL must start with http:// or https://'
- },
- select: {
- id: 'user.settings.hooks_out.select',
- defaultMessage: '--- Select a channel ---'
- }
-});
-
-import React from 'react';
-
-class ManageOutgoingHooks extends React.Component {
- constructor() {
- super();
-
- this.getHooks = this.getHooks.bind(this);
- this.addNewHook = this.addNewHook.bind(this);
- this.updateChannelId = this.updateChannelId.bind(this);
- this.updateTriggerWords = this.updateTriggerWords.bind(this);
- this.updateCallbackURLs = this.updateCallbackURLs.bind(this);
-
- this.state = {hooks: [], channelId: '', triggerWords: '', callbackURLs: '', getHooksComplete: false};
- }
- componentDidMount() {
- this.getHooks();
- }
- addNewHook(e) {
- e.preventDefault();
-
- if ((this.state.channelId === '' && this.state.triggerWords === '') ||
- this.state.callbackURLs === '') {
- return;
- }
-
- const hook = {};
- hook.channel_id = this.state.channelId;
- if (this.state.triggerWords.length !== 0) {
- hook.trigger_words = this.state.triggerWords.trim().split(',');
- }
- hook.callback_urls = this.state.callbackURLs.split('\n').map((url) => url.trim());
-
- Client.addOutgoingHook(
- hook,
- (data) => {
- let hooks = Object.assign([], this.state.hooks);
- if (!hooks) {
- hooks = [];
- }
- hooks.push(data);
- this.setState({hooks, addError: null, channelId: '', triggerWords: '', callbackURLs: ''});
- },
- (err) => {
- this.setState({addError: err.message});
- }
- );
- }
- removeHook(id) {
- const data = {};
- data.id = id;
-
- Client.deleteOutgoingHook(
- data,
- () => {
- const hooks = this.state.hooks;
- let index = -1;
- for (let i = 0; i < hooks.length; i++) {
- if (hooks[i].id === id) {
- index = i;
- break;
- }
- }
-
- if (index !== -1) {
- hooks.splice(index, 1);
- }
-
- this.setState({hooks});
- },
- (err) => {
- this.setState({editError: err.message});
- }
- );
- }
- regenToken(id) {
- const regenData = {};
- regenData.id = id;
-
- Client.regenOutgoingHookToken(
- regenData,
- (data) => {
- const hooks = Object.assign([], this.state.hooks);
- for (let i = 0; i < hooks.length; i++) {
- if (hooks[i].id === id) {
- hooks[i] = data;
- break;
- }
- }
-
- this.setState({hooks, editError: null});
- },
- (err) => {
- this.setState({editError: err.message});
- }
- );
- }
- getHooks() {
- Client.listOutgoingHooks(
- (data) => {
- if (data) {
- this.setState({hooks: data, getHooksComplete: true, editError: null});
- }
- },
- (err) => {
- this.setState({editError: err.message});
- }
- );
- }
- updateChannelId(e) {
- this.setState({channelId: e.target.value});
- }
- updateTriggerWords(e) {
- this.setState({triggerWords: e.target.value});
- }
- updateCallbackURLs(e) {
- this.setState({callbackURLs: e.target.value});
- }
- render() {
- let addError;
- if (this.state.addError) {
- addError = <label className='has-error'>{this.state.addError}</label>;
- }
- let editError;
- if (this.state.editError) {
- addError = <label className='has-error'>{this.state.editError}</label>;
- }
-
- const channels = ChannelStore.getAll();
- const options = [];
- options.push(
- <option
- key='select-channel'
- value=''
- >
- {this.props.intl.formatMessage(holders.select)}
- </option>
- );
-
- channels.forEach((channel) => {
- if (channel.type === Constants.OPEN_CHANNEL) {
- options.push(
- <option
- key={'outgoing-hook' + channel.id}
- value={channel.id}
- >
- {channel.display_name}
- </option>
- );
- }
- });
-
- const hooks = [];
- this.state.hooks.forEach((hook) => {
- const c = ChannelStore.get(hook.channel_id);
-
- if (!c && hook.channel_id && hook.channel_id.length !== 0) {
- return;
- }
-
- let channelDiv;
- if (c) {
- channelDiv = (
- <div className='padding-top'>
- <strong>
- <FormattedMessage
- id='user.settings.hooks_out.channel'
- defaultMessage='Channel: '
- />
- </strong>{c.display_name}
- </div>
- );
- }
-
- let triggerDiv;
- if (hook.trigger_words && hook.trigger_words.length !== 0) {
- triggerDiv = (
- <div className='padding-top'>
- <strong>
- <FormattedMessage
- id='user.settings.hooks_out.trigger'
- defaultMessage='Trigger Words: '
- />
- </strong>{hook.trigger_words.join(', ')}
- </div>
- );
- }
-
- hooks.push(
- <div
- key={hook.id}
- className='webhook__item'
- >
- <div className='padding-top x2 webhook__url'>
- <strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
- </div>
- {channelDiv}
- {triggerDiv}
- <div className='padding-top'>
- <strong>{'Token: '}</strong>{hook.token}
- </div>
- <div className='padding-top'>
- <a
- className='text-danger'
- href='#'
- onClick={this.regenToken.bind(this, hook.id)}
- >
- <FormattedMessage
- id='user.settings.hooks_out.regen'
- defaultMessage='Regen Token'
- />
- </a>
- <a
- className='webhook__remove'
- href='#'
- onClick={this.removeHook.bind(this, hook.id)}
- >
- <span aria-hidden='true'>{'×'}</span>
- </a>
- </div>
- <div className='padding-top x2 divider-light'></div>
- </div>
- );
- });
-
- let displayHooks;
- if (!this.state.getHooksComplete) {
- displayHooks = <LoadingScreen/>;
- } else if (hooks.length > 0) {
- displayHooks = hooks;
- } else {
- displayHooks = (
- <div className='padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_out.none'
- defaultMessage='None'
- />
- </div>
- );
- }
-
- const existingHooks = (
- <div className='webhooks__container'>
- <label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_out.existing'
- defaultMessage='Existing outgoing webhooks'
- />
- </label>
- <div className='padding-top divider-light'></div>
- <div className='webhooks__list'>
- {displayHooks}
- </div>
- </div>
- );
-
- const disableButton = (this.state.channelId === '' && this.state.triggerWords === '') || this.state.callbackURLs === '';
-
- return (
- <div key='addOutgoingHook'>
- <FormattedHTMLMessage
- id='user.settings.hooks_out.addDescription'
- defaultMessage='Create webhooks to send new message events to an external integration. Please see <a href="http://docs.mattermost.com/developer/webhooks-outgoing.html" target="_blank">outgoing webhooks documentation</a> to learn more. View all outgoing webhooks configured on this team below.'
- />
- <div><label className='control-label padding-top x2'>
- <FormattedMessage
- id='user.settings.hooks_out.addTitle'
- defaultMessage='Add a new outgoing webhook'
- />
- </label></div>
- <div className='padding-top divider-light'></div>
- <div className='padding-top'>
- <div>
- <label className='control-label'>
- <FormattedMessage
- id='user.settings.hooks_out.channel'
- defaultMessage='Channel: '
- />
- </label>
- <div className='padding-top'>
- <select
- ref='channelName'
- className='form-control'
- value={this.state.channelId}
- onChange={this.updateChannelId}
- >
- {options}
- </select>
- </div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.hooks_out.only'
- defaultMessage='Only public channels can be used'
- />
- </div>
- </div>
- <div className='padding-top x2'>
- <label className='control-label'>
- <FormattedMessage
- id='user.settings.hooks_out.trigger'
- defaultMessage='Trigger Words: '
- />
- </label>
- <div className='padding-top'>
- <input
- ref='triggerWords'
- className='form-control'
- value={this.state.triggerWords}
- onChange={this.updateTriggerWords}
- placeholder={this.props.intl.formatMessage(holders.optional)}
- />
- </div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.hooks_out.comma'
- defaultMessage='Comma separated words to trigger on'
- />
- </div>
- </div>
- <div className='padding-top x2'>
- <label className='control-label'>
- <FormattedMessage
- id='user.settings.hooks_out.callback'
- defaultMessage='Callback URLs: '
- />
- </label>
- <div className='padding-top'>
- <textarea
- ref='callbackURLs'
- className='form-control no-resize'
- value={this.state.callbackURLs}
- resize={false}
- rows={3}
- onChange={this.updateCallbackURLs}
- placeholder={this.props.intl.formatMessage(holders.callbackHolder)}
- />
- </div>
- <div className='padding-top'>
- <FormattedMessage
- id='user.settings.hooks_out.callbackDesc'
- defaultMessage='New line separated URLs that will receive the HTTP POST event'
- />
- </div>
- {addError}
- </div>
- <div className='padding-top padding-bottom'>
- <a
- className={'btn btn-sm btn-primary'}
- href='#'
- disabled={disableButton}
- onClick={this.addNewHook}
- >
- <FormattedMessage
- id='user.settings.hooks_out.add'
- defaultMessage='Add'
- />
- </a>
- </div>
- </div>
- {existingHooks}
- {editError}
- </div>
- );
- }
-}
-
-ManageOutgoingHooks.propTypes = {
- intl: intlShape.isRequired
-};
-
-export default injectIntl(ManageOutgoingHooks);
diff --git a/webapp/components/user_settings/user_settings_integrations.jsx b/webapp/components/user_settings/user_settings_integrations.jsx
index 9061e34df..37081b863 100644
--- a/webapp/components/user_settings/user_settings_integrations.jsx
+++ b/webapp/components/user_settings/user_settings_integrations.jsx
@@ -4,29 +4,11 @@
import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
-import ManageIncomingHooks from './manage_incoming_hooks.jsx';
-import ManageOutgoingHooks from './manage_outgoing_hooks.jsx';
import ManageCommandHooks from './manage_command_hooks.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
const holders = defineMessages({
- inName: {
- id: 'user.settings.integrations.incomingWebhooks',
- defaultMessage: 'Incoming Webhooks'
- },
- inDesc: {
- id: 'user.settings.integrations.incomingWebhooksDescription',
- defaultMessage: 'Manage your incoming webhooks'
- },
- outName: {
- id: 'user.settings.integrations.outWebhooks',
- defaultMessage: 'Outgoing Webhooks'
- },
- outDesc: {
- id: 'user.settings.integrations.outWebhooksDescription',
- defaultMessage: 'Manage your outgoing webhooks'
- },
cmdName: {
id: 'user.settings.integrations.commands',
defaultMessage: 'Slash Commands'
@@ -52,74 +34,10 @@ class UserSettingsIntegrationsTab extends React.Component {
this.props.updateSection(section);
}
render() {
- let incomingHooksSection;
- let outgoingHooksSection;
let commandHooksSection;
var inputs = [];
const {formatMessage} = this.props.intl;
- if (global.window.mm_config.EnableIncomingWebhooks === 'true') {
- if (this.props.activeSection === 'incoming-hooks') {
- inputs.push(
- <ManageIncomingHooks key='incoming-hook-ui'/>
- );
-
- incomingHooksSection = (
- <SettingItemMax
- title={formatMessage(holders.inName)}
- width='medium'
- inputs={inputs}
- updateSection={(e) => {
- this.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- incomingHooksSection = (
- <SettingItemMin
- title={formatMessage(holders.inName)}
- width='medium'
- describe={formatMessage(holders.inDesc)}
- updateSection={() => {
- this.updateSection('incoming-hooks');
- }}
- />
- );
- }
- }
-
- if (global.window.mm_config.EnableOutgoingWebhooks === 'true') {
- if (this.props.activeSection === 'outgoing-hooks') {
- inputs.push(
- <ManageOutgoingHooks key='outgoing-hook-ui'/>
- );
-
- outgoingHooksSection = (
- <SettingItemMax
- title={formatMessage(holders.outName)}
- width='medium'
- inputs={inputs}
- updateSection={(e) => {
- this.updateSection('');
- e.preventDefault();
- }}
- />
- );
- } else {
- outgoingHooksSection = (
- <SettingItemMin
- title={formatMessage(holders.outName)}
- width='medium'
- describe={formatMessage(holders.outDesc)}
- updateSection={() => {
- this.updateSection('outgoing-hooks');
- }}
- />
- );
- }
- }
-
if (global.window.mm_config.EnableCommands === 'true') {
if (this.props.activeSection === 'command-hooks') {
inputs.push(
@@ -187,10 +105,6 @@ class UserSettingsIntegrationsTab extends React.Component {
/>
</h3>
<div className='divider-dark first'/>
- {incomingHooksSection}
- <div className='divider-light'/>
- {outgoingHooksSection}
- <div className='divider-dark'/>
{commandHooksSection}
<div className='divider-dark'/>
</div>
@@ -209,4 +123,4 @@ UserSettingsIntegrationsTab.propTypes = {
collapseModal: React.PropTypes.func.isRequired
};
-export default injectIntl(UserSettingsIntegrationsTab); \ No newline at end of file
+export default injectIntl(UserSettingsIntegrationsTab);
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 11dfdf1ed..40e486434 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -22,6 +22,28 @@
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android Native App",
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
+ "add_incoming_webhook.cancel": "Cancel",
+ "add_incoming_webhook.channel": "Channel",
+ "add_incoming_webhook.channelRequired": "A valid channel is required",
+ "add_incoming_webhook.description": "Description",
+ "add_incoming_webhook.header": "Add Incoming Webhook",
+ "add_incoming_webhook.name": "Name",
+ "add_incoming_webhook.save": "Save",
+ "add_integration.header": "Add Integration",
+ "add_integration.incomingWebhook.title": "Incoming Webhook",
+ "add_integration.incomingWebhook.description": "Create webhook URLs for use in external integrations.",
+ "add_integration.outgoingWebhook.title": "Outgoing Webhook",
+ "add_integration.outgoingWebhook.description": "Create webhooks to send new message events to an external integration.",
+ "add_outgoing_webhook.callbackUrls": "Callback URLs (One Per Line)",
+ "add_outgoing_webhook.callbackUrlsRequired": "One or more callback URLs are required",
+ "add_outgoing_webhook.cancel": "Cancel",
+ "add_outgoing_webhook.channel": "Channel",
+ "add_outgoing_webhook.description": "Description",
+ "add_outgoing_webhook.header": "Add Outgoing Webhook",
+ "add_outgoing_webhook.name": "Name",
+ "add_outgoing_webhook.save": "Save",
+ "add_outgoing_webhook.triggerWOrds": "Trigger Words (One Per Line)",
+ "add_outgoing_webhook.triggerWordsOrChannelRequired": "A valid channel or a list of trigger words is required",
"admin.audits.reload": "Reload",
"admin.audits.title": "User Activity",
"admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.",
@@ -550,6 +572,12 @@
"authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.",
"authorize.deny": "Deny",
"authorize.title": "An application would like to connect to your {teamName} account",
+ "backstage_navbar.backToMattermost": "Back to {siteName}",
+ "backstage_sidebar.integrations": "Integrations",
+ "backstage_sidebar.integrations.installed": "Installed Integrations",
+ "backstage_sidebar.integrations.add": "Add Integration",
+ "backstage_sidebar.integrations.add.incomingWebhook": "Incoming Webhook",
+ "backstage_sidebar.integrations.add.outgoingWebhook": "Outgoing Webhook",
"center_panel.recent": "Click here to jump to recent messages. ",
"chanel_header.addMembers": "Add Members",
"change_url.close": "Close",
@@ -626,6 +654,7 @@
"channel_notifications.preferences": "Notification Preferences for ",
"channel_notifications.sendDesktop": "Send desktop notifications",
"channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.",
+ "channel_select.placeholder": "--- Select a channel ---",
"choose_auth_page.emailCreate": "Create new team with email address",
"choose_auth_page.find": "Find my teams",
"choose_auth_page.gitlabCreate": "Create new team with GitLab Account",
@@ -768,6 +797,16 @@
"get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it's regenerated in Team Settings by a Team Admin.",
"get_team_invite_link_modal.helpDisabled": "User creation has been disabled for your team. Please ask your team administrator for details.",
"get_team_invite_link_modal.title": "Team Invite Link",
+ "installed_integrations.add": "Add Integration",
+ "installed_integrations.allFilter": "All",
+ "installed_integrations.delete": "Delete",
+ "installed_integrations.header": "Installed Integrations",
+ "installed_integrations.incomingWebhooksFilter": "Incoming Webhooks ({count})",
+ "installed_integrations.incomingWebhookType": "(Incoming Webhook)",
+ "installed_integrations.outgoingWebhooksFilter": "Outgoing Webhooks ({count})",
+ "installed_integrations.outgoingWebhookType": "(Outgoing Webhook)",
+ "installed_integrations.regenToken": "Regen Token",
+ "installed_integrations.search": "Search Integrations",
"intro_messages.DM": "This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.",
"intro_messages.anyMember": " Any member can join and read this channel.",
"intro_messages.beginning": "Beginning of {name}",
@@ -873,6 +912,7 @@
"navbar_dropdown.console": "System Console",
"navbar_dropdown.create": "Create a New Team",
"navbar_dropdown.help": "Help",
+ "navbar_dropdown.integrations": "Integrations",
"navbar_dropdown.inviteMember": "Invite New Member",
"navbar_dropdown.logout": "Logout",
"navbar_dropdown.manageMembers": "Manage Members",
@@ -1274,27 +1314,6 @@
"user.settings.general.usernameRestrictions": "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.",
"user.settings.general.validEmail": "Please enter a valid email address",
"user.settings.general.validImage": "Only JPG or PNG images may be used for profile pictures",
- "user.settings.hooks_in.add": "Add",
- "user.settings.hooks_in.addTitle": "Add a new incoming webhook",
- "user.settings.hooks_in.channel": "Channel: ",
- "user.settings.hooks_in.description": "Create webhook URLs for use in external integrations. Please see <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">incoming webhooks documentation</a> to learn more. View all incoming webhooks configured on this team below.",
- "user.settings.hooks_in.existing": "Existing incoming webhooks",
- "user.settings.hooks_in.none": "None",
- "user.settings.hooks_out.add": "Add",
- "user.settings.hooks_out.addDescription": "Create webhooks to send new message events to an external integration. Please see <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">outgoing webhooks documentation</a> to learn more. View all outgoing webhooks configured on this team below.",
- "user.settings.hooks_out.addTitle": "Add a new outgoing webhook",
- "user.settings.hooks_out.callback": "Callback URLs: ",
- "user.settings.hooks_out.callbackDesc": "New line separated URLs that will receive the HTTP POST event",
- "user.settings.hooks_out.callbackHolder": "Each URL must start with http:// or https://",
- "user.settings.hooks_out.channel": "Channel: ",
- "user.settings.hooks_out.comma": "Comma separated words to trigger on",
- "user.settings.hooks_out.existing": "Existing outgoing webhooks",
- "user.settings.hooks_out.none": "None",
- "user.settings.hooks_out.only": "Only public channels can be used",
- "user.settings.hooks_out.optional": "Optional if channel selected",
- "user.settings.hooks_out.regen": "Regen Token",
- "user.settings.hooks_out.select": "--- Select a channel ---",
- "user.settings.hooks_out.trigger": "Trigger Words: ",
"user.settings.import_theme.cancel": "Cancel",
"user.settings.import_theme.importBody": "To import a theme, go to a Slack team and look for “Preferences -> Sidebar Theme”. Open the custom theme option, copy the theme color values and paste them here:",
"user.settings.import_theme.importHeader": "Import Slack Theme",
@@ -1302,10 +1321,6 @@
"user.settings.import_theme.submitError": "Invalid format, please try copying and pasting in again.",
"user.settings.integrations.commands": "Slash Commands",
"user.settings.integrations.commandsDescription": "Manage your slash commands",
- "user.settings.integrations.incomingWebhooks": "Incoming Webhooks",
- "user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks",
- "user.settings.integrations.outWebhooks": "Outgoing Webhooks",
- "user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks",
"user.settings.integrations.title": "Integration Settings",
"user.settings.languages.change": "Change interface language",
"user.settings.modal.advanced": "Advanced",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index c2b56e1cc..607e46483 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -1274,27 +1274,6 @@
"user.settings.general.usernameRestrictions": "El nombre de usuario debe empezar con una letra, y contener entre {min} a {max} caracteres en minúscula con números, lettras, y los símbolos '.', '-' y '_'.",
"user.settings.general.validEmail": "Por favor ingresa una dirección de correo electrónico válida",
"user.settings.general.validImage": "Sólo pueden ser utilizadas imágenes JPG o PNG en el perfil",
- "user.settings.hooks_in.add": "Agregar",
- "user.settings.hooks_in.addTitle": "Agregar un nuevo webhook de entrada",
- "user.settings.hooks_in.channel": "Canal: ",
- "user.settings.hooks_in.description": "Crea URLs para webhooks a utilizar con integraciones externas. Revisa la <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentación de webhooks de entrada</a> para conocer más. Ver todos los webhooks de entrada configurados para este equipo en la parte de abajo.",
- "user.settings.hooks_in.existing": "Webhooks de entrada existentes",
- "user.settings.hooks_in.none": "Ninguno",
- "user.settings.hooks_out.add": "Agregar",
- "user.settings.hooks_out.addDescription": "Crea webhooks para enviar mensajes a ingraciones externas. Revisa la <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentación de webhooks de saldida</a> para conocer más. Ver todos los webhooks de salida configurados para este equipo en la parte de abajo.",
- "user.settings.hooks_out.addTitle": "Agregar un nuevo webhook de salida",
- "user.settings.hooks_out.callback": "Callback URLs:",
- "user.settings.hooks_out.callbackDesc": "Separa por una nueva linea cada URL donde quieres recibir el evento de HTTP POST",
- "user.settings.hooks_out.callbackHolder": "Cada URL debe comenzar con http:// o https://",
- "user.settings.hooks_out.channel": "Canal: ",
- "user.settings.hooks_out.comma": "Escribe las palabras de activación que ejecutan el evento separadas por coma",
- "user.settings.hooks_out.existing": "Webhooks de salida existentes",
- "user.settings.hooks_out.none": "Ninguno",
- "user.settings.hooks_out.only": "Sólo se pueden utilizar Canales",
- "user.settings.hooks_out.optional": "Opcional si se selecciona un canal",
- "user.settings.hooks_out.regen": "Regenerar Token",
- "user.settings.hooks_out.select": "--- Selecciona un canal ---",
- "user.settings.hooks_out.trigger": "Palabras de activación: ",
"user.settings.import_theme.cancel": "Cancelar",
"user.settings.import_theme.importBody": "Para importar un tema, anda al equipo Slack y busca en [Preferences -> Sidebar Theme]. Abre las opciones del tema, copia los valores de color del tema y pégalo aquí:",
"user.settings.import_theme.importHeader": "Importar Tema de Slack",
@@ -1302,10 +1281,6 @@
"user.settings.import_theme.submitError": "Formato inválido, por favor intenta copiando y pegando nuevamente.",
"user.settings.integrations.commands": "Comandos de Barra",
"user.settings.integrations.commandsDescription": "Administra tus comandos de barra",
- "user.settings.integrations.incomingWebhooks": "Webhooks de entrada",
- "user.settings.integrations.incomingWebhooksDescription": "Administra tus webhooks de entrada",
- "user.settings.integrations.outWebhooks": "Webhooks de salida",
- "user.settings.integrations.outWebhooksDescription": "Administra tus webhooks de salida",
"user.settings.integrations.title": "Configuraciones de Integración",
"user.settings.languages.change": "Cambia el idioma con el que se muestra la intefaz de usuario",
"user.settings.modal.advanced": "Avanzada",
diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json
index 3270b8847..9d68c54ec 100644
--- a/webapp/i18n/fr.json
+++ b/webapp/i18n/fr.json
@@ -1274,27 +1274,6 @@
"user.settings.general.usernameRestrictions": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de chiffres, lettres minuscules et des symboles '.', '-' et '_'",
"user.settings.general.validEmail": "Veuillez entrer une adresse électronique valide",
"user.settings.general.validImage": "Seules les images JPG ou PNG sont autorisées pour les photos de profil",
- "user.settings.hooks_in.add": "Ajouter",
- "user.settings.hooks_in.addTitle": "Ajouter un webhook entrant",
- "user.settings.hooks_in.channel": "Canal\u00a0: ",
- "user.settings.hooks_in.description": "Crééez des URLs de webhooks pour des intégrations externes. Veuillez consulter <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">la documentation sur les webhooks entrants</a> pour en savoir plus. Examinez tous les webhooks entrants configurés pour cette équipe ci-dessous.",
- "user.settings.hooks_in.existing": "Webhooks entrants",
- "user.settings.hooks_in.none": "Aucun",
- "user.settings.hooks_out.add": "Ajouter",
- "user.settings.hooks_out.addDescription": "Crééez des webhooks pour envoyer les évènements de nouveaux messages vers des intégrations externes. Veuillez consulter <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">la documentation sur les webhooks sortants</a> pour en savoir plus. Examinez tous les webhooks sortants configurés pour cette équipe ci-dessous.",
- "user.settings.hooks_out.addTitle": "Ajouter un webhook sortant",
- "user.settings.hooks_out.callback": "URLs de callback :",
- "user.settings.hooks_out.callbackDesc": "URLs séparés par un saut de ligne qui recevront l'événement HTTP POST",
- "user.settings.hooks_out.callbackHolder": "Chaque URL doit commencer par http:// ou https://",
- "user.settings.hooks_out.channel": "Canal\u00a0: ",
- "user.settings.hooks_out.comma": "Liste de mots déclencheurs séparés par une virgule",
- "user.settings.hooks_out.existing": "Webhooks sortants",
- "user.settings.hooks_out.none": "Aucun",
- "user.settings.hooks_out.only": "Seuls les canaux publics peuvent être utilisés",
- "user.settings.hooks_out.optional": "Facultatif si un canal est sélectionné",
- "user.settings.hooks_out.regen": "Réinitialiser le jeton",
- "user.settings.hooks_out.select": "--- Choisissez un canal ---",
- "user.settings.hooks_out.trigger": "Mots de déclenchement :",
"user.settings.import_theme.cancel": "Annuler",
"user.settings.import_theme.importBody": "Pour importer un thème, rendez-vous sur une Slack team et cliquez sur \"Preferences -> Sidebar Theme\". Ouvrez la fenêtre de personnalisation, copiez les couleurs du thèmes et collez-les ici :",
"user.settings.import_theme.importHeader": "Importer un thème Slack",
@@ -1302,10 +1281,6 @@
"user.settings.import_theme.submitError": "Format invalide, veuillez réessayer de copier-coller.",
"user.settings.integrations.commands": "Commandes slash",
"user.settings.integrations.commandsDescription": "Gérez vos commandes slash",
- "user.settings.integrations.incomingWebhooks": "Webhooks entrants",
- "user.settings.integrations.incomingWebhooksDescription": "Gérer les webhooks entrants",
- "user.settings.integrations.outWebhooks": "Webhooks sortants",
- "user.settings.integrations.outWebhooksDescription": "Gérer les webhooks sortants",
"user.settings.integrations.title": "Paramètres d'intégration",
"user.settings.languages.change": "Changer la langue de l'interface",
"user.settings.modal.advanced": "Options avancées",
@@ -1374,4 +1349,4 @@
"web.footer.terms": "Termes",
"web.header.back": "Précédent",
"web.root.singup_info": "Toute la communication de votre équipe à un endroit, accessible de partout"
-} \ No newline at end of file
+}
diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json
index b758dd0b2..c9ab089fb 100644
--- a/webapp/i18n/pt.json
+++ b/webapp/i18n/pt.json
@@ -1273,27 +1273,6 @@
"user.settings.general.usernameRestrictions": "O nome de usuário precisa começar com uma letra, e conter entre {min} e {max} caracteres minúsculos contendo números, letras, e os símbolos '.', '-' e '_'.",
"user.settings.general.validEmail": "Por favor entre um endereço de e-mail válido",
"user.settings.general.validImage": "Somente imagens em JPG ou PNG podem ser usadas como imagem do perfil",
- "user.settings.hooks_in.add": "Adicionar",
- "user.settings.hooks_in.addTitle": "Adicionar um novo webhook entrada",
- "user.settings.hooks_in.channel": "Canal: ",
- "user.settings.hooks_in.description": "Criar URLs webhook para usar em integrações externas. Por favor veja <a href=\"http://docs.mattermost.com/developer/webhooks-incoming.html\" target=\"_blank\">documentação webhook entrada</a> para saber mais. Ver todos os webhooks de entrada configurados nesta equipe abaixo.",
- "user.settings.hooks_in.existing": "Webhooks de entrada existentes",
- "user.settings.hooks_in.none": "Nenhum",
- "user.settings.hooks_out.add": "Adicionar",
- "user.settings.hooks_out.addDescription": "Criar webhooks para enviar novos mensagens de eventos para uma integração externa. Por favor veja <a href=\"http://docs.mattermost.com/developer/webhooks-outgoing.html\" target=\"_blank\">documentação webhook saída</a> para saber mais. Ver todos os webhooks de saída desta equipe configurados abaixo.",
- "user.settings.hooks_out.addTitle": "Adicionar um novo webhook saída",
- "user.settings.hooks_out.callback": "Callback URLs: ",
- "user.settings.hooks_out.callbackDesc": "Nova linha separada de URLs que receberá o evento HTTP POST",
- "user.settings.hooks_out.callbackHolder": "Cada URL deve começar com http:// ou https://",
- "user.settings.hooks_out.channel": "Canal: ",
- "user.settings.hooks_out.comma": "Palavras separadas por virgula para gatilho em",
- "user.settings.hooks_out.existing": "Webhooks de saída existentes",
- "user.settings.hooks_out.none": "Nenhum",
- "user.settings.hooks_out.only": "Apenas canais públicos pode ser usado",
- "user.settings.hooks_out.optional": "Opcional se o canal selecionado",
- "user.settings.hooks_out.regen": "Re-Gerar Token",
- "user.settings.hooks_out.select": "--- Selecione um canal ---",
- "user.settings.hooks_out.trigger": "Palavras de Gatilho: ",
"user.settings.import_theme.cancel": "Cancelar",
"user.settings.import_theme.importBody": "Para importar um tema, vá para uma equipe no Slack e olhe para “Preferences -> Sidebar Theme”. Abra a opção de tema customizado, copie os valores das cores do tema e cole eles aqui:",
"user.settings.import_theme.importHeader": "Importar Tema Slack",
@@ -1301,10 +1280,6 @@
"user.settings.import_theme.submitError": "Formato inválido, por favor tente copiar e colar novamente.",
"user.settings.integrations.commands": "Comandos Slash",
"user.settings.integrations.commandsDescription": "Gerenciar seus comandos slash",
- "user.settings.integrations.incomingWebhooks": "Webhooks Entrada",
- "user.settings.integrations.incomingWebhooksDescription": "Gerencie seus webhooks entrada",
- "user.settings.integrations.outWebhooks": "Webhooks Saída",
- "user.settings.integrations.outWebhooksDescription": "Gerencie seus webhooks saída",
"user.settings.integrations.title": "Configuração de Integração",
"user.settings.languages.change": "Alterar o idioma da interface",
"user.settings.modal.advanced": "Avançado",
@@ -1373,4 +1348,4 @@
"web.footer.terms": "Termos",
"web.header.back": "Voltar",
"web.root.singup_info": "Toda comunicação em um só lugar, pesquisável e acessível em qualquer lugar"
-} \ No newline at end of file
+}
diff --git a/webapp/root.jsx b/webapp/root.jsx
index ce59a95c9..fca368bdb 100644
--- a/webapp/root.jsx
+++ b/webapp/root.jsx
@@ -36,6 +36,12 @@ import ShouldVerifyEmail from 'components/should_verify_email.jsx';
import DoVerifyEmail from 'components/do_verify_email.jsx';
import AdminConsole from 'components/admin_console/admin_controller.jsx';
import TutorialView from 'components/tutorial/tutorial_view.jsx';
+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 AddOutgoingWebhook from 'components/backstage/add_outgoing_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';
@@ -241,6 +247,42 @@ function renderRootComponent() {
path=':team/logout'
onEnter={onLoggedOut}
/>
+ <Route path='settings/integrations'>
+ <IndexRedirect to='installed'/>
+ <Route
+ path='installed'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: InstalledIntegrations
+ }}
+ />
+ <Route path='add'>
+ <IndexRoute
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: AddIntegration
+ }}
+ />
+ <Route
+ path='incoming_webhook'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: AddIncomingWebhook
+ }}
+ />
+ <Route
+ path='outgoing_webhook'
+ components={{
+ navbar: BackstageNavbar,
+ sidebar: BackstageSidebar,
+ center: AddOutgoingWebhook
+ }}
+ />
+ </Route>
+ </Route>
<Route
path='admin_console'
component={AdminConsole}
diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss
new file mode 100644
index 000000000..70bab99cb
--- /dev/null
+++ b/webapp/sass/routes/_backstage.scss
@@ -0,0 +1,212 @@
+.backstage {
+ background-color: #f2f2f2;
+ height: 100%;
+ padding-left: 260px;
+ padding-top: 45px;
+}
+
+.backstage__navbar {
+ background: white;
+ border-bottom: lightgray 1px solid;
+ margin: 0 -15px;
+ padding: 10px;
+ z-index: 10;
+}
+
+.backstage__navbar__back {
+ color: black;
+
+ .fa {
+ font-weight: bold;
+ margin-right: 5px;
+ }
+}
+
+.backstage__sidebar {
+ position: absolute;
+ left: 0;
+ width: 260px;
+ height: 100%;
+ background-color: #f2f2f2;
+ padding-bottom: 20px;
+ padding-left: 20px;
+ padding-right: 20px;
+ padding-top: 45px;
+ z-index: 5;
+
+ ul {
+ padding: 0px;
+ list-style: none;
+ }
+}
+
+.backstage__sidebar__category {
+ border: lightgray 1px solid;
+
+ .category-title {
+ color: gray;
+ display: block;
+ padding: 5px 10px;
+ position: relative;
+ }
+
+ .category-title--active {
+ color: black;
+ }
+
+ .category-title__text {
+ position: absolute;
+ left: 2em;
+ }
+
+ .sections {
+ background: white;
+ border-top: lightgray 1px solid;
+ }
+
+ .section-title {
+ display: block;
+ padding-left: 2em;
+ }
+
+ .subsection {
+ }
+
+ .subsection-title {
+ display: block;
+ padding-left: 3em;
+ }
+
+ .section-title--active, .subsection-title--active {
+ background-color:#2388d6;
+ color: white;
+ }
+}
+
+.backstage__sidebar__category + .backstage__sidebar__category {
+ border-top-width: 0px;
+}
+
+.installed-integrations {
+ height: 100%;
+ max-width: 958px;
+}
+
+.backstage__header {
+ margin-bottom: 20px;
+ width: 100%;
+
+ .text {
+ display: inline;
+ }
+
+ .add-integrations-link {
+ float: right;
+ }
+}
+
+.installed-integrations__filters {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 8px;
+ width: 100%;
+
+ .type-filters {
+ flex-grow: 1;
+ flex-shrink: 0;
+
+ .type-filter {
+ &--selected {
+ color: black;
+ cursor: default;
+ font-weight: bold;
+ }
+ }
+
+ .divider {
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+ }
+
+ .filter-box {
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+}
+
+.installed-integrations__list {
+ background-color: white;
+ border: lightgray 1px solid;
+ padding-bottom: 30px;
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-top: 10px;
+}
+
+.installed-integrations__item {
+ border-bottom: lightgray 1px solid;
+ display: flex;
+ padding: 20px;
+
+ .details {
+ flex-grow: 1;
+ flex-shrink: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .details-row + .details-row {
+ margin-top: 15px;
+ }
+
+ .name {
+ font-weight: bold;
+ margin-bottom: 1em;
+ }
+
+ .type {
+ margin-left: 6px;
+ }
+
+ .description {
+ color: gray;
+ margin-bottom: 1em;
+ }
+ }
+
+ .actions {
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding-left: 20px;
+ }
+}
+
+.add-integration-option {
+ background-color: white;
+ border: lightgray 1px solid;
+ display: inline-block;
+ height: 210px;
+ margin-right: 30px;
+ padding: 20px;
+ text-align: center;
+ width: 250px;
+
+ &:hover {
+ color: default;
+ text-decoration: none;
+ }
+
+ &__image {
+ width: 80px;
+ height: 80px;
+ }
+
+ &__title {
+ color: black;
+ margin-bottom: 10px;
+ }
+
+ &__description {
+ color: gray;
+ }
+}
diff --git a/webapp/sass/routes/_module.scss b/webapp/sass/routes/_module.scss
index 48c1af1d9..b76dd147f 100644
--- a/webapp/sass/routes/_module.scss
+++ b/webapp/sass/routes/_module.scss
@@ -2,6 +2,7 @@
@import 'access-history';
@import 'activity-log';
@import 'admin-console';
+@import 'backstage';
@import 'docs';
@import 'error-page';
@import 'loading';
diff --git a/webapp/stores/file_store.jsx b/webapp/stores/file_store.jsx
index 2628685cc..6473e0474 100644
--- a/webapp/stores/file_store.jsx
+++ b/webapp/stores/file_store.jsx
@@ -13,11 +13,6 @@ class FileStore extends EventEmitter {
constructor() {
super();
- this.addChangeListener = this.addChangeListener.bind(this);
- this.removeChangeListener = this.removeChangeListener.bind(this);
- this.emitChange = this.emitChange.bind(this);
-
- this.handleEventPayload = this.handleEventPayload.bind(this);
this.dispatchToken = AppDispatcher.register(this.handleEventPayload);
this.fileInfo = new Map();
diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx
new file mode 100644
index 000000000..abd7e3558
--- /dev/null
+++ b/webapp/stores/integration_store.jsx
@@ -0,0 +1,134 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
+import Constants from 'utils/constants.jsx';
+import EventEmitter from 'events';
+
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'changed';
+
+class IntegrationStore extends EventEmitter {
+ constructor() {
+ super();
+
+ this.dispatchToken = AppDispatcher.register(this.handleEventPayload.bind(this));
+
+ this.incomingWebhooks = [];
+ this.receivedIncomingWebhooks = false;
+
+ this.outgoingWebhooks = [];
+ this.receivedOutgoingWebhooks = false;
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ hasReceivedIncomingWebhooks() {
+ return this.receivedIncomingWebhooks;
+ }
+
+ getIncomingWebhooks() {
+ return this.incomingWebhooks;
+ }
+
+ setIncomingWebhooks(incomingWebhooks) {
+ this.incomingWebhooks = incomingWebhooks;
+ this.receivedIncomingWebhooks = true;
+ }
+
+ addIncomingWebhook(incomingWebhook) {
+ this.incomingWebhooks.push(incomingWebhook);
+ }
+
+ removeIncomingWebhook(id) {
+ for (let i = 0; i < this.incomingWebhooks.length; i++) {
+ if (this.incomingWebhooks[i].id === id) {
+ this.incomingWebhooks.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ hasReceivedOutgoingWebhooks() {
+ return this.receivedIncomingWebhooks;
+ }
+
+ getOutgoingWebhooks() {
+ return this.outgoingWebhooks;
+ }
+
+ setOutgoingWebhooks(outgoingWebhooks) {
+ this.outgoingWebhooks = outgoingWebhooks;
+ this.receivedOutgoingWebhooks = true;
+ }
+
+ addOutgoingWebhook(outgoingWebhook) {
+ this.outgoingWebhooks.push(outgoingWebhook);
+ }
+
+ updateOutgoingWebhook(outgoingWebhook) {
+ for (let i = 0; i < this.outgoingWebhooks.length; i++) {
+ if (this.outgoingWebhooks[i].id === outgoingWebhook.id) {
+ this.outgoingWebhooks[i] = outgoingWebhook;
+ break;
+ }
+ }
+ }
+
+ removeOutgoingWebhook(id) {
+ for (let i = 0; i < this.outgoingWebhooks.length; i++) {
+ if (this.outgoingWebhooks[i].id === id) {
+ this.outgoingWebhooks.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ handleEventPayload(payload) {
+ const action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECEIVED_INCOMING_WEBHOOKS:
+ this.setIncomingWebhooks(action.incomingWebhooks);
+ this.emitChange();
+ break;
+ case ActionTypes.RECEIVED_INCOMING_WEBHOOK:
+ this.addIncomingWebhook(action.incomingWebhook);
+ this.emitChange();
+ break;
+ case ActionTypes.REMOVED_INCOMING_WEBHOOK:
+ this.removeIncomingWebhook(action.id);
+ 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;
+ case ActionTypes.UPDATED_OUTGOING_WEBHOOK:
+ this.updateOutgoingWebhook(action.outgoingWebhook);
+ this.emitChange();
+ break;
+ case ActionTypes.REMOVED_OUTGOING_WEBHOOK:
+ this.removeOutgoingWebhook(action.id);
+ this.emitChange();
+ break;
+ }
+ }
+}
+
+export default new IntegrationStore();
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 6140fd9e0..cc19baa7e 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -1121,3 +1121,140 @@ export function getRecentAndNewUsersAnalytics(teamId) {
}
);
}
+
+export function listIncomingHooks() {
+ if (isCallInProgress('listIncomingHooks')) {
+ return;
+ }
+
+ callTracker.listIncomingHooks = utils.getTimestamp();
+
+ client.listIncomingHooks(
+ (data) => {
+ callTracker.listIncomingHooks = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_INCOMING_WEBHOOKS,
+ incomingWebhooks: data
+ });
+ },
+ (err) => {
+ callTracker.listIncomingHooks = 0;
+ dispatchError(err, 'getIncomingHooks');
+ }
+ );
+}
+
+export function listOutgoingHooks() {
+ if (isCallInProgress('listOutgoingHooks')) {
+ return;
+ }
+
+ callTracker.listOutgoingHooks = utils.getTimestamp();
+
+ client.listOutgoingHooks(
+ (data) => {
+ callTracker.listOutgoingHooks = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_OUTGOING_WEBHOOKS,
+ outgoingWebhooks: data
+ });
+ },
+ (err) => {
+ callTracker.listOutgoingHooks = 0;
+ dispatchError(err, 'getOutgoingHooks');
+ }
+ );
+}
+
+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);
+ }
+ }
+ );
+}
+
+export function deleteIncomingHook(id) {
+ client.deleteIncomingHook(
+ {id},
+ () => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.REMOVED_INCOMING_WEBHOOK,
+ id
+ });
+ },
+ (err) => {
+ dispatchError(err, 'deleteIncomingHook');
+ }
+ );
+}
+
+export function deleteOutgoingHook(id) {
+ client.deleteOutgoingHook(
+ {id},
+ () => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.REMOVED_OUTGOING_WEBHOOK,
+ id
+ });
+ },
+ (err) => {
+ dispatchError(err, 'deleteOutgoingHook');
+ }
+ );
+}
+
+export function regenOutgoingHookToken(id) {
+ client.regenOutgoingHookToken(
+ {id},
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.UPDATED_OUTGOING_WEBHOOK,
+ outgoingWebhook: data
+ });
+ },
+ (err) => {
+ dispatchError(err, 'regenOutgoingHookToken');
+ }
+ );
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 642ff5fe3..f0e8c260e 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -70,6 +70,14 @@ export default {
RECEIVED_FILE_INFO: null,
RECEIVED_ANALYTICS: null,
+ RECEIVED_INCOMING_WEBHOOKS: null,
+ RECEIVED_INCOMING_WEBHOOK: null,
+ REMOVED_INCOMING_WEBHOOK: null,
+ RECEIVED_OUTGOING_WEBHOOKS: null,
+ RECEIVED_OUTGOING_WEBHOOK: null,
+ UPDATED_OUTGOING_WEBHOOK: null,
+ REMOVED_OUTGOING_WEBHOOK: null,
+
RECEIVED_MSG: null,
RECEIVED_MY_TEAM: null,
diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js
index a049898d6..478c5de81 100644
--- a/webapp/webpack.config.js
+++ b/webapp/webpack.config.js
@@ -56,7 +56,7 @@ var config = {
loaders: ['style', 'css']
},
{
- test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3)$/,
+ test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/,
loader: 'file',
query: {
name: 'files/[hash].[ext]'