[Apps Framework] Separate calls (#5877)
* Port https://github.com/mattermost/mattermost-webapp/pull/9263 to mobile * Fix forms props from commands and missing error->text change * lint * various fixes * fix lookups for command parser * update app command parser * fixes * fixes with embedded forms * lint and types * remove bindings.expanded fix for cleaning bindings on render * lint * re-expand bindings on post update Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
This commit is contained in:
parent
2ec503f5ef
commit
df7945318e
34 changed files with 1535 additions and 901 deletions
|
|
@ -3,17 +3,75 @@
|
|||
|
||||
import {sendEphemeralPost} from '@actions/views/post';
|
||||
import {Client4} from '@client/rest';
|
||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {AppCallResponse, AppCallRequest, AppContext, AppBinding} from '@mm-redux/types/apps';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {cleanForm, makeCallErrorResponse} from '@utils/apps';
|
||||
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
|
||||
|
||||
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
|
||||
if (binding.form?.source) {
|
||||
const callRequest = createCallRequest(
|
||||
binding.form.source,
|
||||
context,
|
||||
);
|
||||
|
||||
const res = await dispatch(doAppFetchForm<Res>(callRequest, intl));
|
||||
return res;
|
||||
}
|
||||
|
||||
// Open form
|
||||
if (binding.form) {
|
||||
// This should come properly formed, but using preventive checks
|
||||
if (!binding.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: binding.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) {
|
||||
|
|
@ -22,10 +80,10 @@ 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)};
|
||||
}
|
||||
|
|
@ -43,14 +101,6 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
|
|||
return {error: 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 {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
return {data: res};
|
||||
default: {
|
||||
const errMsg = intl.formatMessage({
|
||||
|
|
@ -72,6 +122,73 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
|
|||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
switch (responseType) {
|
||||
case AppCallResponseTypes.ERROR:
|
||||
return {error: res};
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (!res.form?.submit) {
|
||||
const errMsg = intl.formatMessage({
|
||||
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) {
|
||||
const errMsg = error.message || intl.formatMessage({
|
||||
id: 'apps.error.responses.unexpected_error',
|
||||
defaultMessage: 'Received an unexpected error.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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',
|
||||
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 postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ export function showSearchModal(initialValue = '') {
|
|||
showModal(name, title, passProps, options);
|
||||
}
|
||||
|
||||
export const showAppForm = async (form, call, theme) => {
|
||||
export const showAppForm = async (form, context, theme) => {
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
let submitButtons;
|
||||
|
|
@ -397,7 +397,7 @@ export const showAppForm = async (form, call, theme) => {
|
|||
},
|
||||
};
|
||||
|
||||
const passProps = {form, call};
|
||||
const passProps = {form, context};
|
||||
showModal('AppForm', form.title, passProps, options);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||
|
|
@ -41,19 +41,19 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
|||
if (appsAreEnabled) {
|
||||
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.team_id, args.root_id);
|
||||
if (parser.isAppCommand(msg)) {
|
||||
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
|
||||
const {creq, errorMessage} = await parser.composeCommandSubmitCall(msg);
|
||||
const createErrorMessage = (errMessage: string) => {
|
||||
return {error: {message: errMessage}};
|
||||
};
|
||||
|
||||
if (!call) {
|
||||
if (!creq) {
|
||||
return createErrorMessage(errorMessage!);
|
||||
}
|
||||
|
||||
const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
|
||||
const res = await dispatch(doAppSubmit(creq, intl));
|
||||
if (res.error) {
|
||||
const errorResponse = res.error as AppCallResponse;
|
||||
return createErrorMessage(errorResponse.error || intl.formatMessage({
|
||||
return createErrorMessage(errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
}));
|
||||
|
|
@ -61,14 +61,14 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
|||
const callResp = res.data as AppCallResponse;
|
||||
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: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
return {data: {
|
||||
form: callResp.form,
|
||||
call,
|
||||
call: creq,
|
||||
}};
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
return {data: {
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
||||
import type {AppBinding, AppCallRequest, AppCallResponse} from '@mm-redux/types/apps';
|
||||
|
||||
export interface ClientAppsMix {
|
||||
executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise<AppCallResponse>;
|
||||
executeAppCall: (call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse>;
|
||||
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
|
||||
}
|
||||
|
||||
const ClientApps = (superclass: any) => class extends superclass {
|
||||
executeAppCall = async (call: AppCallRequest, type: AppCallType) => {
|
||||
executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean) => {
|
||||
const callCopy = {
|
||||
...call,
|
||||
path: `${call.path}/${type}`,
|
||||
context: {
|
||||
...call.context,
|
||||
user_agent: 'mobile',
|
||||
track_as_submit: trackAsSubmit,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from './app_command_parser';
|
||||
import {
|
||||
AppCallResponseTypes,
|
||||
AppCallTypes,
|
||||
AutocompleteSuggestion,
|
||||
} from './app_command_parser_dependencies';
|
||||
import {
|
||||
|
|
@ -243,7 +242,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('epic2');
|
||||
expect(parsed.incompleteStart).toBe(75);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
|
|
@ -254,7 +253,7 @@ describe('AppCommandParser', () => {
|
|||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBe('epic2');
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
|
|
@ -267,7 +266,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('epic2');
|
||||
expect(parsed.incompleteStart).toBe(75);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
|
|
@ -278,7 +277,7 @@ describe('AppCommandParser', () => {
|
|||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBe('epic2');
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
|
|
@ -291,7 +290,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('M');
|
||||
expect(parsed.incompleteStart).toBe(65);
|
||||
expect(parsed.values?.project).toBe('KT');
|
||||
|
|
@ -300,7 +299,7 @@ describe('AppCommandParser', () => {
|
|||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.epic).toBe('M');
|
||||
}},
|
||||
},
|
||||
|
|
@ -310,7 +309,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('MM-123');
|
||||
expect(parsed.incompleteStart).toBe(33);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
|
|
@ -319,7 +318,7 @@ describe('AppCommandParser', () => {
|
|||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe('MM-123');
|
||||
}},
|
||||
|
|
@ -330,7 +329,7 @@ describe('AppCommandParser', () => {
|
|||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.StartParameter);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.incompleteStart).toBe(17);
|
||||
expect(parsed.values).toEqual({});
|
||||
|
|
@ -342,14 +341,14 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.FlagValueSeparator);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.values).toEqual({});
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.values).toEqual({
|
||||
summary: '',
|
||||
|
|
@ -362,7 +361,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.TickValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
|
|
@ -376,7 +375,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.QuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
|
|
@ -390,7 +389,7 @@ describe('AppCommandParser', () => {
|
|||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndQuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
|
|
@ -399,7 +398,7 @@ describe('AppCommandParser', () => {
|
|||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndQuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.resolvedForm?.submit?.path).toBe('/view-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
|
|
@ -923,7 +922,7 @@ describe('AppCommandParser', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('composeCallFromCommand', () => {
|
||||
describe('composeCommandSubmitCall', () => {
|
||||
const base = {
|
||||
context: {
|
||||
app_id: 'jira',
|
||||
|
|
@ -939,13 +938,9 @@ describe('AppCommandParser', () => {
|
|||
const cmd = '/jira issue create';
|
||||
const values = {};
|
||||
|
||||
const {call} = await parser.composeCallFromCommand(cmd);
|
||||
expect(call).toEqual({
|
||||
const {creq} = await parser.composeCommandSubmitCall(cmd);
|
||||
expect(creq).toEqual({
|
||||
...base,
|
||||
context: {
|
||||
...base.context,
|
||||
location: '/command/jira/issue/create',
|
||||
},
|
||||
raw_command: cmd,
|
||||
expand: {},
|
||||
query: undefined,
|
||||
|
|
@ -962,17 +957,13 @@ describe('AppCommandParser', () => {
|
|||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
verbose: 'true',
|
||||
verbose: true,
|
||||
project: '',
|
||||
};
|
||||
|
||||
const {call} = await parser.composeCallFromCommand(cmd);
|
||||
expect(call).toEqual({
|
||||
const {creq} = await parser.composeCommandSubmitCall(cmd);
|
||||
expect(creq).toEqual({
|
||||
...base,
|
||||
context: {
|
||||
...base.context,
|
||||
location: '/command/jira/issue/create',
|
||||
},
|
||||
expand: {},
|
||||
selected_field: undefined,
|
||||
query: undefined,
|
||||
|
|
@ -1009,7 +1000,7 @@ describe('AppCommandParser', () => {
|
|||
team_id: 'team_id',
|
||||
},
|
||||
expand: {},
|
||||
path: '/create-issue',
|
||||
path: '/create-issue-lookup',
|
||||
query: 'special',
|
||||
raw_command: '/jira issue create --summary "The summary" --epic epic1 --project special',
|
||||
selected_field: 'project',
|
||||
|
|
@ -1020,7 +1011,7 @@ describe('AppCommandParser', () => {
|
|||
value: 'epic1',
|
||||
},
|
||||
},
|
||||
}, AppCallTypes.LOOKUP);
|
||||
}, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,18 +3,22 @@
|
|||
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {getUserByUsername, getUser, autocompleteUsers} from '@mm-redux/actions/users';
|
||||
export {doAppFetchForm, doAppLookup} from '@actions/apps';
|
||||
|
||||
export {getChannelByNameAndTeamName, getChannel, autocompleteChannels} from '@mm-redux/actions/channels';
|
||||
import {getUserByUsername, getUser, autocompleteUsers, autocompleteUsersInChannel} from '@mm-redux/actions/users';
|
||||
export {AppsTypes} from '@mm-redux/action_types';
|
||||
export {makeAppBindingsSelector, makeRHSAppBindingSelector, getAppCommandForm, getAppRHSCommandForm} from '@mm-redux/selectors/entities/apps';
|
||||
export {getChannel as selectChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
|
||||
export {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {
|
||||
ActionFunc,
|
||||
DispatchFunc,
|
||||
} from '@mm-redux/types/actions';
|
||||
import Store from '@store/store';
|
||||
export {getUserByUsername as selectUserByUsername, getUser as selectUser} from '@mm-redux/selectors/entities/users';
|
||||
import {DispatchFunc} from '@mm-redux/types/actions';
|
||||
import ReduxStore from '@store/store';
|
||||
|
||||
import type {ParsedCommand} from './app_command_parser';
|
||||
import type {AutocompleteSuggestion} from '@mm-redux/types/integrations';
|
||||
|
||||
export type {
|
||||
AppCall,
|
||||
AppCallRequest,
|
||||
AppBinding,
|
||||
AppField,
|
||||
|
|
@ -29,36 +33,22 @@ export type {
|
|||
AutocompleteUserSelect,
|
||||
AutocompleteChannelSelect,
|
||||
AppLookupResponse,
|
||||
UserAutocomplete,
|
||||
} from '@mm-redux/types/apps';
|
||||
|
||||
export type {
|
||||
DoAppCallResult,
|
||||
} from 'types/actions/apps';
|
||||
|
||||
export {AppsTypes} from '@mm-redux/action_types';
|
||||
|
||||
export type {AutocompleteSuggestion};
|
||||
|
||||
export type {
|
||||
Channel,
|
||||
} from '@mm-redux/types/channels';
|
||||
|
||||
export type {
|
||||
GlobalState,
|
||||
} from '@mm-redux/types/store';
|
||||
export type {Channel} from '@mm-redux/types/channels';
|
||||
import type {AutocompleteSuggestion} from '@mm-redux/types/integrations';
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
export type {
|
||||
DispatchFunc,
|
||||
GlobalState,
|
||||
};
|
||||
|
||||
export type {
|
||||
UserProfile,
|
||||
} from '@mm-redux/types/users';
|
||||
export type {AutocompleteSuggestion};
|
||||
|
||||
export type {DoAppCallResult} from 'types/actions/apps';
|
||||
|
||||
export {
|
||||
AppBindingLocations,
|
||||
AppCallTypes,
|
||||
AppFieldTypes,
|
||||
AppCallResponseTypes,
|
||||
COMMAND_SUGGESTION_ERROR,
|
||||
|
|
@ -66,43 +56,37 @@ export {
|
|||
COMMAND_SUGGESTION_USER,
|
||||
} from '@mm-redux/constants/apps';
|
||||
|
||||
export {makeAppBindingsSelector, makeRHSAppBindingSelector, getAppCommandForm, getAppRHSCommandForm} from '@mm-redux/selectors/entities/apps';
|
||||
|
||||
export {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
export {getChannel as selectChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
|
||||
|
||||
export {
|
||||
getCurrentTeamId,
|
||||
getCurrentTeam,
|
||||
};
|
||||
|
||||
export {getUserByUsername as selectUserByUsername, getUser as selectUser} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
export {
|
||||
getUserByUsername,
|
||||
getUser,
|
||||
autocompleteUsers,
|
||||
autocompleteUsersInChannel,
|
||||
};
|
||||
|
||||
export {getChannelByNameAndTeamName, getChannel, autocompleteChannels} from '@mm-redux/actions/channels';
|
||||
|
||||
export {doAppCall} from '@actions/apps';
|
||||
export {
|
||||
createCallRequest,
|
||||
filterEmptyOptions,
|
||||
} from '@utils/apps';
|
||||
|
||||
export const getStore = () => Store.redux;
|
||||
export interface Store {
|
||||
dispatch: DispatchFunc;
|
||||
getState: () => GlobalState;
|
||||
}
|
||||
|
||||
export const autocompleteUsersInChannel = (prefix: string, channelID: string): ActionFunc => {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentTeamID = getCurrentTeamId(state);
|
||||
return dispatch(autocompleteUsers(prefix, currentTeamID, channelID));
|
||||
};
|
||||
};
|
||||
export const getStore = () => ReduxStore.redux as Store;
|
||||
|
||||
export const EXECUTE_CURRENT_COMMAND_ITEM_ID = '_execute_current_command';
|
||||
export const OPEN_COMMAND_IN_MODAL_ITEM_ID = '_open_command_in_modal';
|
||||
|
||||
export const getOpenInModalSuggestion = (_: ParsedCommand): AutocompleteSuggestion | null => { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// Not supported on mobile yet
|
||||
return null;
|
||||
};
|
||||
|
||||
export type ExtendedAutocompleteSuggestion = AutocompleteSuggestion & {
|
||||
type?: string;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export const reduxTestState = {
|
|||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: 'O',
|
||||
total_msg_count: 10,
|
||||
team_id: 'team_id',
|
||||
},
|
||||
current_user_id__existingId: {
|
||||
|
|
@ -36,13 +35,16 @@ export const reduxTestState = {
|
|||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: '0',
|
||||
total_msg_count: 0,
|
||||
team_id: 'team_id',
|
||||
},
|
||||
},
|
||||
channelsInTeam: {
|
||||
'team-id': ['current_channel_id'],
|
||||
},
|
||||
messageCounts: {
|
||||
current_channel_id: {total: 10},
|
||||
current_user_id__existingId: {total: 0},
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
|
|
@ -103,7 +105,7 @@ export const viewCommand: AppBinding = {
|
|||
location: '/command/jira/issue/view',
|
||||
description: 'View details of a Jira issue',
|
||||
form: {
|
||||
call: {
|
||||
submit: {
|
||||
path: '/view-issue',
|
||||
},
|
||||
fields: [
|
||||
|
|
@ -114,6 +116,9 @@ export const viewCommand: AppBinding = {
|
|||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
hint: 'The Jira project hint',
|
||||
is_required: true,
|
||||
lookup: {
|
||||
path: '/view-issue-lookup',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'issue',
|
||||
|
|
@ -135,7 +140,7 @@ export const createCommand: AppBinding = {
|
|||
icon: 'Create icon',
|
||||
hint: 'Create hint',
|
||||
form: {
|
||||
call: {
|
||||
submit: {
|
||||
path: '/create-issue',
|
||||
},
|
||||
fields: [
|
||||
|
|
@ -145,6 +150,9 @@ export const createCommand: AppBinding = {
|
|||
description: 'The Jira project description',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
hint: 'The Jira project hint',
|
||||
lookup: {
|
||||
path: '/create-issue-lookup',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'summary',
|
||||
|
|
@ -189,7 +197,7 @@ export const restCommand: AppBinding = {
|
|||
icon: 'rest icon',
|
||||
hint: 'rest hint',
|
||||
form: {
|
||||
call: {
|
||||
submit: {
|
||||
path: '/create-issue',
|
||||
},
|
||||
fields: [
|
||||
|
|
@ -252,6 +260,9 @@ export const testBindings: AppBinding[] = [
|
|||
label: 'sub1',
|
||||
description: 'Some Description',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/submit_other',
|
||||
},
|
||||
fields: [{
|
||||
name: 'fieldname',
|
||||
label: 'fieldlabel',
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ export default class DraftInput extends PureComponent {
|
|||
}
|
||||
|
||||
if (data.form) {
|
||||
showAppForm(data.form, data.call, theme);
|
||||
showAppForm(data.form, data.call.context, theme);
|
||||
}
|
||||
|
||||
this.setInputValue('');
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {intlShape, injectIntl} from 'react-intl';
|
|||
import Button from 'react-native-button';
|
||||
|
||||
import {showAppForm} from '@actions/navigation';
|
||||
import {AppExpandLevels, AppBindingLocations, AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {AppBindingLocations, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {AppBinding} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {DoAppCall, PostEphemeralCallResponseForPost} from '@mm-types/actions/apps';
|
||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||
import {HandleBindingClick, PostEphemeralCallResponseForPost} from '@mm-types/actions/apps';
|
||||
import {createCallContext} from '@utils/apps';
|
||||
import {getStatusColors} from '@utils/message_attachment_colors';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
|
@ -21,7 +21,7 @@ import ButtonBindingText from './button_binding_text';
|
|||
|
||||
type Props = {
|
||||
binding: AppBinding;
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
intl: typeof intlShape;
|
||||
post: Post;
|
||||
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||
|
|
@ -53,7 +53,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
};
|
||||
});
|
||||
|
||||
const ButtonBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseForPost, teamID, theme, handleGotoLocation}: Props) => {
|
||||
const ButtonBinding = ({binding, handleBindingClick, intl, post, postEphemeralCallResponseForPost, teamID, theme, handleGotoLocation}: Props) => {
|
||||
const pressed = useRef(false);
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
|
|
@ -62,12 +62,6 @@ const ButtonBinding = ({binding, doAppCall, intl, post, postEphemeralCallRespons
|
|||
return;
|
||||
}
|
||||
|
||||
const call = binding.form?.call || binding.call;
|
||||
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
AppBindingLocations.IN_POST + binding.location,
|
||||
|
|
@ -76,25 +70,14 @@ const ButtonBinding = ({binding, doAppCall, intl, post, postEphemeralCallRespons
|
|||
post.id,
|
||||
);
|
||||
|
||||
const callRequest = createCallRequest(
|
||||
call,
|
||||
context,
|
||||
{post: AppExpandLevels.EXPAND_ALL},
|
||||
);
|
||||
|
||||
if (binding.form) {
|
||||
showAppForm(binding.form, callRequest, theme);
|
||||
return;
|
||||
}
|
||||
|
||||
pressed.current = true;
|
||||
|
||||
const res = await doAppCall(callRequest, AppCallTypes.SUBMIT, intl);
|
||||
const res = await handleBindingClick(binding, context, intl);
|
||||
pressed.current = false;
|
||||
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.error || intl.formatMessage({
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
|
|
@ -106,15 +89,15 @@ const ButtonBinding = ({binding, doAppCall, intl, post, postEphemeralCallRespons
|
|||
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
postEphemeralCallResponseForPost(callResp, callResp.markdown, post);
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForPost(callResp, callResp.text, post);
|
||||
}
|
||||
return;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
handleGotoLocation(callResp.navigate_to_url!, intl);
|
||||
return;
|
||||
case AppCallResponseTypes.FORM:
|
||||
showAppForm(callResp.form, call, theme);
|
||||
showAppForm(callResp.form, context, theme);
|
||||
return;
|
||||
default: {
|
||||
const errorMessage = intl.formatMessage({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||
import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
|
|
@ -32,7 +32,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
doAppCall,
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForPost,
|
||||
handleGotoLocation,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {AppBindingLocations} from '@mm-redux/constants/apps';
|
||||
|
|
@ -39,9 +39,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
});
|
||||
|
||||
const EmbeddedBinding = ({embed, postId, theme}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const [cleanedBindings, setCleanedBindings] = useState<AppBinding[]>([]);
|
||||
|
||||
const bindings = cleanBinding(embed, AppBindingLocations.IN_POST)?.bindings;
|
||||
useEffect(() => {
|
||||
const copiedBindings = JSON.parse(JSON.stringify(embed)) as AppBinding;
|
||||
const bindings = cleanBinding(copiedBindings, AppBindingLocations.IN_POST)?.bindings;
|
||||
setCleanedBindings(bindings!);
|
||||
}, [embed]);
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -58,9 +64,9 @@ const EmbeddedBinding = ({embed, postId, theme}: Props) => {
|
|||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{Boolean(bindings?.length) &&
|
||||
{Boolean(cleanedBindings?.length) &&
|
||||
<EmbedSubBindings
|
||||
bindings={bindings!}
|
||||
bindings={cleanedBindings}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const EmbeddedSubBindings = ({bindings, postId, theme}: Props) => {
|
|||
const content = [] as React.ReactNode[];
|
||||
|
||||
bindings.forEach((binding) => {
|
||||
if (!binding.app_id || !binding.call) {
|
||||
if (!binding.app_id || !(binding.submit || binding.form?.submit || binding.form?.source || binding.bindings?.length)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||
import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
|
|
@ -32,7 +32,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
doAppCall,
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForPost,
|
||||
handleGotoLocation,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,19 +6,19 @@ import {intlShape, injectIntl} from 'react-intl';
|
|||
|
||||
import {showAppForm} from '@actions/navigation';
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import {AppExpandLevels, AppBindingLocations, AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {AppBindingLocations, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||
import {createCallContext} from '@utils/apps';
|
||||
|
||||
import type {AppBinding} from '@mm-redux/types/apps';
|
||||
import type {PostActionOption} from '@mm-redux/types/integration_actions';
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {DoAppCall, PostEphemeralCallResponseForPost} from '@mm-types/actions/apps';
|
||||
import type {HandleBindingClick, PostEphemeralCallResponseForPost} from '@mm-types/actions/apps';
|
||||
|
||||
type Props = {
|
||||
binding: AppBinding;
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
intl: typeof intlShape;
|
||||
post: Post;
|
||||
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||
|
|
@ -27,7 +27,7 @@ type Props = {
|
|||
theme: Theme;
|
||||
}
|
||||
|
||||
const MenuBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseForPost, handleGotoLocation, teamID, theme}: Props) => {
|
||||
const MenuBinding = ({binding, handleBindingClick, intl, post, postEphemeralCallResponseForPost, handleGotoLocation, teamID, theme}: Props) => {
|
||||
const [selected, setSelected] = useState<PostActionOption>();
|
||||
|
||||
const onSelect = useCallback(async (picked?: PostActionOption) => {
|
||||
|
|
@ -42,12 +42,6 @@ const MenuBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseF
|
|||
return;
|
||||
}
|
||||
|
||||
const call = bind.form?.call || bind.call;
|
||||
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = createCallContext(
|
||||
bind.app_id,
|
||||
AppBindingLocations.IN_POST + bind.location,
|
||||
|
|
@ -56,21 +50,10 @@ const MenuBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseF
|
|||
post.id,
|
||||
);
|
||||
|
||||
const callRequest = createCallRequest(
|
||||
call,
|
||||
context,
|
||||
{post: AppExpandLevels.EXPAND_ALL},
|
||||
);
|
||||
|
||||
if (bind.form) {
|
||||
showAppForm(bind.form, callRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await doAppCall(callRequest, AppCallTypes.SUBMIT, intl);
|
||||
const res = await handleBindingClick(bind, context, intl);
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.error || intl.formatMessage({
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
|
|
@ -81,15 +64,15 @@ const MenuBinding = ({binding, doAppCall, intl, post, postEphemeralCallResponseF
|
|||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
postEphemeralCallResponseForPost(callResp, callResp.markdown, post);
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForPost(callResp, callResp.text, post);
|
||||
}
|
||||
return;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
handleGotoLocation(callResp.navigate_to_url!, intl);
|
||||
return;
|
||||
case AppCallResponseTypes.FORM:
|
||||
showAppForm(callResp.form, call, theme);
|
||||
showAppForm(callResp.form, context, theme);
|
||||
return;
|
||||
default: {
|
||||
const errorMessage = intl.formatMessage({
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const contentType: Record<string, string> = {
|
|||
};
|
||||
|
||||
const Content = ({isReplyPost, post, theme}: ContentProps) => {
|
||||
let type: string = post.metadata?.embeds[0]?.type;
|
||||
let type: string = post.metadata?.embeds?.[0]?.type;
|
||||
if (!type && post.props?.app_bindings) {
|
||||
type = contentType.app_bindings;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import {Client4} from '@client/rest';
|
||||
import {analytics} from '@init/analytics';
|
||||
import {UserTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUsers} from '@mm-redux/selectors/entities/users';
|
||||
import {Action, ActionFunc, ActionResult, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {TeamMembership} from '@mm-redux/types/teams';
|
||||
|
|
@ -741,6 +742,14 @@ export function autocompleteUsers(term: string, teamId = '', channelId = '', opt
|
|||
};
|
||||
}
|
||||
|
||||
export function autocompleteUsersInChannel(prefix: string, channelID: string): ActionFunc {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentTeamID = getCurrentTeamId(state);
|
||||
return dispatch(autocompleteUsers(prefix, currentTeamID, channelID));
|
||||
};
|
||||
}
|
||||
|
||||
export function searchProfiles(term: string, options: any = {}): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AppCallResponseType, AppCallType, AppExpandLevel, AppFieldType} from '@mm-redux/types/apps';
|
||||
import {AppCallResponseType, AppExpandLevel, AppFieldType} from '@mm-redux/types/apps';
|
||||
|
||||
export const AppBindingLocations = {
|
||||
POST_MENU_ITEM: '/post_menu',
|
||||
|
|
@ -22,13 +22,6 @@ export const AppCallResponseTypes: { [name: string]: AppCallResponseType } = {
|
|||
NAVIGATE: 'navigate',
|
||||
};
|
||||
|
||||
export const AppCallTypes: { [name: string]: AppCallType } = {
|
||||
SUBMIT: 'submit',
|
||||
LOOKUP: 'lookup',
|
||||
FORM: 'form',
|
||||
CANCEL: 'cancel',
|
||||
};
|
||||
|
||||
export const AppExpandLevels: { [name: string]: AppExpandLevel } = {
|
||||
EXPAND_DEFAULT: '',
|
||||
EXPAND_NONE: 'none',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
|
|
@ -19,7 +23,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
|
|
@ -31,14 +39,22 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"icon": "icon",
|
||||
"label": "b",
|
||||
"location": "/channel_header/locB",
|
||||
},
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c",
|
||||
"location": "/channel_header/locC",
|
||||
},
|
||||
|
|
@ -50,14 +66,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"icon": "icon",
|
||||
"label": "locB",
|
||||
"location": "/channel_header/locB",
|
||||
},
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"icon": "icon",
|
||||
"label": "c",
|
||||
"location": "/channel_header/locC",
|
||||
|
|
@ -70,13 +83,21 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "locB",
|
||||
"location": "/channel_header/locB",
|
||||
},
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c",
|
||||
"location": "/channel_header/locC",
|
||||
},
|
||||
|
|
@ -88,7 +109,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c",
|
||||
"location": "/command/locC",
|
||||
},
|
||||
|
|
@ -105,13 +130,21 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locB",
|
||||
},
|
||||
|
|
@ -123,7 +156,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"icon": "icon",
|
||||
"label": "b",
|
||||
"location": "/channel_header/locB",
|
||||
|
|
@ -139,13 +176,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"label": "subC1",
|
||||
"location": "/command/locC/subC1",
|
||||
},
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c2",
|
||||
"location": "/command/locC/subC2",
|
||||
},
|
||||
|
|
@ -153,31 +188,6 @@ Array [
|
|||
"label": "c",
|
||||
"location": "/command/locC",
|
||||
},
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"label": "subC1",
|
||||
"location": "/command/locD/subC1",
|
||||
},
|
||||
],
|
||||
"label": "d",
|
||||
"location": "/command/locD",
|
||||
},
|
||||
],
|
||||
"location": "/command",
|
||||
},
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"label": "locC",
|
||||
"location": "/command/locC",
|
||||
},
|
||||
],
|
||||
"location": "/command",
|
||||
},
|
||||
|
|
@ -189,18 +199,25 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c1",
|
||||
"location": "/command/locC/subC1",
|
||||
},
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c2",
|
||||
"location": "/command/locC/subC2",
|
||||
},
|
||||
],
|
||||
"call": Object {},
|
||||
"label": "c",
|
||||
"location": "/command/locC",
|
||||
},
|
||||
|
|
@ -217,13 +234,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"label": "locA",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locB",
|
||||
},
|
||||
|
|
@ -235,37 +250,37 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "b",
|
||||
"location": "/post_menu/locB",
|
||||
},
|
||||
],
|
||||
"location": "/post_menu",
|
||||
},
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"label": "locA",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
],
|
||||
"location": "/post_menu",
|
||||
},
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"icon": "icon",
|
||||
"label": "b",
|
||||
"location": "/channel_header/locB",
|
||||
|
|
@ -278,7 +293,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c",
|
||||
"location": "/command/locC",
|
||||
},
|
||||
|
|
@ -295,7 +314,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
|
|
@ -307,7 +330,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "2",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "a",
|
||||
"location": "/post_menu/locA",
|
||||
},
|
||||
|
|
@ -319,7 +346,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "1",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"icon": "icon",
|
||||
"label": "b",
|
||||
"location": "/channel_header/locB",
|
||||
|
|
@ -332,7 +363,11 @@ Array [
|
|||
"bindings": Array [
|
||||
Object {
|
||||
"app_id": "3",
|
||||
"call": Object {},
|
||||
"form": Object {
|
||||
"submit": Object {
|
||||
"path": "/submit_url",
|
||||
},
|
||||
},
|
||||
"label": "c",
|
||||
"location": "/command/locC",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import * as Reducers from './apps';
|
|||
|
||||
describe('bindings', () => {
|
||||
const initialState = [];
|
||||
|
||||
const basicSubmitForm = {
|
||||
submit: {
|
||||
path: '/submit_url',
|
||||
},
|
||||
};
|
||||
test('No element get filtered', () => {
|
||||
const data = [
|
||||
{
|
||||
|
|
@ -17,7 +21,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locA',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -28,7 +32,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locA',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -40,7 +44,7 @@ describe('bindings', () => {
|
|||
location: 'locB',
|
||||
label: 'b',
|
||||
icon: 'icon',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -51,7 +55,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -77,7 +81,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locA',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -88,7 +92,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locA',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -100,12 +104,12 @@ describe('bindings', () => {
|
|||
location: 'locB',
|
||||
label: 'b',
|
||||
icon: 'icon',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -114,15 +118,14 @@ describe('bindings', () => {
|
|||
location: '/channel_header',
|
||||
bindings: [
|
||||
{
|
||||
location: 'locB',
|
||||
icon: 'icon',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
icon: 'icon',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -132,12 +135,12 @@ describe('bindings', () => {
|
|||
bindings: [
|
||||
{
|
||||
location: 'locB',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -148,7 +151,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -172,13 +175,12 @@ describe('bindings', () => {
|
|||
location: '/post_menu',
|
||||
bindings: [
|
||||
{
|
||||
location: 'locA',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'locB',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -189,12 +191,12 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locA',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'locB',
|
||||
label: 'b',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -203,8 +205,7 @@ describe('bindings', () => {
|
|||
location: '/post_menu',
|
||||
bindings: [
|
||||
{
|
||||
location: 'locA',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -216,7 +217,7 @@ describe('bindings', () => {
|
|||
location: 'locB',
|
||||
label: 'b',
|
||||
icon: 'icon',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -227,7 +228,7 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -253,12 +254,12 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locA',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'locB',
|
||||
label: 'a',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -270,7 +271,7 @@ describe('bindings', () => {
|
|||
location: 'locB',
|
||||
label: 'b',
|
||||
icon: 'icon',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -283,13 +284,12 @@ describe('bindings', () => {
|
|||
label: 'c',
|
||||
bindings: [
|
||||
{
|
||||
location: 'subC1',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'subC2',
|
||||
label: 'c2',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -298,8 +298,7 @@ describe('bindings', () => {
|
|||
label: 'd',
|
||||
bindings: [
|
||||
{
|
||||
location: 'subC1',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -310,8 +309,7 @@ describe('bindings', () => {
|
|||
location: '/command',
|
||||
bindings: [
|
||||
{
|
||||
location: 'locC',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -322,17 +320,16 @@ describe('bindings', () => {
|
|||
{
|
||||
location: 'locC',
|
||||
label: 'c',
|
||||
call: {},
|
||||
bindings: [
|
||||
{
|
||||
location: 'subC1',
|
||||
label: 'c1',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'subC2',
|
||||
label: 'c2',
|
||||
call: {},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export type AppManifest = {
|
|||
display_name: string;
|
||||
description?: string;
|
||||
homepage_url?: string;
|
||||
root_url: string;
|
||||
}
|
||||
|
||||
export type AppModalState = {
|
||||
|
|
@ -50,19 +49,17 @@ export type AppBinding = {
|
|||
depends_on_user?: boolean;
|
||||
depends_on_post?: boolean;
|
||||
|
||||
// A Binding is either to a Call, or is a "container" for other locations -
|
||||
// i.e. menu sub-items or subcommands.
|
||||
call?: AppCall;
|
||||
// A Binding is either an action (makes a call), a Form, or is a
|
||||
// "container" for other locations - i.e. menu sub-items or subcommands.
|
||||
bindings?: AppBinding[];
|
||||
form?: AppForm;
|
||||
submit?: AppCall;
|
||||
};
|
||||
|
||||
export type AppCallValues = {
|
||||
[name: string]: any;
|
||||
};
|
||||
|
||||
export type AppCallType = string;
|
||||
|
||||
export type AppCall = {
|
||||
path: string;
|
||||
expand?: AppExpand;
|
||||
|
|
@ -81,9 +78,8 @@ export type AppCallResponseType = string;
|
|||
|
||||
export type AppCallResponse<Res = unknown> = {
|
||||
type: AppCallResponseType;
|
||||
markdown?: string;
|
||||
text?: string;
|
||||
data?: Res;
|
||||
error?: string;
|
||||
navigate_to_url?: string;
|
||||
use_external_browser?: boolean;
|
||||
call?: AppCall;
|
||||
|
|
@ -107,6 +103,7 @@ export type AppContext = {
|
|||
root_id?: string;
|
||||
props?: AppContextProps;
|
||||
user_agent?: string;
|
||||
track_as_submit?: boolean;
|
||||
};
|
||||
|
||||
export type AppContextProps = {
|
||||
|
|
@ -136,8 +133,19 @@ export type AppForm = {
|
|||
submit_buttons?: string;
|
||||
cancel_button?: boolean;
|
||||
submit_on_cancel?: boolean;
|
||||
fields: AppField[];
|
||||
call?: AppCall;
|
||||
fields?: AppField[];
|
||||
|
||||
// source is used in 2 cases:
|
||||
// - if submit is not set, it is used to fetch the submittable form from
|
||||
// the app.
|
||||
// - if a select field change triggers a refresh, the form is refreshed
|
||||
// from source.
|
||||
source?: AppCall;
|
||||
|
||||
// submit is called when one of the submit buttons is pressed, or the
|
||||
// command is executed.
|
||||
submit?: AppCall;
|
||||
|
||||
depends_on?: string[];
|
||||
};
|
||||
|
||||
|
|
@ -176,6 +184,7 @@ export type AppField = {
|
|||
refresh?: boolean;
|
||||
options?: AppSelectOption[];
|
||||
multiselect?: boolean;
|
||||
lookup?: AppCall;
|
||||
|
||||
// Text props
|
||||
subtype?: string;
|
||||
|
|
|
|||
|
|
@ -17,14 +17,11 @@ describe('AppsForm', () => {
|
|||
submit: jest.fn(),
|
||||
handleGotoLocation: jest.fn(),
|
||||
},
|
||||
call: {
|
||||
context: {
|
||||
app_id: 'app1',
|
||||
},
|
||||
path: '/create',
|
||||
},
|
||||
componentId: '',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/create',
|
||||
},
|
||||
title: 'Title',
|
||||
footer: 'Footer',
|
||||
header: 'Header',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import Markdown from '@components/markdown';
|
|||
import StatusBar from '@components/status_bar';
|
||||
import {AppCallResponseTypes, AppFieldTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {AppCallRequest, AppField, AppForm, AppFormValue, AppFormValues, AppLookupResponse, AppSelectOption, FormResponseData} from '@mm-redux/types/apps';
|
||||
import {AppField, AppForm, AppFormValue, AppFormValues, AppLookupResponse, AppSelectOption, FormResponseData} from '@mm-redux/types/apps';
|
||||
import {DialogElement} from '@mm-redux/types/integrations';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@mm-redux/utils/integration_utils';
|
||||
|
|
@ -29,7 +29,6 @@ import AppsFormField from './apps_form_field';
|
|||
import DialogIntroductionText from './dialog_introduction_text';
|
||||
|
||||
export type Props = {
|
||||
call: AppCallRequest;
|
||||
form: AppForm;
|
||||
actions: {
|
||||
submit: (submission: {
|
||||
|
|
@ -155,7 +154,7 @@ export default class AppsFormComponent extends PureComponent<Props, State> {
|
|||
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.error;
|
||||
const errorMessage = errorResponse.text;
|
||||
const hasErrors = this.updateErrors(elements, errorResponse.data?.errors, errorMessage);
|
||||
if (!hasErrors) {
|
||||
this.handleHide();
|
||||
|
|
@ -225,7 +224,7 @@ export default class AppsFormComponent extends PureComponent<Props, State> {
|
|||
|
||||
performLookup = async (name: string, userInput: string): Promise<AppSelectOption[]> => {
|
||||
const intl = this.context.intl;
|
||||
const field = this.props.form.fields.find((f) => f.name === name);
|
||||
const field = this.props.form.fields?.find((f) => f.name === name);
|
||||
if (!field) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -233,7 +232,7 @@ export default class AppsFormComponent extends PureComponent<Props, State> {
|
|||
const res = await this.props.actions.performLookupCall(field, this.state.values, userInput);
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errMsg = errorResponse.error || intl.formatMessage({
|
||||
const errMsg = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
});
|
||||
|
|
@ -294,7 +293,7 @@ export default class AppsFormComponent extends PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
onChange = (name: string, value: any) => {
|
||||
const field = this.props.form.fields.find((f) => f.name === name);
|
||||
const field = this.props.form.fields?.find((f) => f.name === name);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -305,7 +304,7 @@ export default class AppsFormComponent extends PureComponent<Props, State> {
|
|||
this.props.actions.refreshOnSelect(field, values, value).then((res) => {
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMsg = errorResponse.error;
|
||||
const errorMsg = errorResponse.text;
|
||||
const errors = errorResponse.data?.errors;
|
||||
const elements = fieldsAsElements(this.props.form.fields);
|
||||
this.updateErrors(elements, errors, errorMsg);
|
||||
|
|
|
|||
|
|
@ -4,20 +4,22 @@
|
|||
import React, {PureComponent} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse, AppCallRequest, AppField, AppForm, AppFormValues, FormResponseData, AppLookupResponse} from '@mm-redux/types/apps';
|
||||
import {AppCallResponse, AppField, AppForm, AppFormValues, FormResponseData, AppLookupResponse, AppContext} from '@mm-redux/types/apps';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {DoAppCall, DoAppCallResult, PostEphemeralCallResponseForContext} from '@mm-types/actions/apps';
|
||||
import {makeCallErrorResponse} from '@utils/apps';
|
||||
import {DoAppCallResult, DoAppFetchForm, DoAppLookup, DoAppSubmit, PostEphemeralCallResponseForContext} from '@mm-types/actions/apps';
|
||||
import {createCallRequest, makeCallErrorResponse} from '@utils/apps';
|
||||
|
||||
import AppsFormComponent from './apps_form_component';
|
||||
|
||||
export type Props = {
|
||||
form?: AppForm;
|
||||
call?: AppCallRequest;
|
||||
context?: AppContext;
|
||||
actions: {
|
||||
doAppCall: DoAppCall<any>;
|
||||
doAppSubmit: DoAppSubmit<any>;
|
||||
doAppFetchForm: DoAppFetchForm<any>;
|
||||
doAppLookup: DoAppLookup<any>;
|
||||
postEphemeralCallResponseForContext: PostEphemeralCallResponseForContext;
|
||||
handleGotoLocation: (href: string, intl: any) => Promise<ActionResult>;
|
||||
};
|
||||
|
|
@ -61,20 +63,21 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||
)))};
|
||||
}
|
||||
|
||||
const call = this.getCall();
|
||||
if (!call) {
|
||||
if (!form.submit) {
|
||||
return {error: makeCallErrorResponse(makeErrorMsg(intl.formatMessage(
|
||||
{
|
||||
id: 'apps.error.form.no_call',
|
||||
defaultMessage: '`call` is not defined',
|
||||
id: 'apps.error.form.no_submit',
|
||||
defaultMessage: '`submit` is not defined',
|
||||
},
|
||||
)))};
|
||||
}
|
||||
|
||||
const res = await this.props.actions.doAppCall({
|
||||
...call,
|
||||
values: submission.values,
|
||||
}, AppCallTypes.SUBMIT, intl);
|
||||
if (!this.props.context) {
|
||||
return {error: makeCallErrorResponse('unreachable: empty context')};
|
||||
}
|
||||
|
||||
const creq = createCallRequest(form.submit, this.props.context, {}, submission.values);
|
||||
const res = await this.props.actions.doAppSubmit(creq, intl) as DoAppCallResult<FormResponseData>;
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
|
|
@ -83,8 +86,8 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
this.props.actions.postEphemeralCallResponseForContext(callResp, callResp.markdown, call.context);
|
||||
if (callResp.text) {
|
||||
this.props.actions.postEphemeralCallResponseForContext(callResp, callResp.text, creq.context);
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.FORM:
|
||||
|
|
@ -122,11 +125,10 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||
})))};
|
||||
}
|
||||
|
||||
const call = this.getCall();
|
||||
if (!call) {
|
||||
if (!form.source) {
|
||||
return {error: makeCallErrorResponse(makeErrorMsg(intl.formatMessage({
|
||||
id: 'apps.error.form.no_call',
|
||||
defaultMessage: '`call` is not defined.',
|
||||
id: 'apps.error.form.no_source',
|
||||
defaultMessage: '`source` is not defined.',
|
||||
})))};
|
||||
}
|
||||
|
||||
|
|
@ -138,12 +140,14 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||
})))};
|
||||
}
|
||||
|
||||
const res = await this.props.actions.doAppCall({
|
||||
...call,
|
||||
selected_field: field.name,
|
||||
values,
|
||||
if (!this.props.context) {
|
||||
return {error: makeCallErrorResponse('unreachable: empty context')};
|
||||
}
|
||||
|
||||
}, AppCallTypes.FORM, intl);
|
||||
const creq = createCallRequest(form.source, this.props.context, {}, values);
|
||||
creq.selected_field = field.name;
|
||||
|
||||
const res = await this.props.actions.doAppFetchForm(creq, intl);
|
||||
|
||||
if (res.error) {
|
||||
return res;
|
||||
|
|
@ -183,54 +187,33 @@ export default class AppsFormContainer extends PureComponent<Props, State> {
|
|||
},
|
||||
{details: message},
|
||||
);
|
||||
const call = this.getCall();
|
||||
if (!call) {
|
||||
return makeErrorMsg(intl.formatMessage({id: 'apps.error.form.no_lookup_call', defaultMessage: 'performLookupCall props.call is not defined'}));
|
||||
if (!field.lookup) {
|
||||
return {error: makeCallErrorResponse(makeErrorMsg(intl.formatMessage({
|
||||
id: 'apps.error.form.no_lookup',
|
||||
defaultMessage: '`lookup` is not defined.',
|
||||
})))};
|
||||
}
|
||||
|
||||
return this.props.actions.doAppCall({
|
||||
...call,
|
||||
values,
|
||||
selected_field: field.name,
|
||||
query: userInput,
|
||||
}, AppCallTypes.LOOKUP, intl);
|
||||
};
|
||||
|
||||
getCall = (): AppCallRequest | null => {
|
||||
const {form} = this.state;
|
||||
|
||||
const {call} = this.props;
|
||||
if (!call) {
|
||||
return null;
|
||||
if (!this.props.context) {
|
||||
return {error: makeCallErrorResponse('unreachable: empty context')};
|
||||
}
|
||||
|
||||
return {
|
||||
...call,
|
||||
...form?.call,
|
||||
context: {
|
||||
...call.context,
|
||||
},
|
||||
values: {
|
||||
...call.values,
|
||||
},
|
||||
};
|
||||
const creq = createCallRequest(field.lookup, this.props.context, {}, values);
|
||||
creq.selected_field = field.name;
|
||||
creq.query = userInput;
|
||||
|
||||
return this.props.actions.doAppLookup(creq, intl);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {form} = this.state;
|
||||
if (!form) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const call = this.getCall();
|
||||
if (!call) {
|
||||
if (!form?.submit || !this.props.context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppsFormComponent
|
||||
form={form}
|
||||
call={call}
|
||||
actions={{
|
||||
submit: this.handleSubmit,
|
||||
performLookupCall: this.performLookupCall,
|
||||
|
|
|
|||
|
|
@ -4,17 +4,19 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForContext} from '@actions/apps';
|
||||
import {doAppFetchForm, doAppLookup, doAppSubmit, postEphemeralCallResponseForContext} from '@actions/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {ActionFunc, GenericAction} from '@mm-redux/types/actions';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {DoAppCall, PostEphemeralCallResponseForContext} from '@mm-types/actions/apps';
|
||||
import {DoAppFetchForm, DoAppLookup, DoAppSubmit, PostEphemeralCallResponseForContext} from '@mm-types/actions/apps';
|
||||
|
||||
import AppsFormContainer from './apps_form_container';
|
||||
|
||||
type Actions = {
|
||||
doAppCall: DoAppCall;
|
||||
doAppSubmit: DoAppSubmit<any>;
|
||||
doAppFetchForm: DoAppFetchForm<any>;
|
||||
doAppLookup: DoAppLookup<any>;
|
||||
postEphemeralCallResponseForContext: PostEphemeralCallResponseForContext;
|
||||
};
|
||||
|
||||
|
|
@ -27,7 +29,9 @@ function mapStateToProps(state: GlobalState) {
|
|||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||
doAppCall,
|
||||
doAppSubmit,
|
||||
doAppFetchForm,
|
||||
doAppLookup,
|
||||
postEphemeralCallResponseForContext,
|
||||
handleGotoLocation,
|
||||
}, dispatch),
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import {intlShape, injectIntl} from 'react-intl';
|
|||
import {Alert} from 'react-native';
|
||||
|
||||
import {dismissModal, showAppForm} from '@actions/navigation';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {AppBinding} from '@mm-redux/types/apps';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {DoAppCall, PostEphemeralCallResponseForChannel} from '@mm-types/actions/apps';
|
||||
import {HandleBindingClick, PostEphemeralCallResponseForChannel} from '@mm-types/actions/apps';
|
||||
import Separator from '@screens/channel_info/separator';
|
||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||
import {createCallContext} from '@utils/apps';
|
||||
|
||||
import ChannelInfoRow from '../channel_info_row';
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ type Props = {
|
|||
intl: typeof intlShape;
|
||||
currentTeamId: string;
|
||||
actions: {
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
handleGotoLocation: (href: string, intl: any) => Promise<ActionResult>;
|
||||
};
|
||||
|
|
@ -70,7 +70,7 @@ type OptionProps = {
|
|||
intl: typeof intlShape;
|
||||
currentTeamId: string;
|
||||
actions: {
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
handleGotoLocation: (href: string, intl: any) => Promise<ActionResult>;
|
||||
};
|
||||
|
|
@ -87,38 +87,22 @@ class Option extends React.PureComponent<OptionProps, OptionState> {
|
|||
|
||||
onPress = async () => {
|
||||
const {binding, currentChannel, currentTeamId, intl, theme} = this.props;
|
||||
const {doAppCall, postEphemeralCallResponseForChannel} = this.props.actions;
|
||||
const {handleBindingClick, postEphemeralCallResponseForChannel} = this.props.actions;
|
||||
|
||||
if (this.state.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const call = binding.form?.call || binding.call;
|
||||
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
binding.location,
|
||||
currentChannel.id,
|
||||
currentChannel.team_id || currentTeamId,
|
||||
);
|
||||
const callRequest = createCallRequest(
|
||||
call,
|
||||
context,
|
||||
);
|
||||
|
||||
if (binding.form) {
|
||||
await dismissModal();
|
||||
showAppForm(binding.form, callRequest, theme);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({submitting: true});
|
||||
|
||||
const res = await doAppCall(callRequest, AppCallTypes.SUBMIT, intl);
|
||||
const res = await handleBindingClick(binding, context, intl);
|
||||
|
||||
this.setState({submitting: false});
|
||||
|
||||
|
|
@ -128,7 +112,7 @@ class Option extends React.PureComponent<OptionProps, OptionState> {
|
|||
id: 'mobile.general.error.title',
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
const errorMessage = errorResponse.error || intl.formatMessage({
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
|
|
@ -139,8 +123,8 @@ class Option extends React.PureComponent<OptionProps, OptionState> {
|
|||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
postEphemeralCallResponseForChannel(callResp, callResp.markdown, currentChannel.id);
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForChannel(callResp, callResp.text, currentChannel.id);
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
|
|
@ -149,7 +133,7 @@ class Option extends React.PureComponent<OptionProps, OptionState> {
|
|||
return;
|
||||
case AppCallResponseTypes.FORM:
|
||||
await dismissModal();
|
||||
showAppForm(callResp.form, call, theme);
|
||||
showAppForm(callResp.form, context, theme);
|
||||
return;
|
||||
default: {
|
||||
const title = intl.formatMessage({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForChannel} from '@actions/apps';
|
||||
import {handleBindingClick, postEphemeralCallResponseForChannel} from '@actions/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {AppBindingLocations} from '@mm-redux/constants/apps';
|
||||
import {makeAppBindingsSelector} from '@mm-redux/selectors/entities/apps';
|
||||
|
|
@ -13,7 +13,7 @@ import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
|||
import {GenericAction, ActionFunc} from '@mm-redux/types/actions';
|
||||
import {AppBinding} from '@mm-redux/types/apps';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {DoAppCall, PostEphemeralCallResponseForChannel} from '@mm-types/actions/apps';
|
||||
import {HandleBindingClick, PostEphemeralCallResponseForChannel} from '@mm-types/actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
import Bindings from './bindings';
|
||||
|
|
@ -35,14 +35,14 @@ function mapStateToProps(state: GlobalState) {
|
|||
}
|
||||
|
||||
type Actions = {
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||
doAppCall,
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForChannel,
|
||||
handleGotoLocation,
|
||||
}, dispatch),
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@
|
|||
import React, {useState, useEffect} from 'react';
|
||||
import {intlShape, injectIntl} from 'react-intl';
|
||||
import {Alert} from 'react-native';
|
||||
import {DoAppCall, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||
import {HandleBindingClick, PostEphemeralCallResponseForPost} from 'types/actions/apps';
|
||||
|
||||
import {showAppForm} from '@actions/navigation';
|
||||
import {Client4} from '@client/rest';
|
||||
import {AppBindingLocations, AppCallResponseTypes, AppCallTypes, AppExpandLevels} from '@mm-redux/constants/apps';
|
||||
import {AppBindingLocations, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {AppBinding, AppCallResponse} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {isSystemMessage} from '@mm-redux/utils/post_utils';
|
||||
import {createCallContext, createCallRequest} from '@utils/apps';
|
||||
import {createCallContext} from '@utils/apps';
|
||||
|
||||
import PostOption from '../post_option';
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ type Props = {
|
|||
appsEnabled: boolean;
|
||||
intl: typeof intlShape;
|
||||
actions: {
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||
handleGotoLocation: (href: string, intl: any) => Promise<ActionResult>;
|
||||
};
|
||||
|
|
@ -101,7 +101,7 @@ type OptionProps = {
|
|||
closeWithAnimation: (cb?: () => void) => void;
|
||||
intl: typeof intlShape;
|
||||
actions: {
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||
handleGotoLocation: (href: string, intl: any) => Promise<ActionResult>;
|
||||
};
|
||||
|
|
@ -110,13 +110,7 @@ type OptionProps = {
|
|||
class Option extends React.PureComponent<OptionProps> {
|
||||
onPress = async () => {
|
||||
const {post, teamID, binding, intl, theme} = this.props;
|
||||
const {doAppCall, postEphemeralCallResponseForPost} = this.props.actions;
|
||||
|
||||
const call = binding.form?.call || binding.call;
|
||||
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
const {handleBindingClick, postEphemeralCallResponseForPost} = this.props.actions;
|
||||
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
|
|
@ -126,20 +120,8 @@ class Option extends React.PureComponent<OptionProps> {
|
|||
post.id,
|
||||
post.root_id,
|
||||
);
|
||||
const callRequest = createCallRequest(
|
||||
call,
|
||||
context,
|
||||
{
|
||||
post: AppExpandLevels.ALL,
|
||||
},
|
||||
);
|
||||
|
||||
if (binding.form) {
|
||||
showAppForm(binding.form, callRequest, theme);
|
||||
return;
|
||||
}
|
||||
|
||||
const callPromise = doAppCall(callRequest, AppCallTypes.SUBMIT, intl);
|
||||
const callPromise = handleBindingClick(binding, context, intl);
|
||||
await this.close();
|
||||
|
||||
const res = await callPromise;
|
||||
|
|
@ -149,7 +131,7 @@ class Option extends React.PureComponent<OptionProps> {
|
|||
id: 'mobile.general.error.title',
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
const errorMessage = errorResponse.error || intl.formatMessage({
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
|
|
@ -160,15 +142,15 @@ class Option extends React.PureComponent<OptionProps> {
|
|||
const callResp = (res as {data: AppCallResponse}).data;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
postEphemeralCallResponseForPost(callResp, callResp.markdown, post);
|
||||
if (callResp.text) {
|
||||
postEphemeralCallResponseForPost(callResp, callResp.text, post);
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
this.props.actions.handleGotoLocation(callResp.navigate_to_url!, intl);
|
||||
break;
|
||||
case AppCallResponseTypes.FORM:
|
||||
showAppForm(callResp.form, callRequest, theme);
|
||||
showAppForm(callResp.form, context, theme);
|
||||
break;
|
||||
default: {
|
||||
const title = intl.formatMessage({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||
import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {AppBindingLocations} from '@mm-redux/constants/apps';
|
||||
import {getThreadAppsBindingsChannelId, makeAppBindingsSelector, makeRHSAppBindingSelector} from '@mm-redux/selectors/entities/apps';
|
||||
|
|
@ -16,7 +16,7 @@ import {GenericAction, ActionFunc} from '@mm-redux/types/actions';
|
|||
import {AppBinding} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {DoAppCall, PostEphemeralCallResponseForPost} from '@mm-types/actions/apps';
|
||||
import {HandleBindingClick, PostEphemeralCallResponseForPost} from '@mm-types/actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
import Bindings from './bindings';
|
||||
|
|
@ -56,14 +56,14 @@ function mapStateToProps(state: GlobalState, props: OwnProps) {
|
|||
}
|
||||
|
||||
type Actions = {
|
||||
doAppCall: DoAppCall;
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForPost: PostEphemeralCallResponseForPost;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc>, Actions>({
|
||||
doAppCall,
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForPost,
|
||||
handleGotoLocation,
|
||||
}, dispatch),
|
||||
|
|
|
|||
|
|
@ -7,72 +7,70 @@ import {AppBinding, AppCall, AppField, AppForm, AppSelectOption} from '@mm-redux
|
|||
import {cleanForm, cleanBinding} from './apps';
|
||||
|
||||
describe('Apps Utils', () => {
|
||||
describe('cleanBindings', () => {
|
||||
test('Apps IDs, and Calls propagate down, and locations get formed', () => {
|
||||
const basicCall: AppCall = {
|
||||
path: 'url',
|
||||
};
|
||||
const basicSubmitForm: AppForm = {
|
||||
submit: basicCall,
|
||||
};
|
||||
const basicFetchForm: AppForm = {
|
||||
source: basicCall,
|
||||
};
|
||||
|
||||
describe('fillAndTrimBindingsInformation', () => {
|
||||
test('Apps IDs propagate down, and locations get formed', () => {
|
||||
const inBinding: AppBinding = {
|
||||
app_id: 'id',
|
||||
location: 'loc1',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc2',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc3',
|
||||
} as AppBinding,
|
||||
submit: basicCall,
|
||||
},
|
||||
{
|
||||
location: 'loc4',
|
||||
} as AppBinding,
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
location: 'loc5',
|
||||
} as AppBinding,
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
const outBinding: AppBinding = {
|
||||
app_id: 'id',
|
||||
location: 'loc1',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2',
|
||||
app_id: 'id',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2/loc3',
|
||||
app_id: 'id',
|
||||
label: 'loc3',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
} as AppBinding,
|
||||
submit: basicCall,
|
||||
},
|
||||
{
|
||||
location: 'loc1/loc2/loc4',
|
||||
app_id: 'id',
|
||||
label: 'loc4',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
} as AppBinding,
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
location: 'loc1/loc5',
|
||||
app_id: 'id',
|
||||
label: 'loc5',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
|
@ -81,138 +79,6 @@ describe('Apps Utils', () => {
|
|||
expect(inBinding).toEqual(outBinding);
|
||||
});
|
||||
|
||||
test('Do not overwrite calls nor ids on the way down.', () => {
|
||||
const inBinding: AppBinding = {
|
||||
app_id: 'id',
|
||||
location: 'loc1',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'id2',
|
||||
location: 'loc2',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc3',
|
||||
} as AppBinding,
|
||||
{
|
||||
location: 'loc4',
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding,
|
||||
{
|
||||
call: {
|
||||
path: 'url2',
|
||||
} as AppCall,
|
||||
location: 'loc5',
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
const outBinding: AppBinding = {
|
||||
app_id: 'id',
|
||||
location: 'loc1',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2',
|
||||
app_id: 'id2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2/loc3',
|
||||
app_id: 'id2',
|
||||
label: 'loc3',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
} as AppBinding,
|
||||
{
|
||||
location: 'loc1/loc2/loc4',
|
||||
app_id: 'id2',
|
||||
label: 'loc4',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding,
|
||||
{
|
||||
location: 'loc1/loc5',
|
||||
app_id: 'id',
|
||||
label: 'loc5',
|
||||
call: {
|
||||
path: 'url2',
|
||||
} as AppCall,
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
cleanBinding(inBinding, '');
|
||||
expect(inBinding).toEqual(outBinding);
|
||||
});
|
||||
|
||||
test('Trim branches without app_id.', () => {
|
||||
const inBinding: AppBinding = {
|
||||
location: 'loc1',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc2',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc3',
|
||||
} as AppBinding,
|
||||
{
|
||||
app_id: 'id',
|
||||
location: 'loc4',
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding,
|
||||
{
|
||||
location: 'loc5',
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
const outBinding: AppBinding = {
|
||||
location: 'loc1',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2/loc4',
|
||||
app_id: 'id',
|
||||
label: 'loc4',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding,
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
cleanBinding(inBinding, '');
|
||||
expect(inBinding).toEqual(outBinding);
|
||||
});
|
||||
|
||||
test('Trim branches without call.', () => {
|
||||
const inBinding: AppBinding = {
|
||||
location: 'loc1',
|
||||
|
|
@ -223,18 +89,28 @@ describe('Apps Utils', () => {
|
|||
bindings: [
|
||||
{
|
||||
location: 'loc3',
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
location: 'loc4',
|
||||
call: {
|
||||
path: 'url',
|
||||
},
|
||||
} as AppBinding,
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'loc5',
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
location: 'loc5',
|
||||
} as AppBinding,
|
||||
location: 'loc6',
|
||||
},
|
||||
{
|
||||
location: 'loc7',
|
||||
submit: basicCall,
|
||||
},
|
||||
{
|
||||
location: 'loc8',
|
||||
form: {},
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
|
|
@ -250,84 +126,140 @@ describe('Apps Utils', () => {
|
|||
{
|
||||
location: 'loc1/loc2/loc4',
|
||||
app_id: 'id',
|
||||
call: {
|
||||
path: 'url',
|
||||
},
|
||||
form: basicSubmitForm,
|
||||
label: 'loc4',
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
location: 'loc1/loc2/loc5',
|
||||
app_id: 'id',
|
||||
form: basicFetchForm,
|
||||
label: 'loc5',
|
||||
},
|
||||
],
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
location: 'loc1/loc7',
|
||||
app_id: 'id',
|
||||
submit: basicCall,
|
||||
label: 'loc7',
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
cleanBinding(inBinding, '');
|
||||
expect(inBinding).toEqual(outBinding);
|
||||
});
|
||||
|
||||
test('Trim mixed invalid branches.', () => {
|
||||
test('Trim branches with calls, bindings and forms.', () => {
|
||||
const inBinding: AppBinding = {
|
||||
location: 'loc1',
|
||||
app_id: 'id',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc2',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc3',
|
||||
} as AppBinding,
|
||||
submit: basicCall,
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'loc4',
|
||||
call: {
|
||||
path: 'url',
|
||||
} as AppCall,
|
||||
} as AppBinding,
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
{
|
||||
location: 'loc5',
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding,
|
||||
},
|
||||
{
|
||||
app_id: 'id',
|
||||
location: 'loc5',
|
||||
} as AppBinding,
|
||||
location: 'loc6',
|
||||
submit: basicCall,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc9',
|
||||
submit: basicCall,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'loc7',
|
||||
submit: basicCall,
|
||||
},
|
||||
{
|
||||
location: 'loc8',
|
||||
form: basicFetchForm,
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc10',
|
||||
submit: basicCall,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
const outBinding: AppBinding = {
|
||||
app_id: 'id',
|
||||
location: 'loc1',
|
||||
bindings: [] as AppBinding[],
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2',
|
||||
app_id: 'id',
|
||||
label: 'loc2',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc1/loc2/loc4',
|
||||
app_id: 'id',
|
||||
form: basicSubmitForm,
|
||||
label: 'loc4',
|
||||
},
|
||||
{
|
||||
location: 'loc1/loc2/loc5',
|
||||
app_id: 'id',
|
||||
form: basicFetchForm,
|
||||
label: 'loc5',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'loc1/loc7',
|
||||
app_id: 'id',
|
||||
submit: basicCall,
|
||||
label: 'loc7',
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
cleanBinding(inBinding, '');
|
||||
expect(inBinding).toEqual(outBinding);
|
||||
});
|
||||
test('Do not filter bindings with no call but with a form with a call', () => {
|
||||
const inBinding = {
|
||||
app_id: 'appID',
|
||||
test('Trim branches with whitespace labels.', () => {
|
||||
const inBinding: AppBinding = {
|
||||
location: 'loc1',
|
||||
app_id: 'id',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc2',
|
||||
form: {
|
||||
call: {
|
||||
path: 'url',
|
||||
label: ' ',
|
||||
bindings: [
|
||||
{
|
||||
location: 'loc4',
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
},
|
||||
} as AppBinding,
|
||||
{
|
||||
location: 'loc5',
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
||||
const outBinding = {
|
||||
app_id: 'appID',
|
||||
const outBinding: AppBinding = {
|
||||
app_id: 'id',
|
||||
location: 'loc1',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'appID',
|
||||
location: 'loc1/loc2',
|
||||
label: 'loc2',
|
||||
form: {
|
||||
call: {
|
||||
path: 'url',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [] as AppBinding[],
|
||||
} as AppBinding;
|
||||
|
||||
cleanBinding(inBinding, '');
|
||||
|
|
@ -424,7 +356,7 @@ describe('Apps Utils', () => {
|
|||
cleanForm(inForm);
|
||||
expect(inForm).toEqual(outForm);
|
||||
});
|
||||
test('field filter with same label inerred from name', () => {
|
||||
test('field filter with same label inferred from name', () => {
|
||||
const inForm: AppForm = {
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -814,6 +746,58 @@ describe('Apps Utils', () => {
|
|||
],
|
||||
};
|
||||
|
||||
cleanForm(inForm);
|
||||
expect(inForm).toEqual(outForm);
|
||||
});
|
||||
test('field filter dynamic with no valid lookup call', () => {
|
||||
const inForm: AppForm = {
|
||||
fields: [
|
||||
{
|
||||
name: 'opt1',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
lookup: basicCall,
|
||||
},
|
||||
{
|
||||
name: 'opt2',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
},
|
||||
],
|
||||
};
|
||||
const outForm: AppForm = {
|
||||
fields: [
|
||||
{
|
||||
name: 'opt1',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
lookup: basicCall,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(inForm);
|
||||
expect(inForm).toEqual(outForm);
|
||||
});
|
||||
test('invalid dynamic field does not consume namespace', () => {
|
||||
const inForm: AppForm = {
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
},
|
||||
{
|
||||
name: 'field1',
|
||||
type: AppFieldTypes.TEXT,
|
||||
},
|
||||
],
|
||||
};
|
||||
const outForm: AppForm = {
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
type: AppFieldTypes.TEXT,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
cleanForm(inForm);
|
||||
expect(inForm).toEqual(outForm);
|
||||
});
|
||||
|
|
@ -832,17 +816,13 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
},
|
||||
submit: basicCall,
|
||||
},
|
||||
{
|
||||
app_id: 'app',
|
||||
location: 'loc12',
|
||||
label: 'loc12',
|
||||
call: {
|
||||
path: '/path',
|
||||
},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -850,9 +830,7 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
},
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
|
@ -868,17 +846,13 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc1/loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
},
|
||||
submit: basicCall,
|
||||
},
|
||||
{
|
||||
app_id: 'app',
|
||||
location: '/command/loc1/loc12',
|
||||
label: 'loc12',
|
||||
call: {
|
||||
path: '/path',
|
||||
},
|
||||
form: basicSubmitForm,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -886,9 +860,7 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
},
|
||||
form: basicFetchForm,
|
||||
},
|
||||
],
|
||||
} as AppBinding;
|
||||
|
|
@ -909,14 +881,18 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'app',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
} as AppBinding,
|
||||
],
|
||||
|
|
@ -925,8 +901,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -943,8 +921,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc1/loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -953,8 +933,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -976,15 +958,19 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'app',
|
||||
location: 'loc1 2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
} as AppBinding,
|
||||
],
|
||||
|
|
@ -993,8 +979,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1011,8 +999,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc1/loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1021,8 +1011,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1044,16 +1036,20 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'app',
|
||||
location: 'loc12',
|
||||
label: 'loc1 2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1062,8 +1058,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1080,8 +1078,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc1/loc11',
|
||||
label: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1090,8 +1090,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1113,16 +1115,20 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'same',
|
||||
description: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
} as AppBinding,
|
||||
{
|
||||
app_id: 'app',
|
||||
location: 'same',
|
||||
description: 'loc12',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
} as AppBinding,
|
||||
],
|
||||
|
|
@ -1131,8 +1137,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1150,8 +1158,10 @@ describe('Apps Utils', () => {
|
|||
location: '/command/loc1/same',
|
||||
label: 'same',
|
||||
description: 'loc11',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
} as AppBinding,
|
||||
],
|
||||
|
|
@ -1160,8 +1170,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1183,16 +1195,20 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc11',
|
||||
label: 'same',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
app_id: 'app',
|
||||
location: 'loc12',
|
||||
label: 'same',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1201,8 +1217,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1219,8 +1237,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc1/loc11',
|
||||
label: 'same',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1229,8 +1249,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1252,8 +1274,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc11',
|
||||
label: 'loc 1 1',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1262,8 +1286,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: 'loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -1275,8 +1301,10 @@ describe('Apps Utils', () => {
|
|||
app_id: 'app',
|
||||
location: '/command/loc2',
|
||||
label: 'loc2',
|
||||
call: {
|
||||
path: '/path',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import {AppBindingLocations, AppCallResponseTypes, AppFieldTypes} from '@mm-redux/constants/apps';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {AppBinding, AppCall, AppCallRequest, AppCallValues, AppContext, AppExpand, AppField, AppForm, AppSelectOption} from '@mm-redux/types/apps';
|
||||
import {AppBinding, AppCall, AppCallRequest, AppCallResponse, AppCallValues, AppContext, AppExpand, AppField, AppForm, AppSelectOption} from '@mm-redux/types/apps';
|
||||
import {Config} from '@mm-redux/types/config';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
|
|
@ -24,16 +24,6 @@ function cleanBindingRec(binding: AppBinding, topLocation: string, depth: number
|
|||
const usedLabels: {[label: string]: boolean} = {};
|
||||
binding.bindings?.forEach((b, i) => {
|
||||
// Inheritance and defaults
|
||||
if (!b.call && binding.call) {
|
||||
b.call = binding.call;
|
||||
}
|
||||
|
||||
if (b.form) {
|
||||
cleanForm(b.form);
|
||||
} else if (binding.form) {
|
||||
b.form = binding.form;
|
||||
}
|
||||
|
||||
if (!b.app_id) {
|
||||
b.app_id = binding.app_id;
|
||||
}
|
||||
|
|
@ -45,7 +35,13 @@ function cleanBindingRec(binding: AppBinding, topLocation: string, depth: number
|
|||
b.location = binding.location + '/' + b.location;
|
||||
|
||||
// Validation
|
||||
if (!b.label) {
|
||||
if (!b.app_id) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
||||
// No empty labels nor "whitespace" labels
|
||||
if (!b.label.trim()) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
|
@ -72,7 +68,19 @@ function cleanBindingRec(binding: AppBinding, topLocation: string, depth: number
|
|||
}
|
||||
}
|
||||
|
||||
if (b.bindings?.length) {
|
||||
// Must have only subbindings, a form or a submit call.
|
||||
const hasBindings = Boolean(b.bindings?.length);
|
||||
const hasForm = Boolean(b.form);
|
||||
const hasSubmit = Boolean(b.submit);
|
||||
if ((!hasBindings && !hasForm && !hasSubmit) ||
|
||||
(hasBindings && hasForm) ||
|
||||
(hasBindings && hasSubmit) ||
|
||||
(hasForm && hasSubmit)) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasBindings) {
|
||||
cleanBindingRec(b, topLocation, depth + 1);
|
||||
|
||||
// Remove invalid branches
|
||||
|
|
@ -80,18 +88,13 @@ function cleanBindingRec(binding: AppBinding, topLocation: string, depth: number
|
|||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Remove leaves without a call
|
||||
if (!b.call && !b.form?.call) {
|
||||
} else if (hasForm) {
|
||||
if (!b.form?.submit && !b.form?.source) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove leaves without app id
|
||||
if (!b.app_id) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
cleanForm(b.form);
|
||||
}
|
||||
|
||||
usedLabels[b.label] = true;
|
||||
|
|
@ -150,19 +153,26 @@ export function cleanForm(form?: AppForm): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (field.type === AppFieldTypes.STATIC_SELECT) {
|
||||
switch (field.type) {
|
||||
case AppFieldTypes.STATIC_SELECT:
|
||||
cleanStaticSelect(field);
|
||||
if (!field.options?.length) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
if (!field.lookup) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
usedLabels[label] = true;
|
||||
});
|
||||
|
||||
toRemove.forEach((i) => {
|
||||
form.fields.splice(i, 1);
|
||||
form.fields!.splice(i, 1);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -224,8 +234,6 @@ export function createCallRequest(
|
|||
defaultExpand: AppExpand = {},
|
||||
values?: AppCallValues,
|
||||
rawCommand?: string,
|
||||
query?: string,
|
||||
selectedField?: string,
|
||||
): AppCallRequest {
|
||||
return {
|
||||
...call,
|
||||
|
|
@ -236,15 +244,13 @@ export function createCallRequest(
|
|||
...call.expand,
|
||||
},
|
||||
raw_command: rawCommand,
|
||||
query,
|
||||
selected_field: selectedField,
|
||||
};
|
||||
}
|
||||
|
||||
export const makeCallErrorResponse = (errMessage: string) => {
|
||||
export const makeCallErrorResponse = (errMessage: string): AppCallResponse<any> => {
|
||||
return {
|
||||
type: AppCallResponseTypes.ERROR,
|
||||
error: errMessage,
|
||||
text: errMessage,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
autocompleteChannels,
|
||||
AutocompleteSuggestion,
|
||||
autocompleteUsersInChannel,
|
||||
Channel,
|
||||
COMMAND_SUGGESTION_CHANNEL,
|
||||
COMMAND_SUGGESTION_USER,
|
||||
DispatchFunc,
|
||||
GlobalState,
|
||||
UserAutocomplete,
|
||||
UserProfile,
|
||||
} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser_dependencies';
|
||||
import {autocompleteChannels} from '@mm-redux/actions/channels';
|
||||
import {autocompleteUsersInChannel} from '@mm-redux/actions/users';
|
||||
import {COMMAND_SUGGESTION_CHANNEL, COMMAND_SUGGESTION_USER} from '@mm-redux/constants/apps';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
|
||||
import type {DispatchFunc} from '@mm-redux/types/actions';
|
||||
import type {UserAutocomplete} from '@mm-redux/types/apps';
|
||||
import type {AutocompleteSuggestion} from '@mm-redux/types/integrations';
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
import type {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
interface Store {
|
||||
dispatch: DispatchFunc;
|
||||
|
|
|
|||
|
|
@ -16,35 +16,45 @@
|
|||
"api.channel.guest_join_channel.post_and_forget": "{username} joined the channel as a guest.",
|
||||
"apps.error": "Error: {error}",
|
||||
"apps.error.command.field_missing": "Required fields missing: `{fieldName}`.",
|
||||
"apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.same_option": "Option repeated for field `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.same_user": "User repeated for field `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.unknown_channel": "Unknown channel for field `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.unknown_option": "Unknown option for field `{fieldName}`: `{option}`.",
|
||||
"apps.error.command.unknown_user": "Unknown user for field `{fieldName}`: `{option}`.",
|
||||
"apps.error.form.no_call": "`call` is not defined.",
|
||||
"apps.error.form.no_form": "`form` is not defined.",
|
||||
"apps.error.form.no_lookup_call": "performLookupCall props.call is not defined",
|
||||
"apps.error.form.no_lookup": "`lookup` is not defined.",
|
||||
"apps.error.form.no_source": "`source` is not defined.",
|
||||
"apps.error.form.no_submit": "`submit` is not defined",
|
||||
"apps.error.form.refresh": "There has been an error fetching the select fields. Contact the app developer. Details: {details}",
|
||||
"apps.error.form.refresh_no_refresh": "Called refresh on no refresh field.",
|
||||
"apps.error.form.submit.pretext": "There has been an error submitting the modal. Contact the app developer. Details: {details}",
|
||||
"apps.error.lookup.error_preparing_request": "Error preparing lookup request: {errorMessage}",
|
||||
"apps.error.malformed_binding": "This binding is not properly formed. Contact the App developer.",
|
||||
"apps.error.parser": "Parsing error: {error}",
|
||||
"apps.error.parser.empty_value": "empty values are not allowed",
|
||||
"apps.error.parser.execute_non_leaf": "You must select a subcommand.",
|
||||
"apps.error.parser.missing_binding": "Missing command bindings.",
|
||||
"apps.error.parser.missing_call": "Missing binding call.",
|
||||
"apps.error.parser.missing_field_value": "Field value is missing.",
|
||||
"apps.error.parser.missing_list_end": "Expected list closing token.",
|
||||
"apps.error.parser.missing_quote": "Matching double quote expected before end of input.",
|
||||
"apps.error.parser.missing_source": "Form has neither submit nor source.",
|
||||
"apps.error.parser.missing_submit": "No submit call in binding or form.",
|
||||
"apps.error.parser.missing_tick": "Matching tick quote expected before end of input.",
|
||||
"apps.error.parser.multiple_equal": "Multiple `=` signs are not allowed.",
|
||||
"apps.error.parser.no_argument_pos_x": "Unable to identify argument.",
|
||||
"apps.error.parser.no_bindings": "No command bindings.",
|
||||
"apps.error.parser.no_form": "No form found.",
|
||||
"apps.error.parser.no_match": "`{command}`: No matching command found in this workspace.",
|
||||
"apps.error.parser.no_slash_start": "Command must start with a `/`.",
|
||||
"apps.error.parser.unexpected_character": "Unexpected character.",
|
||||
"apps.error.parser.unexpected_comma": "Unexpected comma.",
|
||||
"apps.error.parser.unexpected_error": "Unexpected error.",
|
||||
"apps.error.parser.unexpected_flag": "Command does not accept flag `{flagName}`.",
|
||||
"apps.error.parser.unexpected_squared_bracket": "Unexpected list opening.",
|
||||
"apps.error.parser.unexpected_state": "Unreachable: Unexpected state in matchBinding: `{state}`.",
|
||||
"apps.error.parser.unexpected_whitespace": "Unreachable: Unexpected whitespace.",
|
||||
"apps.error.responses.form.no_form": "Response type is `form`, but no form was included in response.",
|
||||
"apps.error.responses.navigate.no_submit": "Response type is `navigate`, but the call was not a submission.",
|
||||
"apps.error.responses.navigate.no_url": "Response type is `navigate`, but no url was included in response.",
|
||||
"apps.error.responses.unexpected_error": "Received an unexpected error.",
|
||||
"apps.error.responses.unexpected_type": "App response type was not expected. Response type: {type}.",
|
||||
|
|
|
|||
22
types/actions/apps.d.ts
vendored
22
types/actions/apps.d.ts
vendored
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AppCallRequest, AppCallResponse, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {AppCallRequest, AppCallResponse, AppContext} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
|
||||
export type DoAppCallResult<Res=unknown> = {
|
||||
|
|
@ -9,10 +9,6 @@ export type DoAppCallResult<Res=unknown> = {
|
|||
error?: AppCallResponse<Res>;
|
||||
}
|
||||
|
||||
export interface DoAppCall<Res=unknown> {
|
||||
(call: AppCallRequest, type: AppCallType, intl: any): Promise<DoAppCallResult<Res>>;
|
||||
}
|
||||
|
||||
export interface PostEphemeralCallResponseForPost {
|
||||
(response: AppCallResponse, message: string, post: Post): void;
|
||||
}
|
||||
|
|
@ -24,3 +20,19 @@ export interface PostEphemeralCallResponseForChannel {
|
|||
export interface PostEphemeralCallResponseForContext {
|
||||
(response: AppCallResponse, message: string, context: AppContext): void;
|
||||
}
|
||||
|
||||
export interface HandleBindingClick<Res=unknown> {
|
||||
(binding: AppBinding, context: AppContext, intl: any): Promise<DoAppCallResult<Res>>;
|
||||
}
|
||||
|
||||
export interface DoAppSubmit<Res=unknown> {
|
||||
(call: AppCallRequest, intl: any): Promise<DoAppCallResult<Res>>;
|
||||
}
|
||||
|
||||
export interface DoAppFetchForm<Res=unknown> {
|
||||
(call: AppCallRequest, intl: any): Promise<DoAppCallResult<Res>>;
|
||||
}
|
||||
|
||||
export interface DoAppLookup<Res=unknown> {
|
||||
(call: AppCallRequest, intl: any): Promise<DoAppCallResult<Res>>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue