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

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

@@ -2,13 +2,14 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React, {type ChangeEvent} from 'react';
import {FormattedMessage} from 'react-intl'; 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', () => { describe('components/integrations/AbstractOAuthApp', () => {
const team = {name: 'test'};
const header = {id: 'Header', defaultMessage: 'Header'}; const header = {id: 'Header', defaultMessage: 'Header'};
const footer = {id: 'Footer', defaultMessage: 'Footer'}; const footer = {id: 'Footer', defaultMessage: 'Footer'};
const loading = {id: 'Loading', defaultMessage: 'Loading'}; const loading = {id: 'Loading', defaultMessage: 'Loading'};
@@ -25,19 +26,23 @@ describe('components/integrations/AbstractOAuthApp', () => {
update_at: 1501365458934, update_at: 1501365458934,
callback_urls: ['https://test.com/callback', 'https://test.com/callback2'], callback_urls: ['https://test.com/callback', 'https://test.com/callback2'],
}; };
const action = jest.fn().mockImplementation( const action = jest.fn().mockImplementation(
() => { () => {
return new Promise((resolve) => { return new Promise<void>((resolve) => {
process.nextTick(() => resolve()); process.nextTick(() => resolve());
}); });
}, },
); );
const team = TestHelper.getTeamMock({name: 'test', id: initialApp.id});
const baseProps = { const baseProps = {
team, team,
header, header,
footer, footer,
loading, loading,
renderExtra: 'renderExtra', renderExtra: <div>{'renderExtra'}</div>,
serverError: '', serverError: '',
initialApp, initialApp,
action: jest.fn(), action: jest.fn(),
@@ -82,81 +87,89 @@ describe('components/integrations/AbstractOAuthApp', () => {
test('should have correct state when updateName is called', () => { test('should have correct state when updateName is called', () => {
const props = {...baseProps, action}; const props = {...baseProps, action};
const wrapper = shallow( const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>, <AbstractOAuthApp {...props}/>,
); );
const evt = {preventDefault: jest.fn(), target: {value: 'new name'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateName({target: {value: 'new name'}}); wrapper.instance().updateName(evt);
expect(wrapper.state('name')).toEqual('new name'); expect(wrapper.state('name')).toEqual('new name');
const evt2 = {preventDefault: jest.fn(), target: {value: 'another name'}} as unknown as ChangeEvent<HTMLInputElement>;
wrapper.instance().updateName({target: {value: 'other name'}}); wrapper.instance().updateName(evt2);
expect(wrapper.state('name')).toEqual('other name'); expect(wrapper.state('name')).toEqual('another name');
}); });
test('should have correct state when updateTrusted is called', () => { test('should have correct state when updateTrusted is called', () => {
const props = {...baseProps, action}; const props = {...baseProps, action};
const wrapper = shallow( const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>, <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); 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); expect(wrapper.state('is_trusted')).toEqual(true);
}); });
test('should have correct state when updateDescription is called', () => { test('should have correct state when updateDescription is called', () => {
const props = {...baseProps, action}; const props = {...baseProps, action};
const wrapper = shallow( const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>, <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'); 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'); expect(wrapper.state('description')).toEqual('another description');
}); });
test('should have correct state when updateHomepage is called', () => { test('should have correct state when updateHomepage is called', () => {
const props = {...baseProps, action}; const props = {...baseProps, action};
const wrapper = shallow( const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>, <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'); 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'); expect(wrapper.state('homepage')).toEqual('another homepage');
}); });
test('should have correct state when updateIconUrl is called', () => { test('should have correct state when updateIconUrl is called', () => {
const props = {...baseProps, action}; const props = {...baseProps, action};
const wrapper = shallow( const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>, <AbstractOAuthApp {...props}/>,
); );
wrapper.setState({has_icon: true}); 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('icon_url')).toEqual('https://test.com/new_icon_url');
expect(wrapper.state('has_icon')).toEqual(false); expect(wrapper.state('has_icon')).toEqual(false);
wrapper.setState({has_icon: true}); 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('icon_url')).toEqual('https://test.com/another_icon_url');
expect(wrapper.state('has_icon')).toEqual(false); expect(wrapper.state('has_icon')).toEqual(false);
}); });
test('should have correct state when handleSubmit is called', () => { test('should have correct state when handleSubmit is called', () => {
const props = {...baseProps, action}; const props = {...baseProps, action};
const wrapper = shallow( const wrapper = shallow<AbstractOAuthApp>(
<AbstractOAuthApp {...props}/>, <AbstractOAuthApp {...props}/>,
); );
const newState = {saving: false, name: 'name', description: 'description', homepage: 'homepage'}; 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.setState({saving: true});
wrapper.instance().handleSubmit(evt); wrapper.instance().handleSubmit(evt);
expect(evt.preventDefault).toHaveBeenCalled(); expect(evt.preventDefault).toHaveBeenCalled();

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

@@ -1,11 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import PropTypes from 'prop-types'; import type {ChangeEvent, FormEvent} from 'react';
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import {Link} from 'react-router-dom'; 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 {Permissions} from 'mattermost-redux/constants';
import BackstageHeader from 'components/backstage/components/backstage_header'; import BackstageHeader from 'components/backstage/components/backstage_header';
@@ -15,60 +19,76 @@ import SpinnerButton from 'components/spinner_button';
import {localizeMessage} from 'utils/utils'; import {localizeMessage} from 'utils/utils';
export default class AbstractOAuthApp extends React.PureComponent { type Props = {
static propTypes = {
/** /**
* The current team * The current team
*/ */
team: PropTypes.object.isRequired, team: Team;
/** /**
* The header text to render, has id and defaultMessage * The header text to render, has id and defaultMessage
*/ */
header: PropTypes.object.isRequired, header: MessageDescriptor;
/** /**
* The footer text to render, has id and defaultMessage * The footer text to render, has id and defaultMessage
*/ */
footer: PropTypes.object.isRequired, footer: MessageDescriptor;
/** /**
* The spinner loading text to render, has id and defaultMessage * The spinner loading text to render, has id and defaultMessage
*/ */
loading: PropTypes.object.isRequired, loading: MessageDescriptor;
/** /**
* Any extra component/node to render * Any extra component/node to render
*/ */
renderExtra: PropTypes.node.isRequired, renderExtra?: JSX.Element;
/** /**
* The server error text after a failed action * The server error text after a failed action
*/ */
serverError: PropTypes.string.isRequired, serverError: string;
/** /**
* The OAuthApp used to set the initial state * The OAuthApp used to set the initial state
*/ */
initialApp: PropTypes.object, initialApp?: OAuthApp;
/** /**
* The async function to run when the action button is pressed * The async function to run when the action button is pressed
*/ */
action: PropTypes.func.isRequired, 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); super(props);
this.image = new Image(); this.image = new Image();
this.image.onload = this.imageLoaded; this.image.onload = this.imageLoaded;
this.icon_url = React.createRef(); 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 { return {
name: app.name || '', name: app.name || '',
description: app.description || '', description: app.description || '',
@@ -83,13 +103,15 @@ export default class AbstractOAuthApp extends React.PureComponent {
}; };
imageLoaded = () => { imageLoaded = () => {
this.setState({ if (this.icon_url.current?.value) {
has_icon: true, this.setState({
icon_url: this.icon_url.current.value, has_icon: true,
}); icon_url: this.icon_url.current.value,
});
}
}; };
handleSubmit = (e) => { handleSubmit = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
if (this.state.saving) { if (this.state.saving) {
@@ -173,36 +195,36 @@ export default class AbstractOAuthApp extends React.PureComponent {
description: this.state.description, description: this.state.description,
is_trusted: this.state.is_trusted, is_trusted: this.state.is_trusted,
icon_url: this.state.icon_url, icon_url: this.state.icon_url,
}; } as OAuthApp;
this.props.action(app).then(() => this.setState({saving: false})); this.props.action(app).then(() => this.setState({saving: false}));
}; };
updateName = (e) => { updateName = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
name: e.target.value, name: e.target.value,
}); });
}; };
updateTrusted = (e) => { updateTrusted = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
is_trusted: e.target.value === 'true', is_trusted: e.target.value === 'true',
}); });
}; };
updateDescription = (e) => { updateDescription = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
description: e.target.value, description: e.target.value,
}); });
}; };
updateHomepage = (e) => { updateHomepage = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
homepage: e.target.value, homepage: e.target.value,
}); });
}; };
updateIconUrl = (e) => { updateIconUrl = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
has_icon: false, has_icon: false,
icon_url: e.target.value, icon_url: e.target.value,
@@ -210,7 +232,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
this.image.src = e.target.value; this.image.src = e.target.value;
}; };
updateCallbackUrls = (e) => { updateCallbackUrls = (e: ChangeEvent<HTMLTextAreaElement>) => {
this.setState({ this.setState({
callbackUrls: e.target.value, callbackUrls: e.target.value,
}); });
@@ -315,7 +337,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
<input <input
id='name' id='name'
type='text' type='text'
maxLength='64' maxLength={64}
className='form-control' className='form-control'
value={this.state.name} value={this.state.name}
onChange={this.updateName} onChange={this.updateName}
@@ -342,7 +364,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
<input <input
id='description' id='description'
type='text' type='text'
maxLength='512' maxLength={512}
className='form-control' className='form-control'
value={this.state.description} value={this.state.description}
onChange={this.updateDescription} onChange={this.updateDescription}
@@ -369,7 +391,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
<input <input
id='homepage' id='homepage'
type='url' type='url'
maxLength='256' maxLength={256}
className='form-control' className='form-control'
value={this.state.homepage} value={this.state.homepage}
onChange={this.updateHomepage} onChange={this.updateHomepage}
@@ -397,7 +419,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
id='icon_url' id='icon_url'
ref={this.icon_url} ref={this.icon_url}
type='url' type='url'
maxLength='512' maxLength={512}
className='form-control' className='form-control'
value={this.state.icon_url} value={this.state.icon_url}
onChange={this.updateIconUrl} onChange={this.updateIconUrl}
@@ -423,8 +445,8 @@ export default class AbstractOAuthApp extends React.PureComponent {
<div className='col-md-5 col-sm-8'> <div className='col-md-5 col-sm-8'>
<textarea <textarea
id='callbackUrls' id='callbackUrls'
rows='3' rows={3}
maxLength='1024' maxLength={1024}
className='form-control' className='form-control'
value={this.state.callbackUrls} value={this.state.callbackUrls}
onChange={this.updateCallbackUrls} onChange={this.updateCallbackUrls}
@@ -455,7 +477,7 @@ export default class AbstractOAuthApp extends React.PureComponent {
className='btn btn-primary' className='btn btn-primary'
type='submit' type='submit'
spinning={this.state.saving} 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} onClick={this.handleSubmit}
id='saveOauthApp' id='saveOauthApp'
> >

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

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

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

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

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

@@ -14,7 +14,7 @@ import LoadingScreen from 'components/loading_screen';
import {getHistory} from 'utils/browser_history'; 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 HEADER = {id: 'integrations.edit', defaultMessage: 'Edit'};
const FOOTER = {id: 'update_incoming_webhook.update', defaultMessage: 'Update'}; const FOOTER = {id: 'update_incoming_webhook.update', defaultMessage: 'Update'};