Skip to content

Commit

Permalink
Merge branch 'MM-34128' of github.com:brightscout-alpha/mattermost-we…
Browse files Browse the repository at this point in the history
…bapp into MM-34128

* 'MM-34128' of github.com:brightscout-alpha/mattermost-webapp: (104 commits)
  Mm 34578 add subscribe now button (mattermost#7894)
  fix subcription typo in the code (mattermost#7930)
  Fixing keys in Cypress (mattermost#7931)
  fix test on LDAP groups, code theme and incoming webhook (mattermost#7919)
  upgrade cypress to v7.1.0 (mattermost#7923)
  Adding missing Zephyr keys to existing E2E tests (mattermost#7920)
  Prod test fixes (mattermost#7918)
  [MM-11264] Added statuses to channel invite modal (mattermost#7787)
  [MM_20388/ GH_16718] Migrate 'components/signup/signup_email' module and associated tests to TypeScript (mattermost#7699)
  MM-34569 - remove step 3 to guests (mattermost#7838)
  add mfa metadata to specs accordingly (mattermost#7896)
  fix test for forgot password based on new email template (mattermost#7892)
  fix CI run of Cypress and add flag to sort specs to first (mattermost#7913)
  cast to lowercase before comparing (mattermost#7769)
  Migrating a few stragglers from the TS migration (mattermost#7770)
  update tests and keys of search date filters (mattermost#7893)
  [MM-34523] New trial card for license page (mattermost#7826)
  Feature: In-product support for Cloud Trial (mattermost#7907)
  Cypress/E2E: Update tests and keys of search date filters (remaining) (mattermost#7895)
  Mm 29656 (mattermost#7820)
  ...
  • Loading branch information
chetanyakan committed Apr 27, 2021
2 parents 4a4f7f4 + b767b14 commit d3daedd
Show file tree
Hide file tree
Showing 858 changed files with 42,429 additions and 14,562 deletions.
10 changes: 3 additions & 7 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
stages:
- test
# These need to have separate stages, otherwise artifacts would overwrite each other
- te-build
- te-s3
- ee-build
- ee-s3
- create-vars
- build
- s3
- trigger

variables:
Expand All @@ -18,7 +14,7 @@ include:
file: private.yml

empty:
stage: create-vars
stage: test
script:
- echo "empty"

Expand Down
94 changes: 94 additions & 0 deletions actions/apps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

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

import {openModal} from 'actions/views/modals';

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';

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

switch (responseType) {
case AppCallResponseTypes.OK:
return {data: res};
case AppCallResponseTypes.ERROR:
return {data: res};
case AppCallResponseTypes.FORM:
if (!res.form) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.form.no_form',
defaultMessage: 'Response type is `form`, but no form was included in response.',
});
return {data: makeCallErrorResponse(errMsg)};
}

if (type === AppCallTypes.SUBMIT) {
dispatch(openAppsModal(res.form, call));
}

return {data: res};
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 {data: makeCallErrorResponse(errMsg)};
}

if (type !== AppCallTypes.SUBMIT) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.navigate.no_submit',
defaultMessage: 'Response type is `navigate`, but the call was not a submission.',
});
return {data: makeCallErrorResponse(errMsg)};
}

if (shouldOpenInNewTab(res.navigate_to_url, getSiteURL())) {
window.open(res.navigate_to_url);
return {data: res};
}

browserHistory.push(res.navigate_to_url.slice(getSiteURL().length));
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 {data: makeCallErrorResponse(errMsg)};
}
}
} catch (error) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
return {data: makeCallErrorResponse(errMsg)};
}
};
}

export function openAppsModal(form: AppForm, call: AppCallRequest): Action {
return openModal({
modalId: ModalIdentifiers.APPS_MODAL,
dialogType: AppsForm,
dialogProps: {
form,
call,
},
});
}
106 changes: 106 additions & 0 deletions actions/command.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {Client4} from 'mattermost-redux/client';
import * as Channels from 'mattermost-redux/selectors/entities/channels';
import * as Teams from 'mattermost-redux/selectors/entities/teams';

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

import {ActionTypes, Constants} from 'utils/constants';
import * as UserAgent from 'utils/user_agent';
import * as GlobalActions from 'actions/global_actions';
Expand All @@ -24,14 +26,29 @@ const currentTeamId = '321';
const currentUserId = 'user123';
const initialState = {
entities: {
admin: {
pluginStatuses: {
'com.mattermost.apps': {
state: 2,
},
},
},
general: {
config: {
ExperimentalViewArchivedChannels: 'false',
EnableLegacySidebar: 'true',
},
},
posts: {
posts: {
root_id: {id: 'root_id', channel_id: '123'},
},
},
channels: {
currentChannelId,
channels: {
123: {id: '123', team_id: '456'},
},
},
preferences: {
myPreferences: {},
Expand All @@ -51,6 +68,46 @@ const initialState = {
},
},
},
apps: {
bindings: [{
location: '/command',
bindings: [{
app_id: 'appid',
label: 'appid',
bindings: [
{
app_id: 'appid',
label: 'custom',
description: 'Run the command.',
call: {
path: 'https://someserver.com/command',
},
form: {
fields: [
{
name: 'key1',
label: 'key1',
type: 'text',
position: 1,
},
{
name: 'key2',
label: 'key2',
type: 'static_select',
options: [
{
label: 'Value 2',
value: 'value2',
},
],
},
],
},
},
],
}],
}],
},
},
views: {
rhs: {
Expand Down Expand Up @@ -78,6 +135,7 @@ describe('executeCommand', () => {
{type: 'UPDATE_RHS_STATE', state: 'search'},
{type: 'UPDATE_RHS_SEARCH_RESULTS_TERMS', terms: ''},
{type: 'SEARCH_POSTS_REQUEST', isGettingMore: false},
{type: 'SEARCH_FILES_REQUEST', isGettingMore: false},
]);
});
});
Expand Down Expand Up @@ -177,4 +235,52 @@ describe('executeCommand', () => {
expect(result).toEqual({data: true});
});
});

describe('app command', () => {
test('should call executeAppCall', async () => {
const state = {
...initialState,
entities: {
...initialState.entities,
general: {
...initialState.entities.general,
config: {
...initialState.entities.general.config,
FeatureFlagAppsEnabled: 'true',
},
},
},
};
store = await mockStore(state);
const f = Client4.executeAppCall;
const mocked = jest.fn().mockResolvedValue(Promise.resolve({
type: AppCallResponseTypes.OK,
markdown: 'Success',
}));
Client4.executeAppCall = mocked;

const result = await store.dispatch(executeCommand('/appid custom value1 --key2 value2', {channel_id: '123', root_id: 'root_id'}));
Client4.executeAppCall = f;

expect(mocked).toHaveBeenCalledWith({
context: {
app_id: 'appid',
channel_id: '123',
location: '/command',
root_id: 'root_id',
team_id: '456',
},
raw_command: '/appid custom value1 --key2 value2',
path: 'https://someserver.com/command',
values: {
key1: 'value1',
key2: {label: 'Value 2', value: 'value2'},
},
expand: {},
query: undefined,
selected_field: undefined,
}, 'submit');
expect(result).toEqual({data: true});
});
});
});
66 changes: 61 additions & 5 deletions actions/command.js → actions/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,38 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCurrentChannel, getRedirectChannelNameForTeam, isFavoriteChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {appsEnabled} from 'mattermost-redux/selectors/entities/apps';
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 {AppCallResponse} from 'mattermost-redux/types/apps';

import {openModal} from 'actions/views/modals';
import * as GlobalActions from 'actions/global_actions';
import * as PostActions from 'actions/post_actions.jsx';

import {isUrlSafe, getSiteURL} from 'utils/url';
import {localizeMessage, getUserIdFromChannelName} from 'utils/utils.jsx';
import {localizeMessage, getUserIdFromChannelName, localizeAndFormatMessage} from 'utils/utils.jsx';
import * as UserAgent from 'utils/user_agent';
import {Constants, ModalIdentifiers} from 'utils/constants';
import {browserHistory} from 'utils/browser_history';

import UserSettingsModal from 'components/user_settings/modal';
import {AppCommandParser} from 'components/suggestion/command_provider/app_command_parser/app_command_parser';
import {intlShim} from 'components/suggestion/command_provider/app_command_parser/app_command_parser_dependencies';

import {GlobalState} from 'types/store';

import {t} from 'utils/i18n';

import {doAppCall} from './apps';

export function executeCommand(message, args) {
return async (dispatch, getState) => {
const state = getState();
export function executeCommand(message: string, args: CommandArgs): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState() as GlobalState;

let msg = message;

Expand All @@ -47,7 +62,7 @@ export function executeCommand(message, args) {
GlobalActions.toggleShortcutsModal();
return {data: true};
case '/leave': {
// /leave command not supported in reply threads.
// /leave command not supported in reply threads.
if (args.channel_id && (args.root_id || args.parent_id)) {
GlobalActions.sendEphemeralPost('/leave is not supported in reply threads. Use it in the center channel instead.', args.channel_id, args.parent_id);
return {data: true};
Expand Down Expand Up @@ -91,6 +106,47 @@ export function executeCommand(message, args) {
case '/collapse':
case '/expand':
dispatch(PostActions.resetEmbedVisibility());
dispatch(PostActions.resetInlineImageVisibility());
}

if (appsEnabled(state)) {
const getGlobalState = () => getState() as GlobalState;
const createErrorMessage = (errMessage: string) => {
return {error: {message: errMessage}};
};
const parser = new AppCommandParser({dispatch, getState: getGlobalState} as any, intlShim, args.channel_id, args.root_id);
if (parser.isAppCommand(msg)) {
try {
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
if (!call) {
return createErrorMessage(errorMessage!);
}

const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intlShim)) as {data: AppCallResponse};

const callResp = res.data;
switch (callResp.type) {
case AppCallResponseTypes.OK:
if (callResp.markdown) {
GlobalActions.sendEphemeralPost(callResp.markdown, args.channel_id, args.parent_id, callResp.app_metadata?.bot_user_id);
}
return {data: true};
case AppCallResponseTypes.ERROR:
return createErrorMessage(callResp.error || localizeMessage('apps.error.unknown', 'Unknown error.'));
case AppCallResponseTypes.FORM:
case AppCallResponseTypes.NAVIGATE:
return {data: true};
default:
return createErrorMessage(localizeAndFormatMessage(
t('apps.error.responses.unknown_type'),
'App response type not supported. Response type: {type}.',
{type: callResp.type},
));
}
} catch (err) {
return createErrorMessage(err.message || localizeMessage('apps.error.unknown', 'Unknown error.'));
}
}
}

let data;
Expand Down
Loading

0 comments on commit d3daedd

Please sign in to comment.