[MM-47090]: Migrate "components/integrations/abstract_command.jsx" to Typescript (#23325)

Этот коммит содержится в:
Sai Deepesh
2023-07-02 12:25:10 +05:30
коммит произвёл GitHub
родитель 8abc8ed65c
Коммит 70d0a0fe70
6 изменённых файлов: 177 добавлений и 154 удалений

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

@@ -43,7 +43,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<input
className="form-control"
id="displayName"
maxLength="64"
maxLength={64}
onChange={[Function]}
type="text"
value="display_name"
@@ -76,7 +76,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<input
className="form-control"
id="description"
maxLength="128"
maxLength={128}
onChange={[Function]}
type="text"
value="description"
@@ -177,7 +177,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<LocalizedInput
className="form-control"
id="url"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
placeholder={
Object {
@@ -258,7 +258,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<LocalizedInput
className="form-control"
id="username"
maxLength="64"
maxLength={64}
onChange={[Function]}
placeholder={
Object {
@@ -297,7 +297,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<LocalizedInput
className="form-control"
id="iconUrl"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
placeholder={
Object {
@@ -367,7 +367,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<LocalizedInput
className="form-control"
id="autocompleteHint"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
placeholder={
Object {
@@ -406,7 +406,7 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
<LocalizedInput
className="form-control"
id="description"
maxLength="128"
maxLength={128}
onChange={[Function]}
placeholder={
Object {
@@ -462,7 +462,9 @@ exports[`components/integrations/AbstractCommand should match snapshot 1`] = `
id="Footer"
/>
</SpinnerButton>
renderExtra
<div>
renderExtra
</div>
</div>
</form>
</div>
@@ -512,7 +514,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<input
className="form-control"
id="displayName"
maxLength="64"
maxLength={64}
onChange={[Function]}
type="text"
value="display_name"
@@ -545,7 +547,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<input
className="form-control"
id="description"
maxLength="128"
maxLength={128}
onChange={[Function]}
type="text"
value="description"
@@ -646,7 +648,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<LocalizedInput
className="form-control"
id="url"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
placeholder={
Object {
@@ -727,7 +729,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<LocalizedInput
className="form-control"
id="username"
maxLength="64"
maxLength={64}
onChange={[Function]}
placeholder={
Object {
@@ -766,7 +768,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<LocalizedInput
className="form-control"
id="iconUrl"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
placeholder={
Object {
@@ -836,7 +838,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<LocalizedInput
className="form-control"
id="autocompleteHint"
maxLength="1024"
maxLength={1024}
onChange={[Function]}
placeholder={
Object {
@@ -875,7 +877,7 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
<LocalizedInput
className="form-control"
id="description"
maxLength="128"
maxLength={128}
onChange={[Function]}
placeholder={
Object {
@@ -934,7 +936,9 @@ exports[`components/integrations/AbstractCommand should match snapshot, displays
id="Footer"
/>
</SpinnerButton>
renderExtra
<div>
renderExtra
</div>
</div>
</form>
</div>

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

@@ -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<void>((resolve) => {
process.nextTick(() => resolve());
});
},
@@ -47,14 +47,14 @@ describe('components/integrations/AbstractCommand', () => {
header,
footer,
loading,
renderExtra: 'renderExtra',
renderExtra: <div>{'renderExtra'}</div>,
serverError: '',
initialCommand: command,
action,
};
test('should match snapshot', () => {
const wrapper = shallow(
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...baseProps}/>,
);
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<AbstractCommand>(
<AbstractCommand {...props}/>,
);
@@ -75,7 +75,7 @@ describe('components/integrations/AbstractCommand', () => {
});
test('should call action function', () => {
const wrapper = shallow(
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...baseProps}/>,
);
@@ -86,7 +86,7 @@ describe('components/integrations/AbstractCommand', () => {
});
test('should match object returned by getStateFromCommand', () => {
const wrapper = shallow(
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...baseProps}/>,
);
@@ -109,67 +109,77 @@ describe('components/integrations/AbstractCommand', () => {
});
test('should match state when method is called', () => {
const wrapper = shallow(
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...baseProps}/>,
);
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<void>((resolve) => {
process.nextTick(() => resolve());
});
},
);
const props = {...baseProps, action: newAction};
const wrapper = shallow(
const wrapper = shallow<AbstractCommand>(
<AbstractCommand {...props}/>,
);
expect(newAction).toHaveBeenCalledTimes(0);
const evt = {preventDefault: jest.fn()};
const evt = {preventDefault: jest.fn()} as unknown as FormEvent<Element>;
const handleSubmit = wrapper.instance().handleSubmit;
handleSubmit(evt);
expect(wrapper.state('saving')).toEqual(true);

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

@@ -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<Command>;
/**
* 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<void>;
}
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<Props, State> {
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<HTMLInputElement>) => {
this.setState({
displayName: e.target.value,
});
};
updateDescription = (e) => {
updateDescription = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
description: e.target.value,
});
};
updateTrigger = (e) => {
updateTrigger = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
trigger: e.target.value,
});
};
updateUrl = (e) => {
updateUrl = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
url: e.target.value,
});
};
updateMethod = (e) => {
this.setState({
method: e.target.value,
});
updateMethod = (e: ChangeEvent<HTMLSelectElement>) => {
const methodValue = e.target.value;
if (methodValue === 'P' || methodValue === 'G' || methodValue === '') {
this.setState({
method: methodValue,
});
}
};
updateUsername = (e) => {
updateUsername = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
username: e.target.value,
});
};
updateIconUrl = (e) => {
updateIconUrl = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
iconUrl: e.target.value,
});
};
updateAutocomplete = (e) => {
updateAutocomplete = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
autocomplete: e.target.checked,
});
};
updateAutocompleteHint = (e) => {
updateAutocompleteHint = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
autocompleteHint: e.target.value,
});
};
updateAutocompleteDescription = (e) => {
updateAutocompleteDescription = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
autocompleteDescription: e.target.value,
});
@@ -277,7 +305,7 @@ export default class AbstractCommand extends React.PureComponent {
<LocalizedInput
id='autocompleteHint'
type='text'
maxLength='1024'
maxLength={1024}
className='form-control'
value={this.state.autocompleteHint}
onChange={this.updateAutocompleteHint}
@@ -308,7 +336,7 @@ export default class AbstractCommand extends React.PureComponent {
<LocalizedInput
id='description'
type='text'
maxLength='128'
maxLength={128}
className='form-control'
value={this.state.autocompleteDescription}
onChange={this.updateAutocompleteDescription}
@@ -358,7 +386,7 @@ export default class AbstractCommand extends React.PureComponent {
<input
id='displayName'
type='text'
maxLength='64'
maxLength={64}
className='form-control'
value={this.state.displayName}
onChange={this.updateDisplayName}
@@ -385,7 +413,7 @@ export default class AbstractCommand extends React.PureComponent {
<input
id='description'
type='text'
maxLength='128'
maxLength={128}
className='form-control'
value={this.state.description}
onChange={this.updateDescription}
@@ -465,7 +493,7 @@ export default class AbstractCommand extends React.PureComponent {
<LocalizedInput
id='url'
type='text'
maxLength='1024'
maxLength={1024}
className='form-control'
value={this.state.url}
onChange={this.updateUrl}
@@ -525,7 +553,7 @@ export default class AbstractCommand extends React.PureComponent {
<LocalizedInput
id='username'
type='text'
maxLength='64'
maxLength={64}
className='form-control'
value={this.state.username}
onChange={this.updateUsername}
@@ -553,7 +581,7 @@ export default class AbstractCommand extends React.PureComponent {
<LocalizedInput
id='iconUrl'
type='text'
maxLength='1024'
maxLength={1024}
className='form-control'
value={this.state.iconUrl}
onChange={this.updateIconUrl}
@@ -612,7 +640,7 @@ export default class AbstractCommand extends React.PureComponent {
className='btn btn-primary'
type='submit'
spinning={this.state.saving}
spinningText={Utils.localizeMessage(this.props.loading.id, this.props.loading.defaultMessage)}
spinningText={Utils.localizeMessage(this.props.loading?.id ?? '', this.props.loading?.defaultMessage as string)}
onClick={this.handleSubmit}
id='saveCommand'
>

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

@@ -3,25 +3,9 @@
exports[`components/integrations/AddCommand should match snapshot 1`] = `
<AbstractCommand
action={[Function]}
footer={
Object {
"defaultMessage": "Save",
"id": "add_command.save",
}
}
header={
Object {
"defaultMessage": "Add",
"id": "integrations.add",
}
}
loading={
Object {
"defaultMessage": "Saving...",
"id": "add_command.saving",
}
}
renderExtra=""
footer="Save"
header="Add"
loading="Saving..."
serverError=""
team={
Object {

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

@@ -3,19 +3,14 @@
import React, {useState} from 'react';
import {useHistory} from 'react-router-dom';
import {useIntl, MessageDescriptor} from 'react-intl';
import {Command} from '@mattermost/types/integrations';
import {Team} from '@mattermost/types/teams';
import {ActionResult} from 'mattermost-redux/types/actions.js';
import {t} from 'utils/i18n';
import AbstractCommand from '../abstract_command.jsx';
const HEADER = {id: t('integrations.add'), defaultMessage: 'Add'};
const FOOTER = {id: t('add_command.save'), defaultMessage: 'Save'};
const LOADING = {id: t('add_command.saving'), defaultMessage: 'Saving...'};
import AbstractCommand from '../abstract_command';
export type Props = {
@@ -35,7 +30,10 @@ export type Props = {
const AddCommand = ({team, actions}: Props) => {
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 (
<AbstractCommand
team={team}
header={HEADER}
footer={FOOTER}
loading={LOADING}
renderExtra={''}
header={headerMessage}
footer={footerMessage}
loading={loadingMessage}
action={addCommand}
serverError={serverError}
/>

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

@@ -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'};