From 70d0a0fe70d5c0ba7b79465b0747c4cfad92f9bb Mon Sep 17 00:00:00 2001 From: Sai Deepesh Date: Sun, 2 Jul 2023 12:25:10 +0530 Subject: [PATCH] [MM-47090]: Migrate "components/integrations/abstract_command.jsx" to Typescript (#23325) --- ...sx.snap => abstract_command.test.tsx.snap} | 36 ++-- ...and.test.jsx => abstract_command.test.tsx} | 72 ++++--- ...tract_command.jsx => abstract_command.tsx} | 194 ++++++++++-------- .../__snapshots__/add_command.test.tsx.snap | 22 +- .../integrations/add_command/add_command.tsx | 21 +- .../edit_command/edit_command.tsx | 2 +- 6 files changed, 185 insertions(+), 162 deletions(-) rename webapp/channels/src/components/integrations/__snapshots__/{abstract_command.test.jsx.snap => abstract_command.test.tsx.snap} (98%) rename webapp/channels/src/components/integrations/{abstract_command.test.jsx => abstract_command.test.tsx} (76%) rename webapp/channels/src/components/integrations/{abstract_command.jsx => abstract_command.tsx} (87%) 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 bddce4b637bab..82f849c5a06bb 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 6badf0741fda2..1b8630c3657d9 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 d89e0314b42ea..f32f0c1751407 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 = { - - /** - * 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 spinner loading text to render, has id and defaultMessage - */ - loading: PropTypes.object.isRequired, - - /** - * Any extra component/node to render - */ - renderExtra: PropTypes.node.isRequired, - - /** - * The server error text after a failed action - */ - serverError: PropTypes.string.isRequired, - - /** - * The Command used to set the initial state - */ - initialCommand: PropTypes.object, - - /** - * The async function to run when the action button is pressed - */ - action: PropTypes.func.isRequired, - }; +type Props = { + + /** + * The current team + */ + team: Team; + + /** + * The header text to render, has id and defaultMessage + */ + header: MessageDescriptor; + + /** + * The footer text to render, has id and defaultMessage + */ + footer: MessageDescriptor; + + /** + * The spinner loading text to render, has id and defaultMessage + */ + loading: MessageDescriptor; + + /** + * Any extra component/node to render + */ + renderExtra?: JSX.Element; + + /** + * The server error text after a failed action + */ + serverError: string; + + /** + * The Command used to set the initial state + */ + initialCommand?: Partial; + + /** + * The async function to run when the action button is pressed + */ + action: (command: Command) => Promise; +} + +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; +} - constructor(props) { +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 36741fedda46c..ca34aa2a56e59 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 bf35dbb13e698..b2dee54ef306c 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'};