diff --git a/webapp/channels/src/components/interactive_dialog/interactive_dialog.test.jsx b/webapp/channels/src/components/interactive_dialog/interactive_dialog.test.tsx similarity index 74% rename from webapp/channels/src/components/interactive_dialog/interactive_dialog.test.jsx rename to webapp/channels/src/components/interactive_dialog/interactive_dialog.test.tsx index 1ded497382..674c764393 100644 --- a/webapp/channels/src/components/interactive_dialog/interactive_dialog.test.jsx +++ b/webapp/channels/src/components/interactive_dialog/interactive_dialog.test.tsx @@ -6,14 +6,21 @@ import React from 'react'; import {Modal} from 'react-bootstrap'; import {Provider} from 'react-redux'; +import type {DialogElement as TDialogElement} from '@mattermost/types/integrations'; + import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import mockStore from 'tests/test_store'; 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; describe('components/interactive_dialog/InteractiveDialog', () => { - const baseProps = { + const baseProps: Props = { url: 'http://example.com', callbackId: 'abc', elements: [], @@ -22,9 +29,9 @@ describe('components/interactive_dialog/InteractiveDialog', () => { submitLabel: 'Yes', notifyOnCancel: true, state: 'some state', - onExited: () => {}, + onExited: jest.fn(), actions: { - submitInteractiveDialog: () => ({}), + submitInteractiveDialog: jest.fn(), }, emojiMap: new EmojiMap(new Map()), }; @@ -34,14 +41,12 @@ describe('components/interactive_dialog/InteractiveDialog', () => { const props = { ...baseProps, actions: { - submitInteractiveDialog: () => ({ - data: {error: 'This is an error.'}, - }), + submitInteractiveDialog: jest.fn().mockResolvedValue({data: {error: 'This is an error.'}}), }, }; - const wrapper = shallow(); + const wrapper = shallow(); - await wrapper.instance().handleSubmit({preventDefault: jest.fn()}); + await wrapper.instance().handleSubmit(submitEvent); const expected = (
@@ -52,8 +57,8 @@ describe('components/interactive_dialog/InteractiveDialog', () => { }); test('should not appear when submit does not return an error', async () => { - const wrapper = shallow(); - await wrapper.instance().handleSubmit({preventDefault: jest.fn()}); + const wrapper = shallow(); + await wrapper.instance().handleSubmit(submitEvent); 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', () => { test('should be enabled by default', () => { - const selectElement = { + const selectElement: TDialogElement = { data_source: '', default: 'opt3', display_name: 'Option Selector', @@ -73,10 +78,15 @@ describe('components/interactive_dialog/InteractiveDialog', () => { {text: 'Option3', value: 'opt3'}, ], type: 'select', + subtype: '', + placeholder: '', + help_text: '', + min_length: 0, + max_length: 0, }; const {elements, ...rest} = baseProps; - elements.push(selectElement); + elements?.push(selectElement); const props = { ...rest, elements, @@ -93,19 +103,25 @@ describe('components/interactive_dialog/InteractiveDialog', () => { }); describe('bool element in Interactive Dialog', () => { - const element = { + const element: TDialogElement = { data_source: '', display_name: 'Boolean Selector', name: 'somebool', optional: false, type: 'bool', placeholder: 'Subscribe?', + subtype: '', + default: '', + help_text: '', + min_length: 0, + max_length: 0, + options: [], }; const {elements, ...rest} = baseProps; const props = { ...rest, elements: [ - ...elements, + ...elements || [], element, ], }; @@ -122,9 +138,9 @@ describe('components/interactive_dialog/InteractiveDialog', () => { testCases.forEach((testCase) => test(`should interpret ${testCase.description}`, () => { if (testCase.default === undefined) { - delete element.default; + delete (element as any).default; } else { - element.default = testCase.default; + (element as any).default = testCase.default; } const store = mockStore({}); diff --git a/webapp/channels/src/components/interactive_dialog/interactive_dialog.jsx b/webapp/channels/src/components/interactive_dialog/interactive_dialog.tsx similarity index 80% rename from webapp/channels/src/components/interactive_dialog/interactive_dialog.jsx rename to webapp/channels/src/components/interactive_dialog/interactive_dialog.tsx index ee90f9c409..01018edd7e 100644 --- a/webapp/channels/src/components/interactive_dialog/interactive_dialog.jsx +++ b/webapp/channels/src/components/interactive_dialog/interactive_dialog.tsx @@ -1,11 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import PropTypes from 'prop-types'; import React from 'react'; import {Modal} from 'react-bootstrap'; import {FormattedMessage} from 'react-intl'; +import type {DialogSubmission, DialogElement as TDialogElement} from '@mattermost/types/integrations'; + +import type {ActionFunc} from 'mattermost-redux/types/actions'; import { checkDialogElementForError, checkIfErrorsMatchElements, @@ -13,41 +15,48 @@ import { import SpinnerButton from 'components/spinner_button'; +import type EmojiMap from 'utils/emoji_map'; import {localizeMessage} from 'utils/utils'; import DialogElement from './dialog_element'; import DialogIntroductionText from './dialog_introduction_text'; -export default class InteractiveDialog extends React.PureComponent { - static propTypes = { - url: PropTypes.string.isRequired, - callbackId: PropTypes.string, - elements: PropTypes.arrayOf(PropTypes.object), - title: PropTypes.string.isRequired, - introductionText: PropTypes.string, - iconUrl: PropTypes.string, - submitLabel: PropTypes.string, - notifyOnCancel: PropTypes.bool, - state: PropTypes.string, - onExited: PropTypes.func, - actions: PropTypes.shape({ - submitInteractiveDialog: PropTypes.func.isRequired, - }).isRequired, - emojiMap: PropTypes.object.isRequired, +export type Props = { + url: string; + callbackId?: string; + elements?: TDialogElement[]; + title: string; + introductionText?: string; + iconUrl?: string; + submitLabel?: string; + notifyOnCancel?: boolean; + state?: string; + onExited?: () => void; + actions: { + submitInteractiveDialog: (submission: DialogSubmission) => ActionFunc; }; + emojiMap: EmojiMap; +} - constructor(props) { +type State = { + show: boolean; + values: Record; + error: string | null; + errors: Record; + submitting: boolean; +} + +export default class InteractiveDialog extends React.PureComponent { + constructor(props: Props) { super(props); - const values = {}; + const values: Record = {}; if (props.elements != null) { props.elements.forEach((e) => { if (e.type === 'bool') { - values[e.name] = - e.default === true || - String(e.default).toLowerCase() === 'true'; + values[e.name] = String(e.default).toLowerCase() === 'true'; } 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) => { e.preventDefault(); const {elements} = this.props; const values = this.state.values; - const errors = {}; + const errors: Record = {}; + if (elements) { elements.forEach((elem) => { const error = checkDialogElementForError( @@ -93,18 +103,20 @@ export default class InteractiveDialog extends React.PureComponent { const {url, callbackId, state} = this.props; - const dialog = { + const dialog: DialogSubmission = { url, - callback_id: callbackId, - state, - submission: values, + callback_id: callbackId ?? '', + state: state ?? '', + submission: values as { [x: string]: string }, + user_id: '', + channel_id: '', + team_id: '', + cancelled: false, }; this.setState({submitting: true}); - const {data} = await this.props.actions.submitInteractiveDialog( - dialog, - ); + const {data}: any = await this.props.actions.submitInteractiveDialog(dialog) ?? {}; this.setState({submitting: false}); @@ -139,11 +151,15 @@ export default class InteractiveDialog extends React.PureComponent { const {url, callbackId, state, notifyOnCancel} = this.props; if (!submitted && notifyOnCancel) { - const dialog = { + const dialog: DialogSubmission = { url, - callback_id: callbackId, - state, + callback_id: callbackId ?? '', + state: state ?? '', cancelled: true, + user_id: '', + channel_id: '', + team_id: '', + submission: {}, }; this.props.actions.submitInteractiveDialog(dialog); @@ -152,7 +168,7 @@ export default class InteractiveDialog extends React.PureComponent { this.setState({show: false}); }; - onChange = (name, value) => { + onChange = (name: string, value: string) => { const values = {...this.state.values, [name]: value}; this.setState({values}); }; @@ -166,12 +182,13 @@ export default class InteractiveDialog extends React.PureComponent { elements, } = this.props; - let submitText = ( + let submitText: JSX.Element | string = ( ); + if (submitLabel) { submitText = submitLabel; } @@ -207,7 +224,7 @@ export default class InteractiveDialog extends React.PureComponent { >