[MM-47113] Migrate "components/interactive_dialog/interactive_dialog.jsx" and tests to Typescript (#25026)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ee3b5e6810
Коммит
1043aa5330
@@ -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}
|
||||||
Ссылка в новой задаче
Block a user