diff --git a/webapp/channels/src/components/integrations/__snapshots__/abstract_outgoing_webhook.test.jsx.snap b/webapp/channels/src/components/integrations/__snapshots__/abstract_outgoing_webhook.test.tsx.snap
similarity index 92%
rename from webapp/channels/src/components/integrations/__snapshots__/abstract_outgoing_webhook.test.jsx.snap
rename to webapp/channels/src/components/integrations/__snapshots__/abstract_outgoing_webhook.test.tsx.snap
index bba972e9de..ad4cdbde38 100644
--- a/webapp/channels/src/components/integrations/__snapshots__/abstract_outgoing_webhook.test.jsx.snap
+++ b/webapp/channels/src/components/integrations/__snapshots__/abstract_outgoing_webhook.test.tsx.snap
@@ -6,7 +6,7 @@ exports[`components/integrations/AbstractOutgoingWebhook should match snapshot 1
>
@@ -271,10 +276,12 @@ exports[`components/integrations/AbstractOutgoingWebhook should match snapshot 1
diff --git a/webapp/channels/src/components/integrations/abstract_incoming_hook.test.tsx b/webapp/channels/src/components/integrations/abstract_incoming_hook.test.tsx
index 514d2a0acc..3ab74467f5 100644
--- a/webapp/channels/src/components/integrations/abstract_incoming_hook.test.tsx
+++ b/webapp/channels/src/components/integrations/abstract_incoming_hook.test.tsx
@@ -9,6 +9,8 @@ import ChannelSelect from 'components/channel_select';
import AbstractIncomingWebhook from 'components/integrations/abstract_incoming_webhook';
import {Team} from '@mattermost/types/teams';
+type AbstractIncomingWebhookProps = React.ComponentProps;
+
describe('components/integrations/AbstractIncomingWebhook', () => {
const team: Team = {id: 'team_id',
create_at: 0,
@@ -55,7 +57,7 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
},
);
- const requiredProps = {
+ const requiredProps: AbstractIncomingWebhookProps = {
team,
header,
footer,
@@ -80,10 +82,9 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
});
test('should match snapshot, displays client error when no initial hook', () => {
- const newInitialHook = {};
- const props = {...requiredProps, initialHook: newInitialHook};
+ const props = {...requiredProps};
+ delete props.initialHook;
const wrapper = shallow( );
-
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
diff --git a/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx b/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx
index cfeb445fff..6e3a2bc390 100644
--- a/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx
+++ b/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx
@@ -55,7 +55,7 @@ interface Props {
/**
* The hook used to set the initial state
*/
- initialHook?: IncomingWebhook | Record;
+ initialHook?: IncomingWebhook;
/**
* Whether to allow configuration of the default post username.
@@ -77,10 +77,10 @@ export default class AbstractIncomingWebhook extends PureComponent
constructor(props: Props | Readonly) {
super(props);
- this.state = this.getStateFromHook(this.props.initialHook || {});
+ this.state = this.getStateFromHook(this.props.initialHook);
}
- getStateFromHook = (hook: IncomingWebhook | Record) => {
+ getStateFromHook = (hook?: IncomingWebhook) => {
return {
displayName: hook?.display_name || '',
description: hook?.description || '',
diff --git a/webapp/channels/src/components/integrations/abstract_outgoing_webhook.test.jsx b/webapp/channels/src/components/integrations/abstract_outgoing_webhook.test.jsx
deleted file mode 100644
index bbd11904db..0000000000
--- a/webapp/channels/src/components/integrations/abstract_outgoing_webhook.test.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import React from 'react';
-import {shallow} from 'enzyme';
-
-import AbstractOutgoingWebhook from 'components/integrations/abstract_outgoing_webhook';
-
-describe('components/integrations/AbstractOutgoingWebhook', () => {
- const emptyFunction = jest.fn();
- const props = {
- team: {
- id: 'test-team-id',
- name: 'test',
- },
- action: emptyFunction,
- enablePostUsernameOverride: false,
- enablePostIconOverride: false,
- header: {id: 'add', defaultMessage: 'add'},
- footer: {id: 'save', defaultMessage: 'save'},
- loading: {id: 'loading', defaultMessage: 'loading'},
- renderExtra: '',
- serverError: '',
- };
-
- test('should match snapshot', () => {
- const wrapper = shallow( );
- expect(wrapper).toMatchSnapshot();
- });
-
- test('should render username in case of enablePostUsernameOverride is true ', () => {
- const usernameTrueProps = {...props, enablePostUsernameOverride: true};
- const wrapper = shallow( );
- expect(wrapper.find('#username')).toHaveLength(1);
- });
-
- test('should render username in case of enablePostUsernameOverride is true ', () => {
- const iconUrlTrueProps = {...props, enablePostIconOverride: true};
- const wrapper = shallow( );
- expect(wrapper.find('#iconURL')).toHaveLength(1);
- });
-});
diff --git a/webapp/channels/src/components/integrations/abstract_outgoing_webhook.test.tsx b/webapp/channels/src/components/integrations/abstract_outgoing_webhook.test.tsx
new file mode 100644
index 0000000000..5370692752
--- /dev/null
+++ b/webapp/channels/src/components/integrations/abstract_outgoing_webhook.test.tsx
@@ -0,0 +1,165 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import AbstractOutgoingWebhook from 'components/integrations/abstract_outgoing_webhook';
+import ChannelSelect from 'components/channel_select';
+import {Team} from '@mattermost/types/teams';
+
+describe('components/integrations/AbstractOutgoingWebhook', () => {
+ const team: Team = {
+ id: 'team_id',
+ create_at: 0,
+ update_at: 0,
+ delete_at: 0,
+ display_name: 'team_name',
+ name: 'team_name',
+ description: 'team_description',
+ email: 'team_email',
+ type: 'I',
+ company_name: 'team_company_name',
+ allowed_domains: 'team_allowed_domains',
+ invite_id: 'team_invite_id',
+ allow_open_invite: false,
+ scheme_id: 'team_scheme_id',
+ group_constrained: false,
+ };
+ const header = {id: 'header_id', defaultMessage: 'Header'};
+ const footer = {id: 'footer_id', defaultMessage: 'Footer'};
+ const loading = {id: 'loading_id', defaultMessage: 'Loading'};
+
+ const initialHook = {
+ display_name: 'testOutgoingWebhook',
+ channel_id: '88cxd9wpzpbpfp8pad78xj75pr',
+ creator_id: 'test_creator_id',
+ description: 'testing',
+ id: 'test_id',
+ team_id: 'test_team_id',
+ token: 'test_token',
+ trigger_words: ['test', 'trigger', 'word'],
+ trigger_when: 0,
+ callback_urls: ['callbackUrl1.com', 'callbackUrl2.com'],
+ content_type: 'test_content_type',
+ create_at: 0,
+ update_at: 0,
+ delete_at: 0,
+ user_id: 'test_user_id',
+ username: '',
+ icon_url: '',
+ channel_locked: false,
+ };
+
+ const action = jest.fn().mockImplementation(
+ () => {
+ return new Promise((resolve) => {
+ process.nextTick(() => resolve());
+ });
+ },
+ );
+
+ const requiredProps = {
+ team,
+ header,
+ footer,
+ loading,
+ initialHook,
+ enablePostUsernameOverride: false,
+ enablePostIconOverride: false,
+ renderExtra: '',
+ serverError: '',
+ action,
+ };
+
+ test('should match snapshot', () => {
+ const wrapper = shallow( );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should not render username in case of enablePostUsernameOverride is false ', () => {
+ const usernameTrueProps = {...requiredProps};
+ const wrapper = shallow( );
+ expect(wrapper.find('#username')).toHaveLength(0);
+ });
+
+ test('should not render post icon override in case of enablePostIconOverride is false ', () => {
+ const iconUrlTrueProps = {...requiredProps};
+ const wrapper = shallow( );
+ expect(wrapper.find('#iconURL')).toHaveLength(0);
+ });
+
+ test('should render username in case of enablePostUsernameOverride is true ', () => {
+ const usernameTrueProps = {...requiredProps, enablePostUsernameOverride: true};
+ const wrapper = shallow( );
+ expect(wrapper.find('#username')).toHaveLength(1);
+ });
+
+ test('should render post icon override in case of enablePostIconOverride is true ', () => {
+ const iconUrlTrueProps = {...requiredProps, enablePostIconOverride: true};
+ const wrapper = shallow( );
+ expect(wrapper.find('#iconURL')).toHaveLength(1);
+ });
+
+ test('should update state.channelId when on channel change', () => {
+ const newChannelId = 'new_channel_id';
+ const evt = {
+ preventDefault: jest.fn(),
+ target: {value: newChannelId},
+ };
+
+ const wrapper = shallow( );
+ wrapper.find(ChannelSelect).simulate('change', evt);
+
+ expect(wrapper.state('channelId')).toBe(newChannelId);
+ });
+
+ test('should update state.description when on description change', () => {
+ const newDescription = 'new_description';
+ const evt = {
+ preventDefault: jest.fn(),
+ target: {value: newDescription},
+ };
+
+ const wrapper = shallow( );
+ wrapper.find('#description').simulate('change', evt);
+
+ expect(wrapper.state('description')).toBe(newDescription);
+ });
+
+ test('should update state.username on post username change', () => {
+ const usernameTrueProps = {...requiredProps, enablePostUsernameOverride: true};
+ const newUsername = 'new_username';
+ const evt = {
+ preventDefault: jest.fn(),
+ target: {value: newUsername},
+ };
+
+ const wrapper = shallow( );
+ wrapper.find('#username').simulate('change', evt);
+
+ expect(wrapper.state('username')).toBe(newUsername);
+ });
+
+ test('should update state.triggerWhen on selection change', () => {
+ const wrapper = shallow( );
+ expect(wrapper.state('triggerWhen')).toBe(0);
+
+ const selector = wrapper.find('#triggerWhen');
+ selector.simulate('change', {target: {value: 1}});
+ console.log('selector: ', selector.debug());
+ expect(wrapper.state('triggerWhen')).toBe(1);
+ });
+
+ test('should call action function', () => {
+ const wrapper = shallow( );
+
+ wrapper.find('#displayName').simulate('change', {target: {value: 'name'}});
+ wrapper.find('.btn-primary').simulate('click', {preventDefault() {
+ return jest.fn();
+ }});
+
+ expect(action).toBeCalled();
+ expect(action).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/webapp/channels/src/components/integrations/abstract_outgoing_webhook.jsx b/webapp/channels/src/components/integrations/abstract_outgoing_webhook.tsx
similarity index 84%
rename from webapp/channels/src/components/integrations/abstract_outgoing_webhook.jsx
rename to webapp/channels/src/components/integrations/abstract_outgoing_webhook.tsx
index 7627c6cc3e..5dd2cf50f5 100644
--- a/webapp/channels/src/components/integrations/abstract_outgoing_webhook.jsx
+++ b/webapp/channels/src/components/integrations/abstract_outgoing_webhook.tsx
@@ -1,83 +1,97 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import PropTypes from 'prop-types';
-import React from 'react';
-import {FormattedMessage} from 'react-intl';
+import React, {ChangeEventHandler, FormEvent, MouseEvent} from 'react';
+import {FormattedMessage, MessageDescriptor} from 'react-intl';
import {Link} from 'react-router-dom';
import {localizeMessage} from 'utils/utils';
-
+import {Team} from '@mattermost/types/teams';
import BackstageHeader from 'components/backstage/components/backstage_header';
import ChannelSelect from 'components/channel_select';
import FormError from 'components/form_error';
import SpinnerButton from 'components/spinner_button';
import ExternalLink from 'components/external_link';
import {DocLinks} from 'utils/constants';
+import {OutgoingWebhook} from '@mattermost/types/integrations';
-export default class AbstractOutgoingWebhook extends React.PureComponent {
- static propTypes = {
+interface State {
+ callbackUrls: string;
+ channelId: string;
+ clientError: JSX.Element | null;
+ contentType: string;
+ description: string;
+ displayName: string;
+ iconURL: string;
+ saving: boolean;
+ triggerWhen: number;
+ triggerWords: string;
+ username: string;
+}
- /**
- * The current team
- */
- team: PropTypes.object.isRequired,
+interface Props {
- /**
- * The header text to render, has id and defaultMessage
- */
- header: PropTypes.object.isRequired,
+ /**
+ * The current team
+ */
+ team: Team;
- /**
- * The footer text to render, has id and defaultMessage
- */
- footer: PropTypes.object.isRequired,
+ /**
+ * The header text to render, has id and defaultMessage
+ */
+ header: MessageDescriptor;
- /**
- * The spinner loading text to render, has id and defaultMessage
- */
- loading: PropTypes.object.isRequired,
+ /**
+ * The footer text to render, has id and defaultMessage
+ */
+ footer: MessageDescriptor;
- /**
- * Any extra component/node to render
- */
- renderExtra: PropTypes.node.isRequired,
+ /**
+ * The spinner loading text to render, has id and defaultMessage
+ */
+ loading: MessageDescriptor;
- /**
- * The server error text after a failed action
- */
- serverError: PropTypes.string.isRequired,
+ /**
+ * Any extra component/node to render
+ */
+ renderExtra: React.ReactNode;
- /**
- * The hook used to set the initial state
- */
- initialHook: PropTypes.object,
+ /**
+ * The server error text after a failed action
+ */
+ serverError: string;
- /**
- * The async function to run when the action button is pressed
- */
- action: PropTypes.func.isRequired,
+ /**
+ * The hook used to set the initial state
+ */
+ initialHook?: OutgoingWebhook;
- /**
- * Whether to allow configuration of the default post username.
- */
- enablePostUsernameOverride: PropTypes.bool.isRequired,
+ /**
+ * The async function to run when the action button is pressed
+ */
+ action: (hook: OutgoingWebhook) => Promise;
- /**
- * Whether to allow configuration of the default post icon.
- */
- enablePostIconOverride: PropTypes.bool.isRequired,
- };
+ /**
+ * Whether to allow configuration of the default post username.
+ */
+ enablePostUsernameOverride: boolean;
- constructor(props) {
+ /**
+ * Whether to allow configuration of the default post icon.
+ */
+ enablePostIconOverride: boolean;
+}
+
+export default class AbstractOutgoingWebhook extends React.PureComponent {
+ constructor(props: Props | Readonly) {
super(props);
- this.state = this.getStateFromHook(this.props.initialHook || {});
+ this.state = this.getStateFromHook(this.props.initialHook);
}
- getStateFromHook = (hook) => {
+ getStateFromHook = (hook?: OutgoingWebhook) => {
let triggerWords = '';
- if (hook.trigger_words) {
+ if (hook?.trigger_words) {
let i = 0;
for (i = 0; i < hook.trigger_words.length; i++) {
triggerWords += hook.trigger_words[i] + '\n';
@@ -85,7 +99,7 @@ export default class AbstractOutgoingWebhook extends React.PureComponent {
}
let callbackUrls = '';
- if (hook.callback_urls) {
+ if (hook?.callback_urls) {
let i = 0;
for (i = 0; i < hook.callback_urls.length; i++) {
callbackUrls += hook.callback_urls[i] + '\n';
@@ -93,21 +107,21 @@ export default class AbstractOutgoingWebhook extends React.PureComponent {
}
return {
- displayName: hook.display_name || '',
- description: hook.description || '',
- contentType: hook.content_type || 'application/x-www-form-urlencoded',
- channelId: hook.channel_id || '',
+ displayName: hook?.display_name || '',
+ description: hook?.description || '',
+ contentType: hook?.content_type || 'application/x-www-form-urlencoded',
+ channelId: hook?.channel_id || '',
triggerWords,
- triggerWhen: hook.trigger_when || 0,
+ triggerWhen: hook?.trigger_when || 0,
callbackUrls,
saving: false,
clientError: null,
- username: hook.username || '',
- iconURL: hook.icon_url || '',
+ username: hook?.username || '',
+ iconURL: hook?.icon_url || '',
};
};
- handleSubmit = (e) => {
+ handleSubmit = (e: MouseEvent | FormEvent) => {
e.preventDefault();
if (this.state.saving) {
@@ -116,7 +130,7 @@ export default class AbstractOutgoingWebhook extends React.PureComponent {
this.setState({
saving: true,
- clientError: '',
+ clientError: null,
});
const triggerWords = [];
@@ -171,67 +185,73 @@ export default class AbstractOutgoingWebhook extends React.PureComponent {
team_id: this.props.team.id,
channel_id: this.state.channelId,
trigger_words: triggerWords,
- trigger_when: parseInt(this.state.triggerWhen, 10),
+ trigger_when: this.state.triggerWhen,
callback_urls: callbackUrls,
display_name: this.state.displayName,
content_type: this.state.contentType,
description: this.state.description,
username: this.state.username,
icon_url: this.state.iconURL,
+ id: this.props.initialHook?.id || '',
+ create_at: this.props.initialHook?.create_at || 0,
+ update_at: this.props.initialHook?.update_at || 0,
+ delete_at: this.props.initialHook?.delete_at || 0,
+ creator_id: this.props.initialHook?.creator_id || '',
+ token: this.props.initialHook?.token || '',
};
this.props.action(hook).then(() => this.setState({saving: false}));
};
- updateDisplayName = (e) => {
+ updateDisplayName: ChangeEventHandler = (e) => {
this.setState({
displayName: e.target.value,
});
};
- updateDescription = (e) => {
+ updateDescription: ChangeEventHandler = (e) => {
this.setState({
description: e.target.value,
});
};
- updateContentType = (e) => {
+ updateContentType: ChangeEventHandler = (e) => {
this.setState({
contentType: e.target.value,
});
};
- updateChannelId = (e) => {
+ updateChannelId: ChangeEventHandler = (e) => {
this.setState({
channelId: e.target.value,
});
};
- updateTriggerWords = (e) => {
+ updateTriggerWords: ChangeEventHandler = (e) => {
this.setState({
triggerWords: e.target.value,
});
};
- updateTriggerWhen = (e) => {
+ updateTriggerWhen: ChangeEventHandler = (e) => {
this.setState({
- triggerWhen: e.target.value,
+ triggerWhen: parseInt(e.target.value, 10),
});
};
- updateCallbackUrls = (e) => {
+ updateCallbackUrls: ChangeEventHandler = (e) => {
this.setState({
callbackUrls: e.target.value,
});
};
- updateUsername = (e) => {
+ updateUsername: ChangeEventHandler = (e) => {
this.setState({
username: e.target.value,
});
};
- updateIconURL = (e) => {
+ updateIconURL: ChangeEventHandler = (e) => {
this.setState({
iconURL: e.target.value,
});
@@ -241,9 +261,9 @@ export default class AbstractOutgoingWebhook extends React.PureComponent {
const contentTypeOption1 = 'application/x-www-form-urlencoded';
const contentTypeOption2 = 'application/json';
- var headerToRender = this.props.header;
- var footerToRender = this.props.footer;
- var renderExtra = this.props.renderExtra;
+ const headerToRender = this.props.header;
+ const footerToRender = this.props.footer;
+ const renderExtra = this.props.renderExtra;
return (