From 3043b5d52a0192f4e9f1574de42ca9c23a725093 Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Thu, 27 Jul 2017 10:16:16 +0200 Subject: Migrate add and edit incoming webhook components to redux (#6885) * Migrate add incoming webhook components to redux * Migrate edit incoming webhook components to redux * Add tests --- .../components/abstract_incoming_webhook.jsx | 84 +++++++++------- .../components/add_incoming_webhook.jsx | 32 ------ .../add_incoming_webhook/add_incoming_webhook.jsx | 68 +++++++++++++ .../components/add_incoming_webhook/index.js | 25 +++++ .../components/edit_incoming_webhook.jsx | 74 -------------- .../edit_incoming_webhook.jsx | 112 +++++++++++++++++++++ .../components/edit_incoming_webhook/index.js | 30 ++++++ webapp/routes/route_integrations.jsx | 4 +- .../__snapshots__/add_incoming_hook.test.jsx.snap | 26 +++++ .../__snapshots__/edit_incoming_hook.test.jsx.snap | 7 ++ .../integrations/add_incoming_hook.test.jsx | 29 ++++++ .../integrations/edit_incoming_hook.test.jsx | 30 ++++++ 12 files changed, 376 insertions(+), 145 deletions(-) delete mode 100644 webapp/components/integrations/components/add_incoming_webhook.jsx create mode 100644 webapp/components/integrations/components/add_incoming_webhook/add_incoming_webhook.jsx create mode 100644 webapp/components/integrations/components/add_incoming_webhook/index.js delete mode 100644 webapp/components/integrations/components/edit_incoming_webhook.jsx create mode 100644 webapp/components/integrations/components/edit_incoming_webhook/edit_incoming_webhook.jsx create mode 100644 webapp/components/integrations/components/edit_incoming_webhook/index.js create mode 100644 webapp/tests/components/integrations/__snapshots__/add_incoming_hook.test.jsx.snap create mode 100644 webapp/tests/components/integrations/__snapshots__/edit_incoming_hook.test.jsx.snap create mode 100644 webapp/tests/components/integrations/add_incoming_hook.test.jsx create mode 100644 webapp/tests/components/integrations/edit_incoming_hook.test.jsx (limited to 'webapp') diff --git a/webapp/components/integrations/components/abstract_incoming_webhook.jsx b/webapp/components/integrations/components/abstract_incoming_webhook.jsx index a75c29fac..1253842a7 100644 --- a/webapp/components/integrations/components/abstract_incoming_webhook.jsx +++ b/webapp/components/integrations/components/abstract_incoming_webhook.jsx @@ -12,48 +12,57 @@ import SpinnerButton from 'components/spinner_button.jsx'; import {Link} from 'react-router/es6'; export default class AbstractIncomingWebhook extends React.Component { - static get propTypes() { - return { - team: PropTypes.object - }; + static propTypes = { + + /** + * The current team + */ + team: PropTypes.object.isRequired, + + /** + * The header text to render, has id and defaultMessage + */ + header: PropTypes.object.isRequired, + + /** + * The footer text to render, has id and defaultMessage + */ + footer: PropTypes.object.isRequired, + + /** + * The server error text after a failed action + */ + serverError: PropTypes.string.isRequired, + + /** + * The hook used to set the initial state + */ + initialHook: PropTypes.object, + + /** + * The async function to run when the action button is pressed + */ + action: PropTypes.func.isRequired } constructor(props) { super(props); - this.handleSubmit = this.handleSubmit.bind(this); - - this.updateDisplayName = this.updateDisplayName.bind(this); - this.updateDescription = this.updateDescription.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); + this.state = this.getStateFromHook(this.props.initialHook || {}); + } - this.state = { - displayName: '', - description: '', - channelId: '', + getStateFromHook = (hook) => { + return { + displayName: hook.display_name || '', + description: hook.description || '', + channelId: hook.channel_id || '', saving: false, serverError: '', clientError: null }; - - if (typeof this.performAction === 'undefined') { - throw new TypeError('Subclasses must override performAction'); - } - - if (typeof this.header === 'undefined') { - throw new TypeError('Subclasses must override header'); - } - - if (typeof this.footer === 'undefined') { - throw new TypeError('Subclasses must override footer'); - } - - this.performAction = this.performAction.bind(this); - this.header = this.header.bind(this); - this.footer = this.footer.bind(this); } - handleSubmit(e) { + handleSubmit = (e) => { e.preventDefault(); if (this.state.saving) { @@ -86,30 +95,31 @@ export default class AbstractIncomingWebhook extends React.Component { description: this.state.description }; - this.performAction(hook); + this.props.action(hook).then(() => this.setState({saving: false})); } - updateDisplayName(e) { + updateDisplayName = (e) => { this.setState({ displayName: e.target.value }); } - updateDescription(e) { + updateDescription = (e) => { this.setState({ description: e.target.value }); } - updateChannelId(e) { + updateChannelId = (e) => { this.setState({ channelId: e.target.value }); } render() { - var headerToRender = this.header(); - var footerToRender = this.footer(); + var headerToRender = this.props.header; + var footerToRender = this.props.footer; + return (
@@ -212,7 +222,7 @@ export default class AbstractIncomingWebhook extends React.Component {
{ - browserHistory.push(`/${this.props.team.name}/integrations/confirm?type=incoming_webhooks&id=${data.id}`); - }, - (err) => { - this.setState({ - saving: false, - serverError: err.message - }); - } - ); - } - - header() { - return {id: 'integrations.add', defaultMessage: 'Add'}; - } - - footer() { - return {id: 'add_incoming_webhook.save', defaultMessage: 'Save'}; - } -} diff --git a/webapp/components/integrations/components/add_incoming_webhook/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook/add_incoming_webhook.jsx new file mode 100644 index 000000000..23f0fad6f --- /dev/null +++ b/webapp/components/integrations/components/add_incoming_webhook/add_incoming_webhook.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {browserHistory} from 'react-router/es6'; +import PropTypes from 'prop-types'; + +import AbstractIncomingWebhook from 'components/integrations/components/abstract_incoming_webhook.jsx'; + +const HEADER = {id: 'integrations.add', defaultMessage: 'Add'}; +const FOOTER = {id: 'add_incoming_webhook.save', defaultMessage: 'Save'}; + +export default class AddIncomingWebhook extends React.PureComponent { + static propTypes = { + + /** + * The current team + */ + team: PropTypes.object.isRequired, + + /** + * The request state for createIncomingHook action. Contains status and error + */ + createIncomingHookRequest: PropTypes.object.isRequired, + + actions: PropTypes.shape({ + + /** + * The function to call to add a new incoming webhook + */ + createIncomingHook: PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.state = { + serverError: '' + }; + } + + addIncomingHook = async (hook) => { + this.setState({serverError: ''}); + + const data = await this.props.actions.createIncomingHook(hook); + if (data) { + browserHistory.push(`/${this.props.team.name}/integrations/confirm?type=incoming_webhooks&id=${data.id}`); + return; + } + + if (this.props.createIncomingHookRequest.error) { + this.setState({serverError: this.props.createIncomingHookRequest.error.message}); + } + } + + render() { + return ( + + ); + } +} diff --git a/webapp/components/integrations/components/add_incoming_webhook/index.js b/webapp/components/integrations/components/add_incoming_webhook/index.js new file mode 100644 index 000000000..ed2b53ba8 --- /dev/null +++ b/webapp/components/integrations/components/add_incoming_webhook/index.js @@ -0,0 +1,25 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + + import {connect} from 'react-redux'; + import {bindActionCreators} from 'redux'; + import {createIncomingHook} from 'mattermost-redux/actions/integrations'; + + import AddIncomingWebhook from './add_incoming_webhook.jsx'; + + function mapStateToProps(state, ownProps) { + return { + ...ownProps, + createIncomingHookRequest: state.requests.integrations.createIncomingHook + }; + } + + function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + createIncomingHook + }, dispatch) + }; + } + + export default connect(mapStateToProps, mapDispatchToProps)(AddIncomingWebhook); \ No newline at end of file diff --git a/webapp/components/integrations/components/edit_incoming_webhook.jsx b/webapp/components/integrations/components/edit_incoming_webhook.jsx deleted file mode 100644 index 00cb50cbd..000000000 --- a/webapp/components/integrations/components/edit_incoming_webhook.jsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {browserHistory} from 'react-router/es6'; -import IntegrationStore from 'stores/integration_store.jsx'; -import {updateIncomingHook, loadIncomingHooks} from 'actions/integration_actions.jsx'; - -import AbstractIncomingWebhook from './abstract_incoming_webhook.jsx'; -import TeamStore from 'stores/team_store.jsx'; - -export default class EditIncomingWebhook extends AbstractIncomingWebhook { - constructor(props) { - super(props); - - this.handleIntegrationChange = this.handleIntegrationChange.bind(this); - this.originalIncomingHook = null; - } - - componentDidMount() { - IntegrationStore.addChangeListener(this.handleIntegrationChange); - - if (window.mm_config.EnableIncomingWebhooks === 'true') { - loadIncomingHooks(); - } - } - - componentWillUnmount() { - IntegrationStore.removeChangeListener(this.handleIntegrationChange); - } - - handleIntegrationChange() { - const teamId = TeamStore.getCurrentId(); - - const hooks = IntegrationStore.getIncomingWebhooks(teamId); - const loading = !IntegrationStore.hasReceivedIncomingWebhooks(teamId); - - if (!loading) { - this.originalIncomingHook = hooks.filter((hook) => hook.id === this.props.location.query.id)[0]; - - this.setState({ - displayName: this.originalIncomingHook.display_name, - description: this.originalIncomingHook.description, - channelId: this.originalIncomingHook.channel_id - }); - } - } - - performAction(hook) { - if (this.originalIncomingHook.id) { - hook.id = this.originalIncomingHook.id; - } - - updateIncomingHook( - hook, - () => { - browserHistory.push(`/${this.props.team.name}/integrations/incoming_webhooks`); - }, - (err) => { - this.setState({ - saving: false, - serverError: err.message - }); - } - ); - } - - header() { - return {id: 'integrations.edit', defaultMessage: 'Edit'}; - } - - footer() { - return {id: 'update_incoming_webhook.update', defaultMessage: 'Update'}; - } -} diff --git a/webapp/components/integrations/components/edit_incoming_webhook/edit_incoming_webhook.jsx b/webapp/components/integrations/components/edit_incoming_webhook/edit_incoming_webhook.jsx new file mode 100644 index 000000000..35d8983a2 --- /dev/null +++ b/webapp/components/integrations/components/edit_incoming_webhook/edit_incoming_webhook.jsx @@ -0,0 +1,112 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {browserHistory} from 'react-router/es6'; +import LoadingScreen from 'components/loading_screen.jsx'; + +import AbstractIncomingWebhook from 'components/integrations/components/abstract_incoming_webhook.jsx'; + +import React from 'react'; +import PropTypes from 'prop-types'; + +const HEADER = {id: 'integrations.edit', defaultMessage: 'Edit'}; +const FOOTER = {id: 'update_incoming_webhook.update', defaultMessage: 'Update'}; + +export default class EditIncomingWebhook extends React.PureComponent { + static propTypes = { + + /** + * The current team + */ + team: PropTypes.object.isRequired, + + /** + * The incoming webhook to edit + */ + hook: PropTypes.object, + + /** + * The id of the incoming webhook to edit + */ + hookId: PropTypes.string.isRequired, + + /** + * The request state for updateIncomingHook action. Contains status and error + */ + updateIncomingHookRequest: PropTypes.object.isRequired, + + actions: PropTypes.shape({ + + /** + * The function to call to update an incoming webhook + */ + updateIncomingHook: PropTypes.func.isRequired, + + /** + * The function to call to get an incoming webhook + */ + getIncomingHook: PropTypes.func.isRequired + }).isRequired + } + + constructor(props) { + super(props); + + this.state = { + showConfirmModal: false, + serverError: '' + }; + } + + componentDidMount() { + if (window.mm_config.EnableIncomingWebhooks === 'true') { + this.props.actions.getIncomingHook(this.props.hookId); + } + } + + editIncomingHook = async (hook) => { + this.newHook = hook; + + if (this.props.hook.id) { + hook.id = this.props.hook.id; + } + + if (this.props.hook.token) { + hook.token = this.props.hook.token; + } + + await this.submitHook(); + } + + submitHook = async () => { + this.setState({serverError: ''}); + + const data = await this.props.actions.updateIncomingHook(this.newHook); + + if (data) { + browserHistory.push(`/${this.props.team.name}/integrations/incoming_webhooks`); + return; + } + + if (this.props.updateIncomingHookRequest.error) { + this.setState({serverError: this.props.updateIncomingHookRequest.error.message}); + } + } + + render() { + if (!this.props.hook) { + return ; + } + + return ( + + ); + } +} diff --git a/webapp/components/integrations/components/edit_incoming_webhook/index.js b/webapp/components/integrations/components/edit_incoming_webhook/index.js new file mode 100644 index 000000000..a391a98a6 --- /dev/null +++ b/webapp/components/integrations/components/edit_incoming_webhook/index.js @@ -0,0 +1,30 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import {updateIncomingHook, getIncomingHook} from 'mattermost-redux/actions/integrations'; + +import EditIncomingWebhook from './edit_incoming_webhook.jsx'; + +function mapStateToProps(state, ownProps) { + const hookId = ownProps.location.query.id; + + return { + ...ownProps, + hookId, + hook: state.entities.integrations.incomingHooks[hookId], + updateIncomingHookRequest: state.requests.integrations.createIncomingHook + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + updateIncomingHook, + getIncomingHook + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(EditIncomingWebhook); \ No newline at end of file diff --git a/webapp/routes/route_integrations.jsx b/webapp/routes/route_integrations.jsx index 37b33ed40..169d374c6 100644 --- a/webapp/routes/route_integrations.jsx +++ b/webapp/routes/route_integrations.jsx @@ -25,13 +25,13 @@ export default { { path: 'add', getComponents: (location, callback) => { - System.import('components/integrations/components/add_incoming_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); + System.import('components/integrations/components/add_incoming_webhook').then(RouteUtils.importComponentSuccess(callback)); } }, { path: 'edit', getComponents: (location, callback) => { - System.import('components/integrations/components/edit_incoming_webhook.jsx').then(RouteUtils.importComponentSuccess(callback)); + System.import('components/integrations/components/edit_incoming_webhook').then(RouteUtils.importComponentSuccess(callback)); } } ] diff --git a/webapp/tests/components/integrations/__snapshots__/add_incoming_hook.test.jsx.snap b/webapp/tests/components/integrations/__snapshots__/add_incoming_hook.test.jsx.snap new file mode 100644 index 000000000..ce34d81bc --- /dev/null +++ b/webapp/tests/components/integrations/__snapshots__/add_incoming_hook.test.jsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/integrations/AddIncomingWebhook should match snapshot 1`] = ` + +`; diff --git a/webapp/tests/components/integrations/__snapshots__/edit_incoming_hook.test.jsx.snap b/webapp/tests/components/integrations/__snapshots__/edit_incoming_hook.test.jsx.snap new file mode 100644 index 000000000..ed392dcbd --- /dev/null +++ b/webapp/tests/components/integrations/__snapshots__/edit_incoming_hook.test.jsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/integrations/EditIncomingWebhook should match snapshot 1`] = ` + +`; diff --git a/webapp/tests/components/integrations/add_incoming_hook.test.jsx b/webapp/tests/components/integrations/add_incoming_hook.test.jsx new file mode 100644 index 000000000..ae5a46cb2 --- /dev/null +++ b/webapp/tests/components/integrations/add_incoming_hook.test.jsx @@ -0,0 +1,29 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + + import React from 'react'; + import {shallow} from 'enzyme'; + + import AddIncomingWebhook from 'components/integrations/components/add_incoming_webhook/add_incoming_webhook.jsx'; + + describe('components/integrations/AddIncomingWebhook', () => { + test('should match snapshot', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + const teamId = 'testteamid'; + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); diff --git a/webapp/tests/components/integrations/edit_incoming_hook.test.jsx b/webapp/tests/components/integrations/edit_incoming_hook.test.jsx new file mode 100644 index 000000000..cb7544314 --- /dev/null +++ b/webapp/tests/components/integrations/edit_incoming_hook.test.jsx @@ -0,0 +1,30 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + + import React from 'react'; + import {shallow} from 'enzyme'; + + import EditIncomingWebhook from 'components/integrations/components/edit_incoming_webhook/edit_incoming_webhook.jsx'; + + describe('components/integrations/EditIncomingWebhook', () => { + test('should match snapshot', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + const teamId = 'testteamid'; + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); -- cgit v1.2.3-1-g7c22