From ca61faafd5db8387768802b80ce6bf32308a229d Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Tue, 13 Aug 2019 22:16:14 +0800 Subject: [PATCH] MM-15218 Open as alert dialog when interactive message is without element (#3088) * open as alert dialog when interactive message is without element * add missing translations --- .../interactive_dialog_controller/index.js | 5 +- .../interactive_dialog_controller.js | 52 ++++++++++-- .../interactive_dialog_controller.test.js | 82 +++++++++++++++++++ assets/base/i18n/en.json | 3 + 4 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 app/components/interactive_dialog_controller/interactive_dialog_controller.test.js diff --git a/app/components/interactive_dialog_controller/index.js b/app/components/interactive_dialog_controller/index.js index a2577835a..5d0fb3b43 100644 --- a/app/components/interactive_dialog_controller/index.js +++ b/app/components/interactive_dialog_controller/index.js @@ -4,6 +4,8 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; +import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations'; + import {showModal} from 'app/actions/navigation'; import InteractiveDialogController from './interactive_dialog_controller'; @@ -11,7 +13,7 @@ import InteractiveDialogController from './interactive_dialog_controller'; function mapStateToProps(state) { return { triggerId: state.entities.integrations.dialogTriggerId, - dialog: state.entities.integrations.dialog || {}, + dialogData: state.entities.integrations.dialog || {}, }; } @@ -19,6 +21,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ showModal, + submitInteractiveDialog, }, dispatch), }; } diff --git a/app/components/interactive_dialog_controller/interactive_dialog_controller.js b/app/components/interactive_dialog_controller/interactive_dialog_controller.js index 7ec4e0177..e7565c9a7 100644 --- a/app/components/interactive_dialog_controller/interactive_dialog_controller.js +++ b/app/components/interactive_dialog_controller/interactive_dialog_controller.js @@ -4,14 +4,17 @@ import {PureComponent} from 'react'; import PropTypes from 'prop-types'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import {Alert} from 'react-native'; +import {intlShape} from 'react-intl'; export default class InteractiveDialogController extends PureComponent { static propTypes = { actions: PropTypes.shape({ showModal: PropTypes.func.isRequired, + submitInteractiveDialog: PropTypes.func.isRequired, }).isRequired, triggerId: PropTypes.string, - dialog: PropTypes.object, + dialogData: PropTypes.object, theme: PropTypes.object, }; @@ -23,14 +26,18 @@ export default class InteractiveDialogController extends PureComponent { }); } + static contextTypes = { + intl: intlShape, + }; + componentDidUpdate(prevProps) { - const {actions, triggerId} = this.props; + const {triggerId} = this.props; if (!triggerId) { return; } - const dialogData = this.props.dialog || {}; - const prevDialogData = prevProps.dialog || {}; + const dialogData = this.props.dialogData || {}; + const prevDialogData = prevProps.dialogData || {}; if (prevProps.triggerId === triggerId && dialogData.trigger_id === prevDialogData.trigger_id) { return; } @@ -43,9 +50,30 @@ export default class InteractiveDialogController extends PureComponent { return; } - const screen = 'InteractiveDialog'; - const title = dialogData.dialog.title; - const passProps = {}; + if (dialogData.dialog.elements && dialogData.dialog.elements.length > 0) { + this.showInteractiveDialogScreen(dialogData.dialog); + } else { + this.showAlertDialog(dialogData.dialog, dialogData.url); + } + } + + showAlertDialog(dialog, url) { + const {formatMessage} = this.context.intl; + + Alert.alert( + dialog.title, + '', + [{ + text: formatMessage({id: 'mobile.alert_dialog.alertCancel', defaultMessage: 'Cancel'}), + onPress: () => this.handleCancel(dialog, url), + }, { + text: dialog.submit_label, + onPress: () => this.props.actions.submitInteractiveDialog({...dialog, url}), + }], + ); + } + + showInteractiveDialogScreen = (dialog) => { const options = { topBar: { leftButtons: [{ @@ -55,12 +83,18 @@ export default class InteractiveDialogController extends PureComponent { rightButtons: [{ id: 'submit-dialog', showAsAction: 'always', - text: dialogData.dialog.submit_label, + text: dialog.submit_label, }], }, }; - actions.showModal(screen, title, passProps, options); + this.props.actions.showModal('InteractiveDialog', dialog.title, null, options); + } + + handleCancel = (dialog, url) => { + if (dialog.notify_on_cancel) { + this.props.actions.submitInteractiveDialog({...dialog, url, cancelled: true}); + } } render() { diff --git a/app/components/interactive_dialog_controller/interactive_dialog_controller.test.js b/app/components/interactive_dialog_controller/interactive_dialog_controller.test.js new file mode 100644 index 000000000..29bd8a573 --- /dev/null +++ b/app/components/interactive_dialog_controller/interactive_dialog_controller.test.js @@ -0,0 +1,82 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import InteractiveDialogController from './interactive_dialog_controller'; + +jest.mock('react-intl'); +jest.mock('react-native-vector-icons/MaterialIcons', () => ({ + getImageSource: jest.fn().mockResolvedValue({}), +})); + +describe('InteractiveDialogController', () => { + test('should open interactive dialog as alert or screen depending on with or without element', () => { + let baseProps = getBaseProps('trigger_id_1'); + const wrapper = shallow( + , + {context: {intl: {formatMessage: jest.fn()}}}, + ); + + const instance = wrapper.instance(); + instance.showAlertDialog = jest.fn(); + instance.showInteractiveDialogScreen = jest.fn(); + + expect(instance.showAlertDialog).toHaveBeenCalledTimes(0); + expect(instance.showInteractiveDialogScreen).toHaveBeenCalledTimes(0); + + baseProps = getBaseProps('trigger_id_2'); + wrapper.setProps({...baseProps}); + expect(instance.showAlertDialog).toHaveBeenCalledTimes(1); + expect(instance.showAlertDialog).toHaveBeenCalledWith(baseProps.dialogData.dialog, baseProps.dialogData.url); + expect(instance.showInteractiveDialogScreen).toHaveBeenCalledTimes(0); + + const elements = [{ + data_source: '', + default: '', + display_name: 'Number', + help_text: '', + max_length: 0, + min_length: 0, + name: 'somenumber', + optional: false, + options: null, + placeholder: '', + subtype: 'number', + type: 'text', + }]; + + baseProps = getBaseProps('trigger_id_3', elements); + wrapper.setProps({...baseProps}); + expect(instance.showAlertDialog).toHaveBeenCalledTimes(1); + expect(instance.showInteractiveDialogScreen).toHaveBeenCalledTimes(1); + expect(instance.showInteractiveDialogScreen).toHaveBeenCalledWith(baseProps.dialogData.dialog); + }); +}); + +function getBaseProps(triggerId, elements) { + const dialogData = { + dialog: { + callback_id: 'somecallbackid', + elements, + icon_url: 'icon_url', + notify_on_cancel: true, + state: 'somestate', + submit_label: 'Submit Test', + title: 'Dialog Test', + }, + trigger_id: triggerId, + url: 'https://localhost:8065/dialog_submit', + }; + + return { + actions: { + showModal: jest.fn(), + submitInteractiveDialog: jest.fn(), + }, + triggerId, + dialogData, + theme: {}, + }; +} diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index f568af1fa..682c8450d 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -137,6 +137,7 @@ "mobile.advanced_settings.delete_title": "Delete Documents & Data", "mobile.advanced_settings.timezone": "Timezone", "mobile.advanced_settings.title": "Advanced Settings", + "mobile.alert_dialog.alertCancel": "Cancel", "mobile.android.camera_permission_denied_description": "To take photos and videos with your camera, please change your permission settings.", "mobile.android.camera_permission_denied_title": "Camera access is required", "mobile.android.permission_denied_dismiss": "Dismiss", @@ -357,6 +358,8 @@ "mobile.post_textbox.empty.title": "Empty Message", "mobile.post_textbox.entire_channel.cancel": "Cancel", "mobile.post_textbox.entire_channel.confirm": "Confirm", + "mobile.post_textbox.entire_channel.message": "By using @all or @channel you are about to send notifications to {totalMembers} people. Are you sure you want to do this?", + "mobile.post_textbox.entire_channel.message.with_timezones": "By using @all or @channel you are about to send notifications to {totalMembers} people in {timezones, number} {timezones, plural, one {timezone} other {timezones}}. Are you sure you want to do this?", "mobile.post_textbox.entire_channel.title": "Confirm sending notifications to entire channel", "mobile.post_textbox.uploadFailedDesc": "Some attachments failed to upload to the server. Are you sure you want to post the message?", "mobile.post_textbox.uploadFailedTitle": "Attachment failure",