[MM-47113] Migrate "components/interactive_dialog/interactive_dialog.jsx" and tests to Typescript (#25026)

Этот коммит содержится в:
Sudhanva-Nadiger
2023-10-28 12:29:50 +05:30
коммит произвёл GitHub
родитель ee3b5e6810
Коммит 1043aa5330
2 изменённых файлов: 88 добавлений и 56 удалений

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

@@ -6,14 +6,21 @@ import React from 'react';
import {Modal} from 'react-bootstrap'; import {Modal} from 'react-bootstrap';
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import type {DialogElement as TDialogElement} from '@mattermost/types/integrations';
import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store'; import mockStore from 'tests/test_store';
import EmojiMap from 'utils/emoji_map'; import EmojiMap from 'utils/emoji_map';
import InteractiveDialog from './interactive_dialog.jsx'; import type {Props} from './interactive_dialog';
import InteractiveDialog from './interactive_dialog';
const submitEvent = {
preventDefault: jest.fn(),
} as unknown as React.FormEvent<HTMLFormElement>;
describe('components/interactive_dialog/InteractiveDialog', () => { describe('components/interactive_dialog/InteractiveDialog', () => {
const baseProps = { const baseProps: Props = {
url: 'http://example.com', url: 'http://example.com',
callbackId: 'abc', callbackId: 'abc',
elements: [], elements: [],
@@ -22,9 +29,9 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
submitLabel: 'Yes', submitLabel: 'Yes',
notifyOnCancel: true, notifyOnCancel: true,
state: 'some state', state: 'some state',
onExited: () => {}, onExited: jest.fn(),
actions: { actions: {
submitInteractiveDialog: () => ({}), submitInteractiveDialog: jest.fn(),
}, },
emojiMap: new EmojiMap(new Map()), emojiMap: new EmojiMap(new Map()),
}; };
@@ -34,14 +41,12 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
const props = { const props = {
...baseProps, ...baseProps,
actions: { actions: {
submitInteractiveDialog: () => ({ submitInteractiveDialog: jest.fn().mockResolvedValue({data: {error: 'This is an error.'}}),
data: {error: 'This is an error.'},
}),
}, },
}; };
const wrapper = shallow(<InteractiveDialog {...props}/>); const wrapper = shallow<InteractiveDialog>(<InteractiveDialog {...props}/>);
await wrapper.instance().handleSubmit({preventDefault: jest.fn()}); await wrapper.instance().handleSubmit(submitEvent);
const expected = ( const expected = (
<div className='error-text'> <div className='error-text'>
@@ -52,8 +57,8 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
}); });
test('should not appear when submit does not return an error', async () => { test('should not appear when submit does not return an error', async () => {
const wrapper = shallow(<InteractiveDialog {...baseProps}/>); const wrapper = shallow<InteractiveDialog>(<InteractiveDialog {...baseProps}/>);
await wrapper.instance().handleSubmit({preventDefault: jest.fn()}); await wrapper.instance().handleSubmit(submitEvent);
expect(wrapper.find(Modal.Footer).exists('.error-text')).toBe(false); expect(wrapper.find(Modal.Footer).exists('.error-text')).toBe(false);
}); });
@@ -61,7 +66,7 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
describe('default select element in Interactive Dialog', () => { describe('default select element in Interactive Dialog', () => {
test('should be enabled by default', () => { test('should be enabled by default', () => {
const selectElement = { const selectElement: TDialogElement = {
data_source: '', data_source: '',
default: 'opt3', default: 'opt3',
display_name: 'Option Selector', display_name: 'Option Selector',
@@ -73,10 +78,15 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
{text: 'Option3', value: 'opt3'}, {text: 'Option3', value: 'opt3'},
], ],
type: 'select', type: 'select',
subtype: '',
placeholder: '',
help_text: '',
min_length: 0,
max_length: 0,
}; };
const {elements, ...rest} = baseProps; const {elements, ...rest} = baseProps;
elements.push(selectElement); elements?.push(selectElement);
const props = { const props = {
...rest, ...rest,
elements, elements,
@@ -93,19 +103,25 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
}); });
describe('bool element in Interactive Dialog', () => { describe('bool element in Interactive Dialog', () => {
const element = { const element: TDialogElement = {
data_source: '', data_source: '',
display_name: 'Boolean Selector', display_name: 'Boolean Selector',
name: 'somebool', name: 'somebool',
optional: false, optional: false,
type: 'bool', type: 'bool',
placeholder: 'Subscribe?', placeholder: 'Subscribe?',
subtype: '',
default: '',
help_text: '',
min_length: 0,
max_length: 0,
options: [],
}; };
const {elements, ...rest} = baseProps; const {elements, ...rest} = baseProps;
const props = { const props = {
...rest, ...rest,
elements: [ elements: [
...elements, ...elements || [],
element, element,
], ],
}; };
@@ -122,9 +138,9 @@ describe('components/interactive_dialog/InteractiveDialog', () => {
testCases.forEach((testCase) => test(`should interpret ${testCase.description}`, () => { testCases.forEach((testCase) => test(`should interpret ${testCase.description}`, () => {
if (testCase.default === undefined) { if (testCase.default === undefined) {
delete element.default; delete (element as any).default;
} else { } else {
element.default = testCase.default; (element as any).default = testCase.default;
} }
const store = mockStore({}); const store = mockStore({});

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

@@ -1,11 +1,13 @@
// 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 React from 'react'; import React from 'react';
import {Modal} from 'react-bootstrap'; import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import type {DialogSubmission, DialogElement as TDialogElement} from '@mattermost/types/integrations';
import type {ActionFunc} from 'mattermost-redux/types/actions';
import { import {
checkDialogElementForError, checkDialogElementForError,
checkIfErrorsMatchElements, checkIfErrorsMatchElements,
@@ -13,41 +15,48 @@ import {
import SpinnerButton from 'components/spinner_button'; import SpinnerButton from 'components/spinner_button';
import type EmojiMap from 'utils/emoji_map';
import {localizeMessage} from 'utils/utils'; import {localizeMessage} from 'utils/utils';
import DialogElement from './dialog_element'; import DialogElement from './dialog_element';
import DialogIntroductionText from './dialog_introduction_text'; import DialogIntroductionText from './dialog_introduction_text';
export default class InteractiveDialog extends React.PureComponent { export type Props = {
static propTypes = { url: string;
url: PropTypes.string.isRequired, callbackId?: string;
callbackId: PropTypes.string, elements?: TDialogElement[];
elements: PropTypes.arrayOf(PropTypes.object), title: string;
title: PropTypes.string.isRequired, introductionText?: string;
introductionText: PropTypes.string, iconUrl?: string;
iconUrl: PropTypes.string, submitLabel?: string;
submitLabel: PropTypes.string, notifyOnCancel?: boolean;
notifyOnCancel: PropTypes.bool, state?: string;
state: PropTypes.string, onExited?: () => void;
onExited: PropTypes.func, actions: {
actions: PropTypes.shape({ submitInteractiveDialog: (submission: DialogSubmission) => ActionFunc;
submitInteractiveDialog: PropTypes.func.isRequired,
}).isRequired,
emojiMap: PropTypes.object.isRequired,
}; };
emojiMap: EmojiMap;
}
constructor(props) { type State = {
show: boolean;
values: Record<string, string | number | boolean>;
error: string | null;
errors: Record<string, JSX.Element>;
submitting: boolean;
}
export default class InteractiveDialog extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props); super(props);
const values = {}; const values: Record<string, string | number | boolean> = {};
if (props.elements != null) { if (props.elements != null) {
props.elements.forEach((e) => { props.elements.forEach((e) => {
if (e.type === 'bool') { if (e.type === 'bool') {
values[e.name] = values[e.name] = String(e.default).toLowerCase() === 'true';
e.default === true ||
String(e.default).toLowerCase() === 'true';
} else { } else {
values[e.name] = e.default || null; values[e.name] = e.default ?? null;
} }
}); });
} }
@@ -61,12 +70,13 @@ export default class InteractiveDialog extends React.PureComponent {
}; };
} }
handleSubmit = async (e) => { handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const {elements} = this.props; const {elements} = this.props;
const values = this.state.values; const values = this.state.values;
const errors = {}; const errors: Record<string, JSX.Element> = {};
if (elements) { if (elements) {
elements.forEach((elem) => { elements.forEach((elem) => {
const error = checkDialogElementForError( const error = checkDialogElementForError(
@@ -93,18 +103,20 @@ export default class InteractiveDialog extends React.PureComponent {
const {url, callbackId, state} = this.props; const {url, callbackId, state} = this.props;
const dialog = { const dialog: DialogSubmission = {
url, url,
callback_id: callbackId, callback_id: callbackId ?? '',
state, state: state ?? '',
submission: values, submission: values as { [x: string]: string },
user_id: '',
channel_id: '',
team_id: '',
cancelled: false,
}; };
this.setState({submitting: true}); this.setState({submitting: true});
const {data} = await this.props.actions.submitInteractiveDialog( const {data}: any = await this.props.actions.submitInteractiveDialog(dialog) ?? {};
dialog,
);
this.setState({submitting: false}); this.setState({submitting: false});
@@ -139,11 +151,15 @@ export default class InteractiveDialog extends React.PureComponent {
const {url, callbackId, state, notifyOnCancel} = this.props; const {url, callbackId, state, notifyOnCancel} = this.props;
if (!submitted && notifyOnCancel) { if (!submitted && notifyOnCancel) {
const dialog = { const dialog: DialogSubmission = {
url, url,
callback_id: callbackId, callback_id: callbackId ?? '',
state, state: state ?? '',
cancelled: true, cancelled: true,
user_id: '',
channel_id: '',
team_id: '',
submission: {},
}; };
this.props.actions.submitInteractiveDialog(dialog); this.props.actions.submitInteractiveDialog(dialog);
@@ -152,7 +168,7 @@ export default class InteractiveDialog extends React.PureComponent {
this.setState({show: false}); this.setState({show: false});
}; };
onChange = (name, value) => { onChange = (name: string, value: string) => {
const values = {...this.state.values, [name]: value}; const values = {...this.state.values, [name]: value};
this.setState({values}); this.setState({values});
}; };
@@ -166,12 +182,13 @@ export default class InteractiveDialog extends React.PureComponent {
elements, elements,
} = this.props; } = this.props;
let submitText = ( let submitText: JSX.Element | string = (
<FormattedMessage <FormattedMessage
id='interactive_dialog.submit' id='interactive_dialog.submit'
defaultMessage='Submit' defaultMessage='Submit'
/> />
); );
if (submitLabel) { if (submitLabel) {
submitText = submitLabel; submitText = submitLabel;
} }
@@ -207,7 +224,7 @@ export default class InteractiveDialog extends React.PureComponent {
> >
<Modal.Header <Modal.Header
closeButton={true} closeButton={true}
style={{borderBottom: elements == null && '0px'}} style={{borderBottom: elements == null ? '0px' : undefined}}
> >
<Modal.Title <Modal.Title
componentClass='h1' componentClass='h1'
@@ -239,7 +256,6 @@ export default class InteractiveDialog extends React.PureComponent {
helpText={e.help_text} helpText={e.help_text}
errorText={this.state.errors[e.name]} errorText={this.state.errors[e.name]}
placeholder={e.placeholder} placeholder={e.placeholder}
minLength={e.min_length}
maxLength={e.max_length} maxLength={e.max_length}
dataSource={e.data_source} dataSource={e.data_source}
optional={e.optional} optional={e.optional}