[MM-47094] Migrate "components/integrations/abstract_oauth_app.jsx" and tests to Typescript (#24529)

Этот коммит содержится в:
Sai Deepesh
2023-10-12 21:26:54 +05:30
коммит произвёл GitHub
родитель 195927eb4a
Коммит 1fe2295c70
6 изменённых файлов: 138 добавлений и 101 удалений

Просмотреть файл

@@ -113,7 +113,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot 1`] = `
<input
className="form-control"
id="name"
maxLength="64"
maxLength={64}
onChange={[Function]}
type="text"
value="testApp"
@@ -146,7 +146,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot 1`] = `
<input
className="form-control"
id="description"
maxLength="512"
maxLength={512}
onChange={[Function]}
type="text"
value="testing"
@@ -179,7 +179,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot 1`] = `
<input
className="form-control"
id="homepage"
maxLength="256"
maxLength={256}
onChange={[Function]}
type="url"
value="https://test.com"
@@ -212,7 +212,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot 1`] = `
<input
className="form-control"
id="icon_url"
maxLength="512"
maxLength={512}
onChange={[Function]}
type="url"
value="https://test.com/icon"
@@ -245,9 +245,9 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot 1`] = `
<textarea
className="form-control"
id="callbackUrls"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
rows="3"
rows={3}
value="https://test.com/callback
https://test.com/callback2"
/>
@@ -296,7 +296,9 @@ https://test.com/callback2"
id="Footer"
/>
</SpinnerButton>
renderExtra
<div>
renderExtra
</div>
</div>
</form>
</div>
@@ -416,7 +418,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display
<input
className="form-control"
id="name"
maxLength="64"
maxLength={64}
onChange={[Function]}
type="text"
value="testApp"
@@ -449,7 +451,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display
<input
className="form-control"
id="description"
maxLength="512"
maxLength={512}
onChange={[Function]}
type="text"
value="testing"
@@ -482,7 +484,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display
<input
className="form-control"
id="homepage"
maxLength="256"
maxLength={256}
onChange={[Function]}
type="url"
value="https://test.com"
@@ -515,7 +517,7 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display
<input
className="form-control"
id="icon_url"
maxLength="512"
maxLength={512}
onChange={[Function]}
type="url"
value="https://test.com/icon"
@@ -548,9 +550,9 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display
<textarea
className="form-control"
id="callbackUrls"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
rows="3"
rows={3}
value=""
/>
<div
@@ -601,7 +603,9 @@ exports[`components/integrations/AbstractOAuthApp should match snapshot, display
id="Footer"
/>
</SpinnerButton>
renderExtra
<div>
renderExtra
</div>
</div>
</form>
</div>

Просмотреть файл

@@ -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<void>((resolve) => {
process.nextTick(() => resolve());
});
},
);
const team = TestHelper.getTeamMock({name: 'test', id: initialApp.id});
const baseProps = {
team,
header,
footer,
loading,
renderExtra: 'renderExtra',
renderExtra: <div>{'renderExtra'}</div>,
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<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
wrapper.instance().updateName({target: {value: 'new name'}});
const evt = {preventDefault: jest.fn(), target: {value: 'new name'}} as unknown as ChangeEvent<HTMLInputElement>;
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<HTMLInputElement>;
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<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
wrapper.instance().updateTrusted({target: {value: 'false'}});
const evt = {preventDefault: jest.fn(), target: {value: 'false'}} as unknown as ChangeEvent<HTMLInputElement>;
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<HTMLInputElement>;
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<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
wrapper.instance().updateDescription({target: {value: 'new description'}});
const evt = {preventDefault: jest.fn(), target: {value: 'new description'}} as unknown as ChangeEvent<HTMLInputElement>;
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<HTMLInputElement>;
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<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
wrapper.instance().updateHomepage({target: {value: 'new homepage'}});
const evt = {preventDefault: jest.fn(), target: {value: 'new homepage'}} as unknown as ChangeEvent<HTMLInputElement>;
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<HTMLInputElement>;
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<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
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<HTMLInputElement>;
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<HTMLInputElement>;
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<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>,
);
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();

Просмотреть файл

@@ -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<void>;
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<Props, State> {
private image: HTMLImageElement;
private icon_url: React.RefObject<HTMLInputElement>;
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<HTMLInputElement>) => {
this.setState({
name: e.target.value,
});
};
updateTrusted = (e) => {
updateTrusted = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
is_trusted: e.target.value === 'true',
});
};
updateDescription = (e) => {
updateDescription = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
description: e.target.value,
});
};
updateHomepage = (e) => {
updateHomepage = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
homepage: e.target.value,
});
};
updateIconUrl = (e) => {
updateIconUrl = (e: ChangeEvent<HTMLInputElement>) => {
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<HTMLTextAreaElement>) => {
this.setState({
callbackUrls: e.target.value,
});
@@ -315,7 +337,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
<input
id='name'
type='text'
maxLength='64'
maxLength={64}
className='form-control'
value={this.state.name}
onChange={this.updateName}
@@ -342,7 +364,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
<input
id='description'
type='text'
maxLength='512'
maxLength={512}
className='form-control'
value={this.state.description}
onChange={this.updateDescription}
@@ -369,7 +391,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
<input
id='homepage'
type='url'
maxLength='256'
maxLength={256}
className='form-control'
value={this.state.homepage}
onChange={this.updateHomepage}
@@ -397,7 +419,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
id='icon_url'
ref={this.icon_url}
type='url'
maxLength='512'
maxLength={512}
className='form-control'
value={this.state.icon_url}
onChange={this.updateIconUrl}
@@ -423,8 +445,8 @@ export default class AbstractOAuthApp extends React.PureComponent {
<div className='col-md-5 col-sm-8'>
<textarea
id='callbackUrls'
rows='3'
maxLength='1024'
rows={3}
maxLength={1024}
className='form-control'
value={this.state.callbackUrls}
onChange={this.updateCallbackUrls}
@@ -455,7 +477,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
className='btn btn-primary'
type='submit'
spinning={this.state.saving}
spinningText={localizeMessage(this.props.loading.id, this.props.loading.defaultMessage)}
spinningText={localizeMessage(this.props.loading?.id || '', (this.props.loading?.defaultMessage || '') as string)}
onClick={this.handleSubmit}
id='saveOauthApp'
>

Просмотреть файл

@@ -21,7 +21,6 @@ exports[`components/integrations/AddOAuthApp should match snapshot 1`] = `
"id": "installed_oauth_apps.saving",
}
}
renderExtra=""
serverError=""
team={
Object {

Просмотреть файл

@@ -11,7 +11,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions.js';
import {t} from 'utils/i18n';
import AbstractOAuthApp from '../abstract_oauth_app.jsx';
import AbstractOAuthApp from '../abstract_oauth_app';
const HEADER = {id: t('add_oauth_app.header'), defaultMessage: 'Add'};
const FOOTER = {id: t('installed_oauth_apps.save'), defaultMessage: 'Save'};
@@ -58,7 +58,6 @@ const AddOAuthApp = ({team, actions}: Props): JSX.Element => {
header={HEADER}
footer={FOOTER}
loading={LOADING}
renderExtra={''}
action={addOAuthApp}
serverError={serverError}
/>

Просмотреть файл

@@ -14,7 +14,7 @@ import LoadingScreen from 'components/loading_screen';
import {getHistory} from 'utils/browser_history';
import AbstractOAuthApp from '../abstract_oauth_app.jsx';
import AbstractOAuthApp from '../abstract_oauth_app';
const HEADER = {id: 'integrations.edit', defaultMessage: 'Edit'};
const FOOTER = {id: 'update_incoming_webhook.update', defaultMessage: 'Update'};