Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Separate calls for Apps #9263

Merged
merged 39 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f2f9368
WIP - need to restore Binding.Call
Oct 10, 2021
2b9554c
Merge branch 'master' into lev-MM-39092-separate-calls
Oct 12, 2021
98075c4
Merge branch 'master' into lev-MM-39092-separate-calls
larkox Oct 22, 2021
f05821f
Address missing pieces
larkox Oct 25, 2021
b5cd58a
Re-add the submit field on calls and improve tests
larkox Oct 26, 2021
df295bf
Update i18n
larkox Oct 26, 2021
09e0b25
apps API changes fixup
Nov 22, 2021
46a1615
apps API changes fixup #2: form/modal
Nov 24, 2021
3dadf0f
Merge branch 'master' into separateCalls
larkox Nov 30, 2021
a26ac2b
Address feedback
larkox Nov 30, 2021
5e757aa
Merge branch 'separateCalls' into lev-separateCalls
larkox Nov 30, 2021
88ab662
Address feedback
larkox Nov 30, 2021
d9182f2
Merge pull request #7 from levb/lev-separateCalls
larkox Nov 30, 2021
27406da
Fix lint
larkox Nov 30, 2021
bed64e2
Fix missing error->text change
larkox Dec 15, 2021
3c12a59
Merge branch 'master' into separateCalls
mickmister Jan 5, 2022
fcf1c98
fix openAppsModal references
mickmister Jan 5, 2022
c974c9e
lint, types, and i18n
mickmister Jan 6, 2022
47f1794
lint
mickmister Jan 6, 2022
cb60e92
missing method
mickmister Jan 6, 2022
1fe3ccb
update snapshots
mickmister Jan 6, 2022
8c32ad9
Merge branch 'master' of github.com:mattermost/mattermost-webapp into…
Feb 13, 2022
4720a78
Fixed handleBindingClick, was double-wrapping .data
Feb 13, 2022
2e3960a
fixed embedded button binding
Feb 13, 2022
35cd488
Merge branch 'master' of github.com:mattermost/mattermost-webapp into…
Feb 28, 2022
b03f3c0
Merge branch 'master' into separateCalls
mattermod Mar 2, 2022
c55ed22
Merge branch 'master' into separateCalls
mattermod Mar 3, 2022
c5341c4
Merge branch 'master' into separateCalls
mattermod Mar 8, 2022
ec02f34
various fixes
mickmister Mar 15, 2022
0e620e9
fix lookups for command parser
mickmister Mar 16, 2022
ce445e9
sync app command parser
mickmister Mar 17, 2022
944baf7
Merge branch 'master' into separateCalls
mickmister Mar 21, 2022
b31000b
types
mickmister Mar 21, 2022
4571966
fix tests
mickmister Mar 21, 2022
a2c93b5
change control flow for cleaning bindings
mickmister Mar 21, 2022
4a27b70
re-expand bindings when post is updated
mickmister Mar 22, 2022
d43c479
getDerivedStateFromProps to avoid lint rule
mickmister Mar 22, 2022
4c60359
avoid `this` usage in static function
mickmister Mar 22, 2022
7f31aa3
fix other spot with same issue
mickmister Mar 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 140 additions & 21 deletions actions/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// See LICENSE.txt for license information.

import {Client4} from 'mattermost-redux/client';
import {Action, ActionFunc} from 'mattermost-redux/types/actions';
import {AppCallResponse, AppForm, AppCallType, AppCallRequest, AppContext, AppBinding} from 'mattermost-redux/types/apps';
import {AppCallTypes, AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import {Action, ActionFunc, DispatchFunc} from 'mattermost-redux/types/actions';
import {AppCallResponse, AppForm, AppCallRequest, AppContext, AppBinding} from 'mattermost-redux/types/apps';
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import {Post} from 'mattermost-redux/types/posts';
import {CommandArgs} from 'mattermost-redux/types/integrations';

Expand All @@ -15,16 +15,74 @@ import AppsForm from 'components/apps_form';
import {ModalIdentifiers} from 'utils/constants';
import {getSiteURL, shouldOpenInNewTab} from 'utils/url';
import {browserHistory} from 'utils/browser_history';
import {makeCallErrorResponse} from 'utils/apps';
import {createCallRequest, makeCallErrorResponse} from 'utils/apps';

import {cleanForm} from 'mattermost-redux/utils/apps';

import {sendEphemeralPost} from './global_actions';

export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
export function handleBindingClick<Res=unknown>(binding: AppBinding, context: AppContext, intl: any): ActionFunc {
return async (dispatch: DispatchFunc) => {
// Fetch form
let form = binding.form;
if (form?.source) {
const callRequest = createCallRequest(form.source, context);
const res = await dispatch(doAppFetchForm<Res>(callRequest, intl));
if (res.error) {
return res;
}
form = res.data.form;
}

// Open form
if (form) {
// This should come properly formed, but using preventive checks
if (!form?.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.malformed_binding',
defaultMessage: 'This binding is not properly formed. Contact the App developer.',
});
return {error: makeCallErrorResponse(errMsg)};
}

const res: AppCallResponse = {
type: AppCallResponseTypes.FORM,
form,
};
return {data: res};
}

// Submit binding
// This should come properly formed, but using preventive checks
if (!binding.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.malformed_binding',
defaultMessage: 'This binding is not properly formed. Contact the App developer.',
});
return {error: makeCallErrorResponse(errMsg)};
}

