From c417fdc152e953982d9c9af2c04ca2c04ced41b3 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 17 Mar 2016 10:30:49 -0400 Subject: Added initial backstage components and InstalledIntegrations page --- webapp/components/backstage/backstage_category.jsx | 68 +++++ webapp/components/backstage/backstage_navbar.jsx | 62 +++++ webapp/components/backstage/backstage_section.jsx | 122 +++++++++ webapp/components/backstage/backstage_sidebar.jsx | 113 ++++++++ .../backstage/installed_integrations.jsx | 304 +++++++++++++++++++++ webapp/components/logged_in.jsx | 10 +- webapp/root.jsx | 48 ++++ webapp/sass/routes/_backstage.scss | 182 ++++++++++++ webapp/sass/routes/_module.scss | 1 + webapp/stores/file_store.jsx | 5 - webapp/stores/integration_store.jsx | 80 ++++++ webapp/utils/async_client.jsx | 46 ++++ webapp/utils/constants.jsx | 2 + webapp/utils/utils.jsx | 10 + 14 files changed, 1045 insertions(+), 8 deletions(-) create mode 100644 webapp/components/backstage/backstage_category.jsx create mode 100644 webapp/components/backstage/backstage_navbar.jsx create mode 100644 webapp/components/backstage/backstage_section.jsx create mode 100644 webapp/components/backstage/backstage_sidebar.jsx create mode 100644 webapp/components/backstage/installed_integrations.jsx create mode 100644 webapp/sass/routes/_backstage.scss create mode 100644 webapp/stores/integration_store.jsx (limited to 'webapp') 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 = ( + + ); + } + + return ( +
  • + + + + {title} + + + {clonedChildren} +
  • + ); + } +} 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 ( +
    + + + + + + + {'TODO: Switch Teams'} +
    + ); + } +} 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 = ; + } else { + toggle = ; + } + } + + let clonedChildren = null; + if (children.length > 0 && expanded) { + clonedChildren = ( +
      + { + React.Children.map(children, (child) => { + return React.cloneElement(child, { + parentLink: link, + subsection: true + }); + }) + } +
    + ); + } + + let className = 'section'; + if (subsection) { + className = 'subsection'; + } + + return ( +
  • + + {toggle} + + {title} + + + {clonedChildren} +
  • + ); + } +} 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 ( +
    +
      + + } + /> + + } + > + + )} + /> + + )} + collapsible={true} + > + + )} + /> + + )} + /> + + +
    +
    + ); + } +} + 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( + this.setFilter(e, '')} + > + + + ); + } + + if (incomingWebhooks.length > 0) { + fields.push( + + {'|'} + + ); + + let filterClassName = 'type-filter'; + if (this.state.filter === 'incomingWebhooks') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + this.setFilter(e, 'incomingWebhooks')} + > + + + ); + } + + if (outgoingWebhooks.length > 0) { + fields.push( + + {'|'} + + ); + + let filterClassName = 'type-filter'; + if (this.state.filter === 'outgoingWebhooks') { + filterClassName += ' type-filter--selected'; + } + + fields.push( + this.setFilter(e, 'outgoingWebhooks')} + > + + + ); + } + + return ( +
    + {fields} +
    + ); + } + + 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( + + ); + } + } + + if (!this.state.filter || this.state.filter === 'outgoingWebhooks') { + for (const outgoingWebhook of outgoingWebhooks) { + integrations.push( + + ); + } + } + + return ( +
    +
    +
    +

    + +

    + + + +
    +
    + {this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)} + +
    +
    + {integrations} +
    +
    +
    + ); + } +} + +function IncomingWebhook({incomingWebhook}) { + const channel = ChannelStore.get(incomingWebhook.channel_id); + const channelName = channel ? channel.display_name : 'cannot find channel'; + + return ( +
    +
    +
    + + {channelName} + + + + +
    +
    + + {Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id} + +
    +
    +
    + ); +} + +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 ( +
    +
    +
    + + {channelName} + + + + +
    +
    + + {Utils.getWindowLocationOrigin() + '/hooks/' + outgoingWebhook.id} + +
    +
    +
    + ); +} + +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 @@ -199,6 +199,9 @@ export default class LoggedIn extends React.Component { if (this.props.children) { content = this.props.children; } else { + content.push( + this.props.navbar + ); content.push( this.props.sidebar ); @@ -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'; @@ -241,6 +244,51 @@ function renderRootComponent() { path=':team/logout' onEnter={onLoggedOut} /> + + + + + + + + + + +>>>>>>> Added initial backstage components and InstalledIntegrations page { + 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; +} -- cgit v1.2.3-1-g7c22 From 6fff6a1025be8f13bfea19869da4038a48238ccf Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 24 Mar 2016 16:41:53 -0400 Subject: Added jpg files to webpack --- webapp/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'webapp') diff --git a/webapp/webpack.config.js b/webapp/webpack.config.js index ee5c7e70b..6b22f5b3a 100644 --- a/webapp/webpack.config.js +++ b/webapp/webpack.config.js @@ -52,7 +52,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]' -- cgit v1.2.3-1-g7c22 From dcdea9f30b419eeb8d55ed9be3f824aaf27de50c Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 24 Mar 2016 16:52:29 -0400 Subject: Added AddIntegration page --- webapp/components/backstage/add_integration.jsx | 110 +++++++++++++++++++++ .../backstage/add_integration_option.jsx | 39 ++++++++ .../backstage/installed_integrations.jsx | 2 +- webapp/root.jsx | 3 +- webapp/sass/routes/_backstage.scss | 32 +++++- 5 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 webapp/components/backstage/add_integration.jsx create mode 100644 webapp/components/backstage/add_integration_option.jsx (limited to 'webapp') diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/add_integration.jsx new file mode 100644 index 000000000..1ca079bb7 --- /dev/null +++ b/webapp/components/backstage/add_integration.jsx @@ -0,0 +1,110 @@ +// 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 AddIntegrationOption from './add_integration_option.jsx'; + +import WebhookIcon from 'images/webhook_icon.jpg'; + +export default class AddIntegration 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; + } + + const options = []; + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + options.push( + + } + description={ + + } + link={`/${team.name}/integrations/add/incoming_webhook`} + /> + ); + } + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + options.push( + + } + description={ + + } + link={`/${team.name}/integrations/add/outgoing_webhook`} + /> + ); + } + + return ( +
    +
    +
    +

    + +

    +
    +
    +
    + {options} +
    +
    + ); + } +} + 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 ( + + +
    + {title} +
    +
    + {description} +
    + + ); + } +} diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx index cfb68c660..8dae44295 100644 --- a/webapp/components/backstage/installed_integrations.jsx +++ b/webapp/components/backstage/installed_integrations.jsx @@ -198,7 +198,7 @@ export default class InstalledIntegrations extends React.Component { return (
    -
    +

    Date: Mon, 28 Mar 2016 09:41:03 -0400 Subject: Added basic screen to add incoming webhooks --- .../components/backstage/add_incoming_webhook.jsx | 214 +++++++++++++++++++++ webapp/components/channel_invite_button.jsx | 1 + webapp/components/form_error.jsx | 50 +++++ webapp/components/more_direct_channels.jsx | 1 + webapp/components/spinner_button.jsx | 22 +-- webapp/root.jsx | 3 +- webapp/stores/integration_store.jsx | 21 +- webapp/utils/async_client.jsx | 46 +++++ 8 files changed, 338 insertions(+), 20 deletions(-) create mode 100644 webapp/components/backstage/add_incoming_webhook.jsx create mode 100644 webapp/components/form_error.jsx (limited to 'webapp') diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx new file mode 100644 index 000000000..962792ed0 --- /dev/null +++ b/webapp/components/backstage/add_incoming_webhook.jsx @@ -0,0 +1,214 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router'; +import ChannelStore from 'stores/channel_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddIncomingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelName = this.updateChannelName.bind(this); + + this.state = { + team: TeamStore.getCurrent(), + name: '', + description: '', + channelName: '', + saving: false, + serverError: '', + clientError: null + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.handleChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.handleChange); + } + + handleChange() { + this.setState({ + team: TeamStore.getCurrent() + }); + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + const channel = ChannelStore.getByName(this.state.channelName); + + if (!channel) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + const hook = { + channel_id: channel.id + }; + + AsyncClient.addIncomingHook( + hook, + () => { + browserHistory.push(`/${this.state.team.name}/integrations/installed`); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelName(e) { + this.setState({ + channelName: e.target.value + }); + } + + render() { + const team = TeamStore.getCurrent(); + + if (!team) { + return null; + } + + return ( +
    +
    +
    +

    + +

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + + + + +
    + + +
    + ); + } +} 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 ( 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 ( +
    + +
    + ); + } +} 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 ( 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 ( - {this.props.children} + {children} ); } diff --git a/webapp/root.jsx b/webapp/root.jsx index b6ec954c2..7e6352b11 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -40,6 +40,7 @@ import BackstageNavbar from 'components/backstage/backstage_navbar.jsx'; import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx'; import InstalledIntegrations from 'components/backstage/installed_integrations.jsx'; import AddIntegration from 'components/backstage/add_integration.jsx'; +import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; import SignupTeamComplete from 'components/signup_team_complete/components/signup_team_complete.jsx'; import WelcomePage from 'components/signup_team_complete/components/team_signup_welcome_page.jsx'; @@ -276,7 +277,7 @@ function renderRootComponent() { components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, - center: null + center: AddIncomingWebhook }} /> { + 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); + } + } + ); +} -- cgit v1.2.3-1-g7c22 From 3bb9c31967cb0c171c6de9eb81d9fb6e7b3e5126 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 28 Mar 2016 10:10:49 -0400 Subject: Added searching to InstalledIntegrations page --- .../backstage/installed_integrations.jsx | 50 +++++++++++++++++----- 1 file changed, 39 insertions(+), 11 deletions(-) (limited to 'webapp') diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx index 8dae44295..9f41ab85e 100644 --- a/webapp/components/backstage/installed_integrations.jsx +++ b/webapp/components/backstage/installed_integrations.jsx @@ -16,11 +16,13 @@ export default class InstalledIntegrations extends React.Component { super(props); this.handleChange = this.handleChange.bind(this); - this.setFilter = this.setFilter.bind(this); + this.updateFilter = this.updateFilter.bind(this); + this.updateTypeFilter = this.updateTypeFilter.bind(this); this.state = { incomingWebhooks: [], outgoingWebhooks: [], + typeFilter: '', filter: '' }; } @@ -60,11 +62,17 @@ export default class InstalledIntegrations extends React.Component { }); } - setFilter(e, filter) { + updateTypeFilter(e, typeFilter) { e.preventDefault(); this.setState({ - filter + typeFilter + }); + } + + updateFilter(e) { + this.setState({ + filter: e.target.value }); } @@ -73,7 +81,7 @@ export default class InstalledIntegrations extends React.Component { if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) { let filterClassName = 'type-filter'; - if (this.state.filter === '') { + if (this.state.typeFilter === '') { filterClassName += ' type-filter--selected'; } @@ -82,7 +90,7 @@ export default class InstalledIntegrations extends React.Component { key='allFilter' className={filterClassName} href='#' - onClick={(e) => this.setFilter(e, '')} + onClick={(e) => this.updateTypeFilter(e, '')} > this.setFilter(e, 'incomingWebhooks')} + onClick={(e) => this.updateTypeFilter(e, 'incomingWebhooks')} > this.setFilter(e, 'outgoingWebhooks')} + onClick={(e) => this.updateTypeFilter(e, 'outgoingWebhooks')} >

    -- cgit v1.2.3-1-g7c22 From 5e8ab52d23862e22886ddebecd3a455b0da076b0 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 28 Mar 2016 13:04:35 -0400 Subject: Added basic AddOutgoingWebhook page --- .../components/backstage/add_outgoing_webhook.jsx | 297 +++++++++++++++++++++ webapp/root.jsx | 3 +- 2 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 webapp/components/backstage/add_outgoing_webhook.jsx (limited to 'webapp') diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx new file mode 100644 index 000000000..6e2f749f8 --- /dev/null +++ b/webapp/components/backstage/add_outgoing_webhook.jsx @@ -0,0 +1,297 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import * as AsyncClient from 'utils/async_client.jsx'; +import {browserHistory} from 'react-router'; +import ChannelStore from 'stores/channel_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; + +import {FormattedMessage} from 'react-intl'; +import FormError from 'components/form_error.jsx'; +import {Link} from 'react-router'; +import SpinnerButton from 'components/spinner_button.jsx'; + +export default class AddOutgoingWebhook extends React.Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + + this.updateName = this.updateName.bind(this); + this.updateDescription = this.updateDescription.bind(this); + this.updateChannelName = this.updateChannelName.bind(this); + this.updateTriggerWords = this.updateTriggerWords.bind(this); + this.updateCallbackUrls = this.updateCallbackUrls.bind(this); + + this.state = { + team: TeamStore.getCurrent(), + name: '', + description: '', + channelName: '', + triggerWords: '', + callbackUrls: '', + saving: false, + serverError: '', + clientError: null + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.handleChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.handleChange); + } + + handleChange() { + this.setState({ + team: TeamStore.getCurrent() + }); + } + + handleSubmit(e) { + e.preventDefault(); + + if (this.state.saving) { + return; + } + + this.setState({ + saving: true, + serverError: '', + clientError: '' + }); + + let channelId = ''; + if (this.state.channelName) { + const channel = ChannelStore.getByName(this.state.channelName); + + if (!channel) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + channelId = channel.id; + } else if (!this.state.triggerWords) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + if (!this.state.callbackUrls) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + const hook = { + channel_id: 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(`/${this.state.team.name}/integrations/installed`); + }, + (err) => { + this.setState({ + serverError: err.message + }); + } + ); + } + + updateName(e) { + this.setState({ + name: e.target.value + }); + } + + updateDescription(e) { + this.setState({ + description: e.target.value + }); + } + + updateChannelName(e) { + this.setState({ + channelName: e.target.value + }); + } + + updateTriggerWords(e) { + this.setState({ + triggerWords: e.target.value + }); + } + + updateCallbackUrls(e) { + this.setState({ + callbackUrls: e.target.value + }); + } + + render() { + const team = TeamStore.getCurrent(); + + if (!team) { + return null; + } + + return ( +
    +
    +
    +

    + +

    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +