Skip to content

Commit

Permalink
[MM-8338] Option to send as message when an invalid slash command is …
Browse files Browse the repository at this point in the history
…entered (mattermost#1969)

* general feature complete. changed functionality is scoped to create_post.jsx

* textbox now keeps focus after slash command error. submitting twice will force submit the message.

update create_post test

* move "message submit error" into its own component

* handle server error type correctly, and fix some i18n references

* move tests to be next to source code

* fix nested `FormattedMessage` components
  • Loading branch information
mickmister authored and sudheerDev committed Nov 20, 2018
1 parent 24e026c commit 4453f86
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 12 deletions.
46 changes: 34 additions & 12 deletions components/create_post/create_post.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {sortFileInfos} from 'mattermost-redux/utils/file_utils';

import * as GlobalActions from 'actions/global_actions.jsx';
import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants.jsx';
import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox} from 'utils/post_utils.jsx';
import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox, isErrorInvalidSlashCommand} from 'utils/post_utils.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
import * as Utils from 'utils/utils.jsx';

Expand All @@ -27,6 +27,7 @@ import Textbox from 'components/textbox.jsx';
import TutorialTip from 'components/tutorial/tutorial_tip';

import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx';
import MessageSubmitError from 'components/message_submit_error';

const KeyCodes = Constants.KeyCodes;

Expand Down Expand Up @@ -311,9 +312,18 @@ export default class CreatePost extends React.Component {
return;
}

let message = this.state.message;
let ignoreSlash = false;
const serverError = this.state.serverError;

if (serverError && isErrorInvalidSlashCommand(serverError) && serverError.submittedMessage === message) {
message = serverError.submittedMessage;
ignoreSlash = true;
}

const post = {};
post.file_ids = [];
post.message = this.state.message;
post.message = message;

if (post.message.trim().length === 0 && this.props.draft.fileInfos.length === 0) {
return;
Expand All @@ -332,7 +342,7 @@ export default class CreatePost extends React.Component {
this.setState({submitting: true, serverError: null});

const isReaction = Utils.REACTION_PATTERN.exec(post.message);
if (post.message.indexOf('/') === 0) {
if (post.message.indexOf('/') === 0 && !ignoreSlash) {
this.setState({message: '', postError: null, enableSendButton: false});
const args = {};
args.channel_id = channelId;
Expand All @@ -345,7 +355,10 @@ export default class CreatePost extends React.Component {
this.sendMessage(post);
} else {
this.setState({
serverError: error.message,
serverError: {
...error,
submittedMessage: post.message,
},
message: post.message,
});
}
Expand Down Expand Up @@ -540,9 +553,16 @@ export default class CreatePost extends React.Component {
const message = e.target.value;
const channelId = this.props.currentChannel.id;
const enableSendButton = this.handleEnableSendButton(message, this.props.draft.fileInfos);

let serverError = this.state.serverError;
if (isErrorInvalidSlashCommand(serverError)) {
serverError = null;
}

this.setState({
message,
enableSendButton,
serverError,
});

const draft = {
Expand Down Expand Up @@ -602,10 +622,9 @@ export default class CreatePost extends React.Component {
handleUploadError = (err, clientId, channelId) => {
const draft = {...this.draftsForChannel[channelId]};

let message = err;
if (message && typeof message !== 'string') {
// err is an AppError from the server
message = err.message;
let serverError = err;
if (typeof err === 'string') {
serverError = new Error(err);
}

if (clientId !== -1 && draft.uploadsInProgress) {
Expand All @@ -622,7 +641,7 @@ export default class CreatePost extends React.Component {
}
}

this.setState({serverError: message});
this.setState({serverError});
}

removePreview = (id) => {
Expand Down Expand Up @@ -926,9 +945,12 @@ export default class CreatePost extends React.Component {
let serverError = null;
if (this.state.serverError) {
serverError = (
<div className='has-error'>
<label className='control-label'>{this.state.serverError}</label>
</div>
<MessageSubmitError
id='postServerError'
error={this.state.serverError}
submittedMessage={this.state.serverError.submittedMessage}
handleSubmit={this.handleSubmit}
/>
);
}

Expand Down
82 changes: 82 additions & 0 deletions components/create_post/create_post.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -755,4 +755,86 @@ describe('components/create_post', () => {
const wrapper = shallow(createPost({canUploadFiles: false}));
expect(wrapper).toMatchSnapshot();
});

it('should allow to force send invalid slash command as a message', async () => {
const error = {
message: 'No command found',
server_error_id: 'api.command.execute_command.not_found.app_error',
};
const executeCommand = jest.fn(() => Promise.resolve({error}));
const onSubmitPost = jest.fn();

const wrapper = shallow(
createPost({
actions: {
...actionsProp,
executeCommand,
onSubmitPost,
},
})
);

wrapper.setState({
message: '/fakecommand some text',
});
expect(wrapper.find('[id="postServerError"]').exists()).toBe(false);

await wrapper.instance().handleSubmit({preventDefault: jest.fn()});
expect(executeCommand).toHaveBeenCalled();
expect(wrapper.find('[id="postServerError"]').exists()).toBe(true);
expect(onSubmitPost).not.toHaveBeenCalled();

await wrapper.instance().handleSubmit({preventDefault: jest.fn()});
expect(wrapper.find('[id="postServerError"]').exists()).toBe(false);

expect(onSubmitPost).toHaveBeenCalledWith(
expect.objectContaining({
message: '/fakecommand some text',
}),
expect.anything(),
);
});

it('should throw away invalid command error if user resumes typing', async () => {
const error = {
message: 'No command found',
server_error_id: 'api.command.execute_command.not_found.app_error',
};
const executeCommand = jest.fn(() => Promise.resolve({error}));
const onSubmitPost = jest.fn();

const wrapper = shallow(
createPost({
actions: {
...actionsProp,
executeCommand,
onSubmitPost,
},
})
);

wrapper.setState({
message: '/fakecommand some text',
});
expect(wrapper.find('[id="postServerError"]').exists()).toBe(false);

await wrapper.instance().handleSubmit({preventDefault: jest.fn()});
expect(executeCommand).toHaveBeenCalled();
expect(wrapper.find('[id="postServerError"]').exists()).toBe(true);
expect(onSubmitPost).not.toHaveBeenCalled();

wrapper.instance().handleChange({
target: {value: 'some valid text'},
});
expect(wrapper.find('[id="postServerError"]').exists()).toBe(false);

wrapper.instance().handleSubmit({preventDefault: jest.fn()});

expect(onSubmitPost).toHaveBeenCalledWith(
expect.objectContaining({
message: 'some valid text',
}),
expect.anything(),
);
});
});
67 changes: 67 additions & 0 deletions components/message_submit_error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';

import {isErrorInvalidSlashCommand} from 'utils/post_utils.jsx';

class MessageSubmitError extends React.PureComponent {
static propTypes = {
error: PropTypes.object.isRequired,
handleSubmit: PropTypes.func.isRequired,
submittedMessage: PropTypes.string,
}

renderSlashCommandError = () => {
if (!this.props.submittedMessage) {
return this.props.error.message;
}

const command = this.props.submittedMessage.split(' ')[0];
return (
<React.Fragment>
<FormattedMessage
id='message_submit_error.invalidCommand'
defaultMessage={'Command with a trigger of \'{command}\' not found. '}
values={{
command,
}}
/>
<a
href='#'
onClick={this.props.handleSubmit}
>
<FormattedMessage
id='message-submit-error.sendAsMessageLink'
defaultMessage='Click here to send as a message.'
/>
</a>
</React.Fragment>
);
}

render() {
const error = this.props.error;

if (!error) {
return null;
}

let errorContent = error.message;
if (isErrorInvalidSlashCommand(error)) {
errorContent = this.renderSlashCommandError();
}

return (
<div className='has-error'>
<label className='control-label'>
{errorContent}
</label>
</div>
);
}
}

export default MessageSubmitError;
55 changes: 55 additions & 0 deletions components/message_submit_error.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React from 'react';
import {shallow} from 'enzyme';

import MessageSubmitError from 'components/message_submit_error.jsx';

describe('components/MessageSubmitError', () => {
const baseProps = {
handleSubmit: jest.fn(),
};

it('should display the submit link if the error is for an invalid slash command', () => {
const error = {
message: 'No command found',
server_error_id: 'api.command.execute_command.not_found.app_error',
};
const submittedMessage = 'fakecommand some text';

const props = {
...baseProps,
error,
submittedMessage,
};

const wrapper = shallow(
<MessageSubmitError {...props}/>
);

expect(wrapper.find('[id="message_submit_error.invalidCommand"]').exists()).toBe(true);
expect(wrapper.text()).not.toEqual('No command found');
});

it('should not display the submit link if the error is not for an invalid slash command', () => {
const error = {
message: 'Some server error',
server_error_id: 'api.other_error',
};
const submittedMessage = '/fakecommand some text';

const props = {
...baseProps,
error,
submittedMessage,
};

const wrapper = shallow(
<MessageSubmitError {...props}/>
);

expect(wrapper.find('[id="message_submit_error.invalidCommand"]').exists()).toBe(false);
expect(wrapper.text()).toEqual('Some server error');
});
});
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2165,6 +2165,8 @@
"members_popover.manageMembers": "Manage Members",
"members_popover.title": "Channel Members",
"members_popover.viewMembers": "View Members",
"message_submit_error.sendAsMessageLink": "Click here to send as a message.",
"message_submit_error.invalidCommand": "Command with a trigger of '{command}' not found. ",
"mfa.confirm.complete": "**Set up complete!**",
"mfa.confirm.okay": "Okay",
"mfa.confirm.secure": "Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.",
Expand Down
8 changes: 8 additions & 0 deletions utils/post_utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,11 @@ export function postMessageOnKeyPress(event, message, sendMessageOnCtrlEnter, se

return {allowSending: false};
}

export function isErrorInvalidSlashCommand(error) {
if (error && error.server_error_id) {
return error.server_error_id === 'api.command.execute_command.not_found.app_error';
}

return false;
}

0 comments on commit 4453f86

Please sign in to comment.