const callRequest = createCallRequest(
binding.submit,
context,
);

const res = await dispatch(doAppSubmit<Res>(callRequest, intl));
return res;
};
}

export function doAppSubmit<Res=unknown>(inCall: AppCallRequest, intl: any): ActionFunc {
return async () => {
try {
const res = await Client4.executeAppCall(call, type) as AppCallResponse<Res>;
const call: AppCallRequest = {
...inCall,
context: {
...inCall.context,
track_as_submit: true,
},
};
const res = await Client4.executeAppCall(call, true) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;

switch (responseType) {
Expand All @@ -33,41 +91,102 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
case AppCallResponseTypes.ERROR:
return {error: res};
case AppCallResponseTypes.FORM:
if (!res.form) {
if (!res.form?.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.form.no_form',
defaultMessage: 'Response type is `form`, but no form was included in response.',
defaultMessage: 'Response type is `form`, but no valid form was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}

cleanForm(res.form);

return {data: res};
case AppCallResponseTypes.NAVIGATE:

case AppCallResponseTypes.NAVIGATE: {
if (!res.navigate_to_url) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.navigate.no_url',
defaultMessage: 'Response type is `navigate`, but no url was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}
if (shouldOpenInNewTab(res.navigate_to_url, getSiteURL())) {
window.open(res.navigate_to_url);
return {data: res};
}
const navigateURL = res.navigate_to_url.startsWith(getSiteURL()) ?
res.navigate_to_url.slice(getSiteURL().length) :
res.navigate_to_url;
browserHistory.push(navigateURL);
return {data: res};
}
default: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {type: responseType});
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error: any) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
return {error: makeCallErrorResponse(errMsg)};
}
};
}

export function doAppFetchForm<Res=unknown>(call: AppCallRequest, intl: any): ActionFunc {
return async () => {
try {
const res = await Client4.executeAppCall(call, false) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;

if (type !== AppCallTypes.SUBMIT) {
switch (responseType) {
case AppCallResponseTypes.ERROR:
return {error: res};
case AppCallResponseTypes.FORM:
if (!res.form?.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.navigate.no_submit',
defaultMessage: 'Response type is `navigate`, but the call was not a submission.',
id: 'apps.error.responses.form.no_form',
defaultMessage: 'Response type is `form`, but no valid form was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}
cleanForm(res.form);
return {data: res};
default: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {type: responseType});
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error: any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (error: any) {
} catch (error: Error) {

const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
return {error: makeCallErrorResponse(errMsg)};
}
};
}

if (shouldOpenInNewTab(res.navigate_to_url, getSiteURL())) {
window.open(res.navigate_to_url);
return {data: res};
}
export function doAppLookup<Res=unknown>(call: AppCallRequest, intl: any): ActionFunc {
return async () => {
try {
const res = await Client4.executeAppCall(call, false) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;

browserHistory.push(res.navigate_to_url.slice(getSiteURL().length));
switch (responseType) {
case AppCallResponseTypes.OK:
return {data: res};
case AppCallResponseTypes.ERROR:
return {error: res};

default: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unknown_type',
Expand All @@ -76,7 +195,7 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error) {
} catch (error: any) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
Expand All @@ -101,13 +220,13 @@ export function makeFetchBindings(location: string): (userId: string, channelId:
};
}

export function openAppsModal(form: AppForm, call: AppCallRequest): Action {
export function openAppsModal(form: AppForm, context: AppContext): Action {
return openModal({
modalId: ModalIdentifiers.APPS_MODAL,
dialogType: AppsForm,
dialogProps: {
form,
call,
context,
},
});
}
Expand Down
9 changes: 5 additions & 4 deletions actions/command.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ const initialState = {
app_id: 'appid',
label: 'custom',
description: 'Run the command.',
call: {
path: 'https://someserver.com/command',
},
form: {
submit: {
path: 'https://someserver.com/command',
},
fields: [
{
name: 'key1',
Expand Down Expand Up @@ -291,6 +291,7 @@ describe('executeCommand', () => {
location: '/command/appid/custom',
root_id: '',
team_id: '456',
track_as_submit: true,
},
raw_command: '/appid custom value1 --key2 value2',
path: 'https://someserver.com/command',
Expand All @@ -301,7 +302,7 @@ describe('executeCommand', () => {
expand: {},
query: undefined,
selected_field: undefined,
}, 'submit');
}, true);
expect(result).toEqual({data: true});
});
});
Expand Down
20 changes: 10 additions & 10 deletions actions/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {IntegrationTypes} from 'mattermost-redux/action_types';
import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
import type {CommandArgs} from 'mattermost-redux/types/integrations';

import {AppCallResponseTypes, AppCallTypes} from 'mattermost-redux/constants/apps';
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';

import {DoAppCallResult} from 'types/apps';

Expand All @@ -36,7 +36,7 @@ import {GlobalState} from 'types/store';

import {t} from 'utils/i18n';

import {doAppCall, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps';
import {doAppSubmit, openAppsModal, postEphemeralCallResponseForCommandArgs} from './apps';

export function executeCommand(message: string, args: CommandArgs): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
Expand Down Expand Up @@ -123,16 +123,16 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
const parser = new AppCommandParser({dispatch, getState: getGlobalState} as any, intlShim, args.channel_id, args.team_id, args.root_id);
if (parser.isAppCommand(msg)) {
try {
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
if (!call) {
const {creq, errorMessage} = await parser.composeCommandSubmitCall(msg);
if (!creq) {
return createErrorMessage(errorMessage!);
}

const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intlShim)) as DoAppCallResult;
const res = await dispatch(doAppSubmit(creq, intlShim)) as DoAppCallResult;

if (res.error) {
const errorResponse = res.error;
return createErrorMessage(errorResponse.error || intlShim.formatMessage({
return createErrorMessage(errorResponse.text || intlShim.formatMessage({
id: 'apps.error.unknown',
defaultMessage: 'Unknown error.',
}));
Expand All @@ -141,13 +141,13 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
const callResp = res.data!;
switch (callResp.type) {
case AppCallResponseTypes.OK:
if (callResp.markdown) {
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
if (callResp.text) {
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.text, args));
}
return {data: true};
case AppCallResponseTypes.FORM:
if (callResp.form) {
dispatch(openAppsModal(callResp.form, call));
dispatch(openAppsModal(callResp.form, creq.context));
}
return {data: true};
case AppCallResponseTypes.NAVIGATE:
Expand All @@ -159,7 +159,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
{type: callResp.type},
));
}
} catch (err) {
} catch (err: any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (err: any) {
} catch (err: Error) {

return createErrorMessage(err.message || localizeMessage('apps.error.unknown', 'Unknown error.'));
}
}
Expand Down
32 changes: 8 additions & 24 deletions components/app_bar/app_bar_binding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import {useDispatch, useSelector} from 'react-redux';
import {useIntl} from 'react-intl';
import {Tooltip} from 'react-bootstrap';

import {AppCallResponseTypes, AppCallTypes} from 'mattermost-redux/constants/apps';
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {AppBinding, AppCallResponse} from 'mattermost-redux/types/apps';

import {doAppCall, openAppsModal, postEphemeralCallResponseForContext} from 'actions/apps';
import {handleBindingClick, openAppsModal, postEphemeralCallResponseForContext} from 'actions/apps';

import {createCallContext, createCallRequest} from 'utils/apps';
import {createCallContext} from 'utils/apps';
import Constants from 'utils/constants';
import {DoAppCallResult} from 'types/apps';

Expand All @@ -33,33 +33,17 @@ const AppBarBinding = (props: BindingComponentProps) => {
const teamId = useSelector(getCurrentTeamId);

const submitAppCall = async () => {
const call = binding.form?.call || binding.call;

if (!call) {
return;
}
const context = createCallContext(
binding.app_id,
binding.location,
channelId,
teamId,
'',
'',
);
const callRequest = createCallRequest(
call,
context,
);

if (binding.form) {
dispatch(openAppsModal(binding.form, callRequest));
return;
}

const result = await dispatch(doAppCall(callRequest, AppCallTypes.SUBMIT, intl)) as DoAppCallResult;
const result = await dispatch(handleBindingClick(binding, context, intl)) as DoAppCallResult;

if (result.error) {
const errMsg = result.error.error || 'An error occurred';
const errMsg = result.error.text || 'An error occurred';
dispatch(postEphemeralCallResponseForContext(result.error, errMsg, context));
return;
}
Expand All @@ -68,13 +52,13 @@ const AppBarBinding = (props: BindingComponentProps) => {

switch (callResp.type) {
case AppCallResponseTypes.OK:
if (callResp.markdown) {
dispatch(postEphemeralCallResponseForContext(callResp, callResp.markdown, context));
if (callResp.text) {
dispatch(postEphemeralCallResponseForContext(callResp, callResp.text, context));
}
return;
case AppCallResponseTypes.FORM:
if (callResp.form) {
dispatch(openAppsModal(callResp.form, callRequest));
dispatch(openAppsModal(callResp.form, context));
}
return;
case AppCallResponseTypes.NAVIGATE:
Expand Down
Loading