diff --git a/webapp/channels/src/components/integrations/__snapshots__/abstract_oauth_app.test.jsx.snap b/webapp/channels/src/components/integrations/__snapshots__/abstract_oauth_app.test.tsx.snap similarity index 97% rename from webapp/channels/src/components/integrations/__snapshots__/abstract_oauth_app.test.jsx.snap rename to webapp/channels/src/components/integrations/__snapshots__/abstract_oauth_app.test.tsx.snap index 4c8da97d1f..a52c83bc56 100644 --- a/webapp/channels/src/components/integrations/__snapshots__/abstract_oauth_app.test.jsx.snap +++ b/webapp/channels/src/components/integrations/__snapshots__/abstract_oauth_app.test.tsx.snap @@ -113,7 +113,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot 1`] = ` @@ -296,7 +296,9 @@ https://test.com/callback2" id="Footer" /> - renderExtra +
+ renderExtra +
@@ -416,7 +418,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display diff --git a/webapp/channels/src/components/integrations/abstract_oauth_app.test.jsx b/webapp/channels/src/components/integrations/abstract_oauth_app.test.tsx similarity index 71% rename from webapp/channels/src/components/integrations/abstract_oauth_app.test.jsx rename to webapp/channels/src/components/integrations/abstract_oauth_app.test.tsx index 649c556b2d..40068dfaca 100644 --- a/webapp/channels/src/components/integrations/abstract_oauth_app.test.jsx +++ b/webapp/channels/src/components/integrations/abstract_oauth_app.test.tsx @@ -2,13 +2,14 @@ // See LICENSE.txt for license information. import {shallow} from 'enzyme'; -import React from 'react'; +import React, {type ChangeEvent} from 'react'; import {FormattedMessage} from 'react-intl'; -import AbstractOAuthApp from 'components/integrations/abstract_oauth_app.jsx'; +import AbstractOAuthApp from 'components/integrations/abstract_oauth_app'; + +import {TestHelper} from 'utils/test_helper'; describe('components/integrations/AbstractOAuthApp', () => { - const team = {name: 'test'}; const header = {id: 'Header', defaultMessage: 'Header'}; const footer = {id: 'Footer', defaultMessage: 'Footer'}; const loading = {id: 'Loading', defaultMessage: 'Loading'}; @@ -25,19 +26,23 @@ describe('components/integrations/AbstractOAuthApp', () => { update_at: 1501365458934, callback_urls: ['https://test.com/callback', 'https://test.com/callback2'], }; + const action = jest.fn().mockImplementation( () => { - return new Promise((resolve) => { + return new Promise((resolve) => { process.nextTick(() => resolve()); }); }, ); + + const team = TestHelper.getTeamMock({name: 'test', id: initialApp.id}); + const baseProps = { team, header, footer, loading, - renderExtra: 'renderExtra', + renderExtra:
{'renderExtra'}
, serverError: '', initialApp, action: jest.fn(), @@ -82,81 +87,89 @@ describe('components/integrations/AbstractOAuthApp', () => { test('should have correct state when updateName is called', () => { const props = {...baseProps, action}; - const wrapper = shallow( + const wrapper = shallow( , ); - - wrapper.instance().updateName({target: {value: 'new name'}}); + const evt = {preventDefault: jest.fn(), target: {value: 'new name'}} as unknown as ChangeEvent; + wrapper.instance().updateName(evt); expect(wrapper.state('name')).toEqual('new name'); - - wrapper.instance().updateName({target: {value: 'other name'}}); - expect(wrapper.state('name')).toEqual('other name'); + const evt2 = {preventDefault: jest.fn(), target: {value: 'another name'}} as unknown as ChangeEvent; + wrapper.instance().updateName(evt2); + expect(wrapper.state('name')).toEqual('another name'); }); test('should have correct state when updateTrusted is called', () => { const props = {...baseProps, action}; - const wrapper = shallow( + const wrapper = shallow( , ); - wrapper.instance().updateTrusted({target: {value: 'false'}}); + const evt = {preventDefault: jest.fn(), target: {value: 'false'}} as unknown as ChangeEvent; + wrapper.instance().updateTrusted(evt); expect(wrapper.state('is_trusted')).toEqual(false); - wrapper.instance().updateTrusted({target: {value: 'true'}}); + const evt2 = {preventDefault: jest.fn(), target: {value: 'true'}} as unknown as ChangeEvent; + wrapper.instance().updateTrusted(evt2); expect(wrapper.state('is_trusted')).toEqual(true); }); test('should have correct state when updateDescription is called', () => { const props = {...baseProps, action}; - const wrapper = shallow( + const wrapper = shallow( , ); - wrapper.instance().updateDescription({target: {value: 'new description'}}); + const evt = {preventDefault: jest.fn(), target: {value: 'new description'}} as unknown as ChangeEvent; + wrapper.instance().updateDescription(evt); expect(wrapper.state('description')).toEqual('new description'); - wrapper.instance().updateDescription({target: {value: 'another description'}}); + const evt2 = {preventDefault: jest.fn(), target: {value: 'another description'}} as unknown as ChangeEvent; + wrapper.instance().updateDescription(evt2); expect(wrapper.state('description')).toEqual('another description'); }); test('should have correct state when updateHomepage is called', () => { const props = {...baseProps, action}; - const wrapper = shallow( + const wrapper = shallow( , ); - wrapper.instance().updateHomepage({target: {value: 'new homepage'}}); + const evt = {preventDefault: jest.fn(), target: {value: 'new homepage'}} as unknown as ChangeEvent; + wrapper.instance().updateHomepage(evt); expect(wrapper.state('homepage')).toEqual('new homepage'); - wrapper.instance().updateHomepage({target: {value: 'another homepage'}}); + const evt2 = {preventDefault: jest.fn(), target: {value: 'another homepage'}} as unknown as ChangeEvent; + wrapper.instance().updateHomepage(evt2); expect(wrapper.state('homepage')).toEqual('another homepage'); }); test('should have correct state when updateIconUrl is called', () => { const props = {...baseProps, action}; - const wrapper = shallow( + const wrapper = shallow( , ); wrapper.setState({has_icon: true}); - wrapper.instance().updateIconUrl({target: {value: 'https://test.com/new_icon_url'}}); + const evt = {preventDefault: jest.fn(), target: {value: 'https://test.com/new_icon_url'}} as unknown as ChangeEvent; + wrapper.instance().updateIconUrl(evt); expect(wrapper.state('icon_url')).toEqual('https://test.com/new_icon_url'); expect(wrapper.state('has_icon')).toEqual(false); wrapper.setState({has_icon: true}); - wrapper.instance().updateIconUrl({target: {value: 'https://test.com/another_icon_url'}}); + const evt2 = {preventDefault: jest.fn(), target: {value: 'https://test.com/another_icon_url'}} as unknown as ChangeEvent; + wrapper.instance().updateIconUrl(evt2); expect(wrapper.state('icon_url')).toEqual('https://test.com/another_icon_url'); expect(wrapper.state('has_icon')).toEqual(false); }); test('should have correct state when handleSubmit is called', () => { const props = {...baseProps, action}; - const wrapper = shallow( + const wrapper = shallow( , ); const newState = {saving: false, name: 'name', description: 'description', homepage: 'homepage'}; - const evt = {preventDefault: jest.fn()}; + const evt = {preventDefault: jest.fn()} as any; wrapper.setState({saving: true}); wrapper.instance().handleSubmit(evt); expect(evt.preventDefault).toHaveBeenCalled(); diff --git a/webapp/channels/src/components/integrations/abstract_oauth_app.jsx b/webapp/channels/src/components/integrations/abstract_oauth_app.tsx similarity index 87% rename from webapp/channels/src/components/integrations/abstract_oauth_app.jsx rename to webapp/channels/src/components/integrations/abstract_oauth_app.tsx index e985b54039..bfe3eb738c 100644 --- a/webapp/channels/src/components/integrations/abstract_oauth_app.jsx +++ b/webapp/channels/src/components/integrations/abstract_oauth_app.tsx @@ -1,11 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import PropTypes from 'prop-types'; +import type {ChangeEvent, FormEvent} from 'react'; import React from 'react'; import {FormattedMessage} from 'react-intl'; +import type {MessageDescriptor} from 'react-intl'; import {Link} from 'react-router-dom'; +import type {OAuthApp} from '@mattermost/types/integrations'; +import type {Team} from '@mattermost/types/teams'; + import {Permissions} from 'mattermost-redux/constants'; import BackstageHeader from 'components/backstage/components/backstage_header'; @@ -15,60 +19,76 @@ import SpinnerButton from 'components/spinner_button'; import {localizeMessage} from 'utils/utils'; -export default class AbstractOAuthApp 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 OAuthApp used to set the initial state - */ - initialApp: PropTypes.object, + /** + * The OAuthApp used to set the initial state + */ + initialApp?: OAuthApp; - /** - * 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: (app: OAuthApp) => Promise; - constructor(props) { +} + +type State = { + name: string; + description: string; + homepage: string; + icon_url: string; + callbackUrls: string; + is_trusted: boolean; + has_icon: boolean; + saving: boolean; + clientError: JSX.Element | null | string; +}; + +export default class AbstractOAuthApp extends React.PureComponent { + private image: HTMLImageElement; + private icon_url: React.RefObject; + + constructor(props: Props) { super(props); this.image = new Image(); this.image.onload = this.imageLoaded; this.icon_url = React.createRef(); - this.state = this.getStateFromApp(this.props.initialApp || {}); + this.state = this.getStateFromApp(this.props.initialApp || {} as OAuthApp); } - getStateFromApp = (app) => { + getStateFromApp = (app: OAuthApp) => { return { name: app.name || '', description: app.description || '', @@ -83,13 +103,15 @@ export default class AbstractOAuthApp extends React.PureComponent { }; imageLoaded = () => { - this.setState({ - has_icon: true, - icon_url: this.icon_url.current.value, - }); + if (this.icon_url.current?.value) { + this.setState({ + has_icon: true, + icon_url: this.icon_url.current.value, + }); + } }; - handleSubmit = (e) => { + handleSubmit = (e: FormEvent) => { e.preventDefault(); if (this.state.saving) { @@ -173,36 +195,36 @@ export default class AbstractOAuthApp extends React.PureComponent { description: this.state.description, is_trusted: this.state.is_trusted, icon_url: this.state.icon_url, - }; + } as OAuthApp; this.props.action(app).then(() => this.setState({saving: false})); }; - updateName = (e) => { + updateName = (e: ChangeEvent) => { this.setState({ name: e.target.value, }); }; - updateTrusted = (e) => { + updateTrusted = (e: ChangeEvent) => { this.setState({ is_trusted: e.target.value === 'true', }); }; - updateDescription = (e) => { + updateDescription = (e: ChangeEvent) => { this.setState({ description: e.target.value, }); }; - updateHomepage = (e) => { + updateHomepage = (e: ChangeEvent) => { this.setState({ homepage: e.target.value, }); }; - updateIconUrl = (e) => { + updateIconUrl = (e: ChangeEvent) => { this.setState({ has_icon: false, icon_url: e.target.value, @@ -210,7 +232,7 @@ export default class AbstractOAuthApp extends React.PureComponent { this.image.src = e.target.value; }; - updateCallbackUrls = (e) => { + updateCallbackUrls = (e: ChangeEvent) => { this.setState({ callbackUrls: e.target.value, }); @@ -315,7 +337,7 @@ export default class AbstractOAuthApp extends React.PureComponent {