diff options
-rw-r--r-- | webapp/components/backstage/backstage_category.jsx | 68 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_navbar.jsx | 62 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_section.jsx | 122 | ||||
-rw-r--r-- | webapp/components/backstage/backstage_sidebar.jsx | 113 | ||||
-rw-r--r-- | webapp/components/backstage/installed_integrations.jsx | 304 | ||||
-rw-r--r-- | webapp/components/logged_in.jsx | 10 | ||||
-rw-r--r-- | webapp/root.jsx | 48 | ||||
-rw-r--r-- | webapp/sass/routes/_backstage.scss | 182 | ||||
-rw-r--r-- | webapp/sass/routes/_module.scss | 1 | ||||
-rw-r--r-- | webapp/stores/file_store.jsx | 5 | ||||
-rw-r--r-- | webapp/stores/integration_store.jsx | 80 | ||||
-rw-r--r-- | webapp/utils/async_client.jsx | 46 | ||||
-rw-r--r-- | webapp/utils/constants.jsx | 2 | ||||
-rw-r--r-- | webapp/utils/utils.jsx | 10 |
14 files changed, 1045 insertions, 8 deletions
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..8ba8669c5 --- /dev/null +++ b/webapp/components/backstage/backstage_navbar.jsx @@ -0,0 +1,62 @@ +// 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.back_to_mattermost' + defaultMessage='Back to {siteName}' + values={{ + siteName: global.window.mm_config.SiteName + }} + /> + </span> + </Link> + <span style={{float: 'right'}}>{'TODO: Switch Teams'}</span> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx new file mode 100644 index 000000000..41ce766bd --- /dev/null +++ b/webapp/components/backstage/backstage_section.jsx @@ -0,0 +1,122 @@ +// 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 + }; + } + + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + + this.state = { + expanded: true + }; + } + + getLink() { + return this.props.parentLink + '/' + this.props.name; + } + + isActive() { + const link = this.getLink(); + + return this.context.router.isActive(link); + } + + handleClick(e) { + if (this.isActive()) { + // we're already on this page so just toggle the link + e.preventDefault(); + + this.setState({ + expanded: !this.state.expanded + }); + } + + // otherwise, just follow the link + } + + render() { + const {title, subsection, children} = this.props; + + const link = this.getLink(); + const active = this.isActive(); + + // act like docs.mattermost.com and only expand if this link is active + const expanded = active && this.state.expanded; + + let toggle = null; + if (children.length > 0) { + if (expanded) { + toggle = <i className='fa fa-minus-square-o'/>; + } else { + toggle = <i className='fa fa-plus-square-o'/>; + } + } + + let clonedChildren = null; + if (children.length > 0 && expanded) { + 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`} + onClick={this.handleClick} + to={link} + > + {toggle} + <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..672005333 --- /dev/null +++ b/webapp/components/backstage/backstage_sidebar.jsx @@ -0,0 +1,113 @@ +// 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 BackstageCategory from './backstage_category.jsx'; +import BackstageSection from './backstage_section.jsx'; +import {FormattedMessage} from 'react-intl'; + +export default class BackstageSidebar 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() { + const team = TeamStore.getCurrent(); + + if (!team) { + return null; + } + + return ( + <div className='backstage__sidebar'> + <ul> + <BackstageCategory + name='team_settings' + parentLink={`/${team.name}`} + icon='fa-users' + title={ + <FormattedMessage + id='backstage.team_settings' + defaultMessage='Team Settings' + /> + } + /> + <BackstageCategory + name='integrations' + parentLink={`/${team.name}`} + icon='fa-link' + title={ + <FormattedMessage + id='backstage.integrations' + defaultMessage='Integrations' + /> + } + > + <BackstageSection + name='installed' + title={( + <FormattedMessage + id='backstage.integrations.installed' + defaultMessage='Installed Integrations' + /> + )} + /> + <BackstageSection + name='add' + title={( + <FormattedMessage + id='backstage.integrations.add' + defaultMessage='Add Integration' + /> + )} + collapsible={true} + > + <BackstageSection + name='incoming_webhook' + title={( + <FormattedMessage + id='backstage.integrations.add.incomingWebhook' + defaultMessage='Incoming Webhook' + /> + )} + /> + <BackstageSection + name='outgoing_webhook' + title={( + <FormattedMessage + id='backstage.integrations.add.outgoingWebhook' + defaultMessage='Outgoing Webhook' + /> + )} + /> + </BackstageSection> + </BackstageCategory> + </ul> + </div> + ); + } +} + diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx new file mode 100644 index 000000000..cfb68c660 --- /dev/null +++ b/webapp/components/backstage/installed_integrations.jsx @@ -0,0 +1,304 @@ +// 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 {Link} from 'react-router'; + +export default class InstalledIntegrations extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.setFilter = this.setFilter.bind(this); + + this.state = { + incomingWebhooks: [], + outgoingWebhooks: [], + filter: '' + }; + } + + componentWillMount() { + IntegrationStore.addChangeListener(this.handleChange); + + 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.handleChange); + } + + handleChange() { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks(), + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } + + setFilter(e, filter) { + e.preventDefault(); + + this.setState({ + filter + }); + } + + renderTypeFilters(incomingWebhooks, outgoingWebhooks) { + const fields = []; + + if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) { + let filterClassName = 'type-filter'; + if (this.state.filter === '') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + <a + key='allFilter' + className={filterClassName} + href='#' + onClick={(e) => this.setFilter(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.filter === 'incomingWebhooks') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + <a + key='incomingWebhooksFilter' + className={filterClassName} + href='#' + onClick={(e) => this.setFilter(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.filter === 'outgoingWebhooks') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + <a + key='outgoingWebhooksFilter' + className={filterClassName} + href='#' + onClick={(e) => this.setFilter(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 integrations = []; + if (!this.state.filter || this.state.filter === 'incomingWebhooks') { + for (const incomingWebhook of incomingWebhooks) { + integrations.push( + <IncomingWebhook + key={incomingWebhook.id} + incomingWebhook={incomingWebhook} + /> + ); + } + } + + if (!this.state.filter || this.state.filter === 'outgoingWebhooks') { + for (const outgoingWebhook of outgoingWebhooks) { + integrations.push( + <OutgoingWebhook + key={outgoingWebhook.id} + outgoingWebhook={outgoingWebhook} + /> + ); + } + } + + return ( + <div className='backstage row'> + <div className='installed-integrations'> + <div className='installed-integrations__header'> + <h1 className='text'> + <FormattedMessage + id='installed_integrations.header' + defaultMessage='Installed Integrations' + /> + </h1> + <Link + className='add-integrations-link' + to={'/yourteamhere/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')} + style={{flexGrow: 0, flexShrink: 0}} + /> + </div> + <div className='installed-integrations__list'> + {integrations} + </div> + </div> + </div> + ); + } +} + +function IncomingWebhook({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> + ); +} + +IncomingWebhook.propTypes = { + incomingWebhook: React.PropTypes.object.isRequired +}; + +function OutgoingWebhook({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} + </span> + </div> + </div> + </div> + ); +} + +OutgoingWebhook.propTypes = { + outgoingWebhook: React.PropTypes.object.isRequired +}; 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/root.jsx b/webapp/root.jsx index ce59a95c9..c10099967 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -36,6 +36,9 @@ 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 SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx'; import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx'; @@ -242,6 +245,51 @@ function renderRootComponent() { onEnter={onLoggedOut} /> <Route + path=':team/team_settings' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: null + }} + /> + <Route path=':team/integrations'> + <IndexRedirect to='installed'/> + <Route + path='installed' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: InstalledIntegrations + }} + /> + <Route path='add'> + <IndexRoute + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: null + }} + /> + <Route + path='incoming_webhook' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: null + }} + /> + <Route + path='outgoing_webhook' + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: null + }} + /> + </Route> + </Route> +>>>>>>> Added initial backstage components and InstalledIntegrations page + <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..4619e4cbc --- /dev/null +++ b/webapp/sass/routes/_backstage.scss @@ -0,0 +1,182 @@ +.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; +} + +.installed-integrations__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; + } +} 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..4e9212bcb --- /dev/null +++ b/webapp/stores/integration_store.jsx @@ -0,0 +1,80 @@ +// 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'; +import * as Utils from 'utils/utils.jsx'; + +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 = Utils.freezeArray(incomingWebhooks); + this.receivedIncomingWebhooks = true; + } + + hasReceivedOutgoingWebhooks() { + return this.receivedIncomingWebhooks; + } + + getOutgoingWebhooks() { + return this.outgoingWebhooks; + } + + setOutgoingWebhooks(outgoingWebhooks) { + this.outgoingWebhooks = Utils.freezeArray(outgoingWebhooks); + this.receivedOutgoingWebhooks = true; + } + + handleEventPayload(payload) { + const action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_INCOMING_WEBHOOKS: + this.setIncomingWebhooks(action.incomingWebhooks); + this.emitChange(); + break; + case ActionTypes.RECEIVED_OUTGOING_WEBHOOKS: + this.setOutgoingWebhooks(action.outgoingWebhooks); + this.emitChange(); + break; + } + } +} + +export default new IntegrationStore(); diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 6140fd9e0..2154fbe43 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1121,3 +1121,49 @@ 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'); + } + ); +} diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index bcd2fadb9..1f316369a 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -68,6 +68,8 @@ export default { RECEIVED_PREFERENCE: null, RECEIVED_PREFERENCES: null, RECEIVED_FILE_INFO: null, + RECEIVED_INCOMING_WEBHOOKS: null, + RECEIVED_OUTGOING_WEBHOOKS: null, RECEIVED_MSG: null, diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 83519a6ec..33a3d8b27 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -1398,3 +1398,13 @@ export function localizeMessage(id, defaultMessage) { return id; } + +export function freezeArray(arr) { + for (const obj of arr) { + Object.freeze(obj); + } + + Object.freeze(arr); + + return arr; +} |