From 985070ee7183791d41d19e5ea392b49dde446348 Mon Sep 17 00:00:00 2001 From: Michael Kochell <6913320+mickmister@users.noreply.github.com> Date: Wed, 24 Mar 2021 13:39:42 -0400 Subject: [PATCH] [MM-33766] Handle channel header call error (#5233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Apps bindings support (#5012) * Add redux related information * Add binding loading on channel refresh * Add channel header and post option bindings * Fix test * Minor fixes * Fix snapshots * Handle errors and show bindings only on main channel * Update Expand Levels keys and values to match latest changes * Add NAVIGATE call response handling. * Add more isolation to apps related code * Add defaults to send ephemeral * Update variable naming * Rename shouldProcessApps by a more meaningful appsEnabled * Embedded forms (#5169) * Add redux related information * Add binding loading on channel refresh * Add channel header and post option bindings * Fix test * Minor fixes * Fix snapshots * Handle errors and show bindings only on main channel * Update Expand Levels keys and values to match latest changes * Add NAVIGATE call response handling. * Add more isolation to apps related code * Add Embedded Forms * Fix snapshots * Add Embedded Forms * Improve naming, change the root element to be a binding and improve binding handling * Get post down on the buttons, remove unneeded variables and minor fixes from the review * Allow undefined bindings to fillBindingsInformation and add logging for error. * Address review feedback * Add App Forms (#5177) * Add App Forms * Address feedback and self review * Add dynamic select * Fixes and improvements * Add the ability to refresh on submit. * Use AppFormValue instead of redoing the type * Address feedback * [MM-31508] Rename URL to Path in Call (#5186) * Add user agent to call context (#5193) * Remove unneeded state reference (#5196) * Change user agent query on fetch bindings (#5201) * Add refresh websocket event to refetch bindings (#5194) * Add refresh websocket event to refetch bindings * Add missing changes * Declare socket event constant and separate the handler to a different function * Add binding validation on binding fetch (#5200) * Apps commands (#5107) * Add redux related information * Add binding loading on channel refresh * Add channel header and post option bindings * Fix test * Minor fixes * Fix snapshots * apps modals draft * Handle errors and show bindings only on main channel * Update Expand Levels keys and values to match latest changes * Add NAVIGATE call response handling. * Add more isolation to apps related code * reuse command parser throughout slash_suggestion component's lifecycle * fix prop and lint * using alert to show error message. need another way * duplicate import * types * types * types * rename file * copy webapp parser and test * dependencies moved. tests pass * move app command parser into its own folder * converted to typescript, all tests are passing * automated and manual tests work * lint * lint * remove fall throughs with blocks * types * doAppCall type * extract displayError to deps file * test types * lint * fix tests * unused import * PR feedback * fix imports and show errors * types * remove execute suggestion for mobile * watch feature flag * fix tests * change form text arugment behavior to show user input and not hint * return call response error in doAppCall * update tests to remove hint from text field suggestions * lint * use new base command structure * fix tests * wrap appsEnabled * typescript actions/command.ts * update app constants * PR feedback * fix error handling from doAppCall action * Use App CallRequest structure (#5212) * error handling * move test files * remove unused import Co-authored-by: Daniel Espino García * Add feature flag (#5207) * Add feature flag * Simplify return * Add localization, call validation and use call type on subpath (#5221) * Add localization, call validation and use call type on subpath * Add more localization to the parser and bring fixes from webapp * Fix ephemerals and channel header / post options crashes * fix app command parser deps and alert messages * Improve suggestion handling * Fix test * Fix lint * Return errors as error * Address PR feedback * return error property * fix error string wordings Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com> * leave the channel info screen when the call is successful. stay if there is an error * fix intl in tests Co-authored-by: Daniel Espino García Co-authored-by: Ben Schumacher Co-authored-by: Mattermod --- .../__snapshots__/channel_info.test.js.snap | 6 +- .../channel_info/bindings/bindings.tsx | 82 ++++++++++++------- .../__snapshots__/post_options.test.js.snap | 6 +- .../post_options/bindings/bindings.tsx | 57 +++++++------ app/screens/post_options/post_options.test.js | 7 +- 5 files changed, 91 insertions(+), 67 deletions(-) diff --git a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap index 201240039..18e2a2c45 100644 --- a/app/screens/channel_info/__snapshots__/channel_info.test.js.snap +++ b/app/screens/channel_info/__snapshots__/channel_info.test.js.snap @@ -503,7 +503,7 @@ exports[`channelInfo should match snapshot 1`] = ` } } /> - - - Promise<{data?: AppCallResponse, error?: AppCallResponse}>; @@ -28,7 +29,7 @@ type Props = { } } -const Bindings: React.FC = (props: Props) => { +const Bindings: React.FC = injectIntl((props: Props) => { if (!props.appsEnabled) { return null; } @@ -51,7 +52,7 @@ const Bindings: React.FC = (props: Props) => { {options} ); -}; +}); export default Bindings; @@ -67,40 +68,60 @@ type OptionProps = { }, } -const Option = injectIntl((props: OptionProps) => { - const onPress = async () => { +type OptionState = { + submitting: boolean; +} + +class Option extends React.PureComponent { + state = { + submitting: false, + }; + + onPress = async () => { + const {binding, currentChannel, currentTeamId, intl} = this.props; + const {doAppCall, sendEphemeralPost} = this.props.actions; + + if (this.state.submitting) { + return; + } + if (!binding.call) { return; } + const context = createCallContext( binding.app_id, binding.location, - props.currentChannel.id, - props.currentChannel.team_id || props.currentTeamId, + currentChannel.id, + currentChannel.team_id || currentTeamId, ); const call = createCallRequest( binding.call, context, ); - const res = await props.actions.doAppCall(call, AppCallTypes.SUBMIT, props.intl); + this.setState({submitting: true}); + + const res = await doAppCall(call, AppCallTypes.SUBMIT, intl); + + this.setState({submitting: false}); + if (res.error) { const errorResponse = res.error; - const title = props.intl.formatMessage({ + const title = intl.formatMessage({ id: 'mobile.general.error.title', defaultMessage: 'Error', }); - const errorMessage = errorResponse.error || props.intl.formatMessage({ + const errorMessage = errorResponse.error || intl.formatMessage({ id: 'apps.error.unknown', defaultMessage: 'Unknown error occurred.', }); Alert.alert(title, errorMessage); - dismissModal(); return; } const callResp = res.data!; - const ephemeral = (message: string) => props.actions.sendEphemeralPost(message, props.currentChannel.id); + const ephemeral = (message: string) => sendEphemeralPost(message, currentChannel.id); switch (callResp.type) { case AppCallResponseTypes.OK: if (callResp.markdown) { @@ -111,37 +132,40 @@ const Option = injectIntl((props: OptionProps) => { case AppCallResponseTypes.FORM: break; default: { - const title = props.intl.formatMessage({ + const title = intl.formatMessage({ id: 'mobile.general.error.title', defaultMessage: 'Error', }); - const errMessage = props.intl.formatMessage({ + const errMessage = intl.formatMessage({ id: 'apps.error.responses.unknown_type', defaultMessage: 'App response type not supported. Response type: {type}.', }, { type: callResp.type, }); Alert.alert(title, errMessage); + return; } } dismissModal(); }; - const {binding, theme} = props; - if (!binding.label) { - return null; + render() { + const {binding, theme} = this.props; + if (!binding.label) { + return null; + } + return ( + <> + + + + ); } - return ( - <> - - - - ); -}); +} diff --git a/app/screens/post_options/__snapshots__/post_options.test.js.snap b/app/screens/post_options/__snapshots__/post_options.test.js.snap index 76e28657f..643869556 100644 --- a/app/screens/post_options/__snapshots__/post_options.test.js.snap +++ b/app/screens/post_options/__snapshots__/post_options.test.js.snap @@ -192,7 +192,7 @@ exports[`PostOptions should match snapshot, no option for system message to user } } /> - - - void, appsEnabled: boolean, + intl: typeof intlShape, actions: { doAppCall: (call: AppCallRequest, type: AppCallType, intl: any) => Promise<{data?: AppCallResponse, error?: AppCallResponse}>; sendEphemeralPost: (message: any, channelId?: string, parentId?: string) => Promise; } } -const Bindings = (props: Props) => { +const Bindings = injectIntl((props: Props) => { if (!props.appsEnabled) { return null; } @@ -58,7 +59,7 @@ const Bindings = (props: Props) => { {options} ); -}; +}); export default Bindings; @@ -76,9 +77,10 @@ type OptionProps = { }, } -const Option = injectIntl((props: OptionProps) => { - const onPress = async () => { - const {closeWithAnimation, post, teamID} = props; +class Option extends React.PureComponent { + onPress = async () => { + const {closeWithAnimation, post, teamID, binding, intl} = this.props; + const {doAppCall, sendEphemeralPost} = this.props.actions; if (!binding.call) { return; @@ -98,24 +100,25 @@ const Option = injectIntl((props: OptionProps) => { post: AppExpandLevels.ALL, }, ); - const res = await props.actions?.doAppCall(call, AppCallTypes.SUBMIT, props.intl); + + closeWithAnimation(); + const res = await doAppCall(call, AppCallTypes.SUBMIT, intl); if (res.error) { const errorResponse = res.error; - const title = props.intl.formatMessage({ + const title = intl.formatMessage({ id: 'mobile.general.error.title', defaultMessage: 'Error', }); - const errorMessage = errorResponse.error || props.intl.formatMessage({ + const errorMessage = errorResponse.error || intl.formatMessage({ id: 'apps.error.unknown', defaultMessage: 'Unknown error occurred.', }); Alert.alert(title, errorMessage); - closeWithAnimation(); return; } const callResp = (res as {data: AppCallResponse}).data; - const ephemeral = (message: string) => props.actions.sendEphemeralPost(message, props.post.channel_id, props.post.root_id); + const ephemeral = (message: string) => sendEphemeralPost(message, post.channel_id, post.root_id); switch (callResp.type) { case AppCallResponseTypes.OK: if (callResp.markdown) { @@ -126,11 +129,11 @@ const Option = injectIntl((props: OptionProps) => { case AppCallResponseTypes.FORM: break; default: { - const title = props.intl.formatMessage({ + const title = intl.formatMessage({ id: 'mobile.general.error.title', defaultMessage: 'Error', }); - const errMessage = props.intl.formatMessage({ + const errMessage = intl.formatMessage({ id: 'apps.error.responses.unknown_type', defaultMessage: 'App response type not supported. Response type: {type}.', }, { @@ -139,21 +142,21 @@ const Option = injectIntl((props: OptionProps) => { Alert.alert(title, errMessage); } } - - closeWithAnimation(); }; - const {binding, theme} = props; - if (!binding.label) { - return null; - } + render() { + const {binding, theme} = this.props; + if (!binding.label) { + return null; + } - return ( - - ); -}); + return ( + + ); + } +} diff --git a/app/screens/post_options/post_options.test.js b/app/screens/post_options/post_options.test.js index 5ab5e3a92..3b01f47ec 100644 --- a/app/screens/post_options/post_options.test.js +++ b/app/screens/post_options/post_options.test.js @@ -3,14 +3,12 @@ import React from 'react'; import {Alert} from 'react-native'; -import {shallow} from 'enzyme'; +import {shallowWithIntl} from 'test/intl-test-helper'; import Preferences from '@mm-redux/constants/preferences'; import PostOptions from './post_options'; -jest.mock('react-intl'); - describe('PostOptions', () => { const actions = { addReaction: jest.fn(), @@ -55,12 +53,11 @@ describe('PostOptions', () => { }; function getWrapper(props = {}) { - return shallow( + return shallowWithIntl( , - {context: {intl: {formatMessage: ({defaultMessage}) => defaultMessage}}}, ); }