diff --git a/webapp/channels/src/components/integrations/__snapshots__/abstract_command.test.jsx.snap b/webapp/channels/src/components/integrations/__snapshots__/abstract_command.test.tsx.snap
similarity index 98%
rename from webapp/channels/src/components/integrations/__snapshots__/abstract_command.test.jsx.snap
rename to webapp/channels/src/components/integrations/__snapshots__/abstract_command.test.tsx.snap
index bddce4b637..82f849c5a0 100644
--- a/webapp/channels/src/components/integrations/__snapshots__/abstract_command.test.jsx.snap
+++ b/webapp/channels/src/components/integrations/__snapshots__/abstract_command.test.tsx.snap
@@ -43,7 +43,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
- renderExtra
+
+ renderExtra
+
@@ -512,7 +514,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
- renderExtra
+
+ renderExtra
+
diff --git a/webapp/channels/src/components/integrations/abstract_command.test.jsx b/webapp/channels/src/components/integrations/abstract_command.test.tsx
similarity index 76%
rename from webapp/channels/src/components/integrations/abstract_command.test.jsx
rename to webapp/channels/src/components/integrations/abstract_command.test.tsx
index 6badf0741f..1b8630c365 100644
--- a/webapp/channels/src/components/integrations/abstract_command.test.jsx
+++ b/webapp/channels/src/components/integrations/abstract_command.test.tsx
@@ -1,16 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
+import React, {FormEvent} from 'react';
import {shallow} from 'enzyme';
import {FormattedMessage} from 'react-intl';
-import AbstractCommand from 'components/integrations/abstract_command.jsx';
+import AbstractCommand from 'components/integrations/abstract_command';
+import {TestHelper} from 'utils/test_helper';
describe('components/integrations/AbstractCommand', () => {
const header = {id: 'Header', defaultMessage: 'Header'};
const footer = {id: 'Footer', defaultMessage: 'Footer'};
const loading = {id: 'Loading', defaultMessage: 'Loading'};
+ const method: 'G' | 'P' | '' = 'G';
const command = {
id: 'r5tpgt4iepf45jt768jz84djic',
display_name: 'display_name',
@@ -20,23 +22,21 @@ describe('components/integrations/AbstractCommand', () => {
auto_complete_hint: 'auto_complete_hint',
auto_complete_desc: 'auto_complete_desc',
token: 'jb6oyqh95irpbx8fo9zmndkp1r',
- create_at: '1499722850203',
+ create_at: 1499722850203,
creator_id: '88oybd1dwfdoxpkpw1h5kpbyco',
delete_at: 0,
icon_url: 'https://google.com/icon',
- method: 'G',
+ method,
team_id: 'm5gix3oye3du8ghk4ko6h9cq7y',
update_at: 1504468859001,
url: 'https://google.com/command',
username: 'username',
};
- const team = {
- name: 'test',
- id: command.team_id,
- };
+ const team = TestHelper.getTeamMock({name: 'test', id: command.team_id});
+
const action = jest.fn().mockImplementation(
() => {
- return new Promise((resolve) => {
+ return new Promise((resolve) => {
process.nextTick(() => resolve());
});
},
@@ -47,14 +47,14 @@ describe('components/integrations/AbstractCommand', () => {
header,
footer,
loading,
- renderExtra: 'renderExtra',
+ renderExtra: {'renderExtra'}
,
serverError: '',
initialCommand: command,
action,
};
test('should match snapshot', () => {
- const wrapper = shallow(
+ const wrapper = shallow(
,
);
expect(wrapper).toMatchSnapshot();
@@ -63,7 +63,7 @@ describe('components/integrations/AbstractCommand', () => {
test('should match snapshot, displays client error', () => {
const newSeverError = 'server error';
const props = {...baseProps, serverError: newSeverError};
- const wrapper = shallow(
+ const wrapper = shallow(
,
);
@@ -75,7 +75,7 @@ describe('components/integrations/AbstractCommand', () => {
});
test('should call action function', () => {
- const wrapper = shallow(
+ const wrapper = shallow(
,
);
@@ -86,7 +86,7 @@ describe('components/integrations/AbstractCommand', () => {
});
test('should match object returned by getStateFromCommand', () => {
- const wrapper = shallow(
+ const wrapper = shallow(
,
);
@@ -109,67 +109,77 @@ describe('components/integrations/AbstractCommand', () => {
});
test('should match state when method is called', () => {
- const wrapper = shallow(
+ const wrapper = shallow(
,
);
-
const displayName = 'new display_name';
- wrapper.instance().updateDisplayName({target: {value: displayName}});
+ const displayNameEvent = {preventDefault: jest.fn(), target: {value: displayName}} as any;
+ wrapper.instance().updateDisplayName(displayNameEvent);
expect(wrapper.state('displayName')).toEqual(displayName);
const description = 'new description';
- wrapper.instance().updateDescription({target: {value: description}});
+ const descriptionEvent = {preventDefault: jest.fn(), target: {value: description}} as any;
+ wrapper.instance().updateDescription(descriptionEvent);
expect(wrapper.state('description')).toEqual(description);
const trigger = 'new trigger';
- wrapper.instance().updateTrigger({target: {value: trigger}});
+ const triggerEvent = {preventDefault: jest.fn(), target: {value: trigger}} as any;
+ wrapper.instance().updateTrigger(triggerEvent);
expect(wrapper.state('trigger')).toEqual(trigger);
const url = 'new url';
- wrapper.instance().updateUrl({target: {value: url}});
+ const urlEvent = {preventDefault: jest.fn(), target: {value: url}} as any;
+ wrapper.instance().updateUrl(urlEvent);
expect(wrapper.state('url')).toEqual(url);
- const method = 'new method';
- wrapper.instance().updateMethod({target: {value: method}});
+ const method = 'P';
+ const methodEvent = {preventDefault: jest.fn(), target: {value: method}} as any;
+ wrapper.instance().updateMethod(methodEvent);
expect(wrapper.state('method')).toEqual(method);
const username = 'new username';
- wrapper.instance().updateUsername({target: {value: username}});
+ const usernameEvent = {preventDefault: jest.fn(), target: {value: username}} as any;
+ wrapper.instance().updateUsername(usernameEvent);
expect(wrapper.state('username')).toEqual(username);
const iconUrl = 'new iconUrl';
- wrapper.instance().updateIconUrl({target: {value: iconUrl}});
+ const iconUrlEvent = {preventDefault: jest.fn(), target: {value: iconUrl}} as any;
+ wrapper.instance().updateIconUrl(iconUrlEvent);
expect(wrapper.state('iconUrl')).toEqual(iconUrl);
- wrapper.instance().updateAutocomplete({target: {checked: true}});
+ const trueUpdateAutocompleteEvent = {target: {checked: true}} as any;
+ const falseeUpdateAutocompleteEvent = {target: {checked: false}} as any;
+ wrapper.instance().updateAutocomplete(trueUpdateAutocompleteEvent);
expect(wrapper.state('autocomplete')).toEqual(true);
- wrapper.instance().updateAutocomplete({target: {checked: false}});
+ wrapper.instance().updateAutocomplete(falseeUpdateAutocompleteEvent);
expect(wrapper.state('autocomplete')).toEqual(false);
const autocompleteHint = 'new autocompleteHint';
- wrapper.instance().updateAutocompleteHint({target: {value: autocompleteHint}});
+ const autocompleteHintEvent = {preventDefault: jest.fn(), target: {value: autocompleteHint}} as any;
+ wrapper.instance().updateAutocompleteHint(autocompleteHintEvent);
expect(wrapper.state('autocompleteHint')).toEqual(autocompleteHint);
const autocompleteDescription = 'new autocompleteDescription';
- wrapper.instance().updateAutocompleteDescription({target: {value: autocompleteDescription}});
+ const autocompleteDescriptionEvent = {preventDefault: jest.fn(), target: {value: autocompleteDescription}} as any;
+ wrapper.instance().updateAutocompleteDescription(autocompleteDescriptionEvent);
expect(wrapper.state('autocompleteDescription')).toEqual(autocompleteDescription);
});
test('should match state when handleSubmit is called', () => {
const newAction = jest.fn().mockImplementation(
() => {
- return new Promise((resolve) => {
+ return new Promise((resolve) => {
process.nextTick(() => resolve());
});
},
);
const props = {...baseProps, action: newAction};
- const wrapper = shallow(
+ const wrapper = shallow(
,
);
expect(newAction).toHaveBeenCalledTimes(0);
- const evt = {preventDefault: jest.fn()};
+ const evt = {preventDefault: jest.fn()} as unknown as FormEvent;
const handleSubmit = wrapper.instance().handleSubmit;
handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(true);
diff --git a/webapp/channels/src/components/integrations/abstract_command.jsx b/webapp/channels/src/components/integrations/abstract_command.tsx
similarity index 87%
rename from webapp/channels/src/components/integrations/abstract_command.jsx
rename to webapp/channels/src/components/integrations/abstract_command.tsx
index d89e0314b4..f32f0c1751 100644
--- a/webapp/channels/src/components/integrations/abstract_command.jsx
+++ b/webapp/channels/src/components/integrations/abstract_command.tsx
@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
-import PropTypes from 'prop-types';
-import {FormattedMessage} from 'react-intl';
+import React, {ChangeEvent} from 'react';
+import {FormattedMessage, MessageDescriptor} from 'react-intl';
+
import {Link} from 'react-router-dom';
import BackstageHeader from 'components/backstage/components/backstage_header';
@@ -12,81 +12,98 @@ import * as Utils from 'utils/utils';
import FormError from 'components/form_error';
import SpinnerButton from 'components/spinner_button';
import LocalizedInput from 'components/localized_input/localized_input';
+import ExternalLink from 'components/external_link';
import {t} from 'utils/i18n';
-import ExternalLink from 'components/external_link';
+import {Command} from '@mattermost/types/integrations';
+import {Team} from '@mattermost/types/teams';
const REQUEST_POST = 'P';
const REQUEST_GET = 'G';
-export default class AbstractCommand extends React.PureComponent {
- static propTypes = {
+type Props = {
- /**
- * The current team
- */
- team: PropTypes.object.isRequired,
+ /**
+ * The current team
+ */
+ team: Team;
- /**
- * The header text to render, has id and defaultMessage
- */
- header: PropTypes.object.isRequired,
+ /**
+ * The header text to render, has id and defaultMessage
+ */
+ header: MessageDescriptor;
- /**
- * The footer text to render, has id and defaultMessage
- */
- footer: PropTypes.object.isRequired,
+ /**
+ * The footer text to render, has id and defaultMessage
+ */
+ footer: MessageDescriptor;
- /**
- * The spinner loading text to render, has id and defaultMessage
- */
- loading: PropTypes.object.isRequired,
+ /**
+ * The spinner loading text to render, has id and defaultMessage
+ */
+ loading: MessageDescriptor;
- /**
- * Any extra component/node to render
- */
- renderExtra: PropTypes.node.isRequired,
+ /**
+ * Any extra component/node to render
+ */
+ renderExtra?: JSX.Element;
- /**
- * The server error text after a failed action
- */
- serverError: PropTypes.string.isRequired,
+ /**
+ * The server error text after a failed action
+ */
+ serverError: string;
- /**
- * The Command used to set the initial state
- */
- initialCommand: PropTypes.object,
+ /**
+ * The Command used to set the initial state
+ */
+ initialCommand?: Partial;
- /**
- * The async function to run when the action button is pressed
- */
- action: PropTypes.func.isRequired,
- };
+ /**
+ * The async function to run when the action button is pressed
+ */
+ action: (command: Command) => Promise;
+}
- constructor(props) {
+type State= {
+ saving: boolean;
+ clientError: null | JSX.Element | string;
+ trigger: string;
+ displayName: string;
+ description: string;
+ url: string;
+ method: 'P' | 'G' | '';
+ username: string;
+ iconUrl: string;
+ autocomplete: boolean;
+ autocompleteHint: string;
+ autocompleteDescription: string;
+}
+
+export default class AbstractCommand extends React.PureComponent {
+ constructor(props: Props) {
super(props);
this.state = this.getStateFromCommand(this.props.initialCommand || {});
}
- getStateFromCommand = (command) => {
+ getStateFromCommand = (command: Props['initialCommand']) => {
return {
- displayName: command.display_name || '',
- description: command.description || '',
- trigger: command.trigger || '',
- url: command.url || '',
- method: command.method || REQUEST_POST,
- username: command.username || '',
- iconUrl: command.icon_url || '',
- autocomplete: command.auto_complete || false,
- autocompleteHint: command.auto_complete_hint || '',
- autocompleteDescription: command.auto_complete_desc || '',
+ displayName: command?.display_name ?? '',
+ description: command?.description ?? '',
+ trigger: command?.trigger ?? '',
+ url: command?.url ?? '',
+ method: command?.method ?? REQUEST_POST,
+ username: command?.username ?? '',
+ iconUrl: command?.icon_url ?? '',
+ autocomplete: command?.auto_complete ?? false,
+ autocompleteHint: command?.auto_complete_hint ?? '',
+ autocompleteDescription: command?.auto_complete_desc ?? '',
saving: false,
clientError: null,
};
};
- handleSubmit = (e) => {
+ handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (this.state.saving) {
@@ -113,11 +130,19 @@ export default class AbstractCommand extends React.PureComponent {
icon_url: this.state.iconUrl,
auto_complete: this.state.autocomplete,
team_id: this.props.team.id,
+ auto_complete_desc: '',
+ auto_complete_hint: '',
+ token: '',
+ create_at: 0,
+ update_at: 0,
+ delete_at: 0,
+ id: '',
+ creator_id: '',
};
if (command.auto_complete) {
- command.auto_complete_desc = this.state.autocompleteDescription;
- command.auto_complete_hint = this.state.autocompleteHint;
+ command.auto_complete_desc = this.state.autocompleteDescription ?? '';
+ command.auto_complete_hint = this.state.autocompleteHint ?? '';
}
if (!command.trigger) {
@@ -197,61 +222,64 @@ export default class AbstractCommand extends React.PureComponent {
this.props.action(command).then(() => this.setState({saving: false}));
};
- updateDisplayName = (e) => {
+ updateDisplayName = (e: ChangeEvent) => {
this.setState({
displayName: e.target.value,
});
};
- updateDescription = (e) => {
+ updateDescription = (e: ChangeEvent) => {
this.setState({
description: e.target.value,
});
};
- updateTrigger = (e) => {
+ updateTrigger = (e: ChangeEvent) => {
this.setState({
trigger: e.target.value,
});
};
- updateUrl = (e) => {
+ updateUrl = (e: ChangeEvent) => {
this.setState({
url: e.target.value,
});
};
- updateMethod = (e) => {
- this.setState({
- method: e.target.value,
- });
+ updateMethod = (e: ChangeEvent) => {
+ const methodValue = e.target.value;
+ if (methodValue === 'P' || methodValue === 'G' || methodValue === '') {
+ this.setState({
+ method: methodValue,
+ });
+ }
};
- updateUsername = (e) => {
+ updateUsername = (e: ChangeEvent) => {
this.setState({
username: e.target.value,
});
};
- updateIconUrl = (e) => {
+ updateIconUrl = (e: ChangeEvent) => {
this.setState({
iconUrl: e.target.value,
});
};
- updateAutocomplete = (e) => {
+ updateAutocomplete = (e: ChangeEvent) => {
this.setState({
autocomplete: e.target.checked,
});
};
- updateAutocompleteHint = (e) => {
+ updateAutocompleteHint = (e: ChangeEvent) => {
this.setState({
autocompleteHint: e.target.value,
});
};
- updateAutocompleteDescription = (e) => {
+ updateAutocompleteDescription = (e: ChangeEvent) => {
this.setState({
autocompleteDescription: e.target.value,
});
@@ -277,7 +305,7 @@ export default class AbstractCommand extends React.PureComponent {
diff --git a/webapp/channels/src/components/integrations/add_command/__snapshots__/add_command.test.tsx.snap b/webapp/channels/src/components/integrations/add_command/__snapshots__/add_command.test.tsx.snap
index 36741fedda..ca34aa2a56 100644
--- a/webapp/channels/src/components/integrations/add_command/__snapshots__/add_command.test.tsx.snap
+++ b/webapp/channels/src/components/integrations/add_command/__snapshots__/add_command.test.tsx.snap
@@ -3,25 +3,9 @@
exports[`components/integrations/AddCommand should match snapshot 1`] = `
{
const history = useHistory();
-
+ const {formatMessage} = useIntl();
+ const headerMessage = formatMessage({id: ('integrations.add'), defaultMessage: 'Add'}) as MessageDescriptor;
+ const footerMessage = formatMessage({id: ('add_command.save'), defaultMessage: 'Save'}) as MessageDescriptor;
+ const loadingMessage = formatMessage({id: ('add_command.saving'), defaultMessage: 'Saving...'}) as MessageDescriptor;
const [serverError, setServerError] = useState('');
const addCommand = async (command: Command) => {
@@ -55,10 +53,9 @@ const AddCommand = ({team, actions}: Props) => {
return (
diff --git a/webapp/channels/src/components/integrations/edit_command/edit_command.tsx b/webapp/channels/src/components/integrations/edit_command/edit_command.tsx
index bf35dbb13e..b2dee54ef3 100644
--- a/webapp/channels/src/components/integrations/edit_command/edit_command.tsx
+++ b/webapp/channels/src/components/integrations/edit_command/edit_command.tsx
@@ -12,7 +12,7 @@ import {getHistory} from 'utils/browser_history';
import {t} from 'utils/i18n';
import LoadingScreen from 'components/loading_screen';
import ConfirmModal from 'components/confirm_modal';
-import AbstractCommand from '../abstract_command.jsx';
+import AbstractCommand from '../abstract_command';
const HEADER = {id: t('integrations.edit'), defaultMessage: 'Edit'};
const FOOTER = {id: t('edit_command.update'), defaultMessage: 'Update'};