summaryrefslogtreecommitdiffstats
path: root/webapp
diff options
context:
space:
mode:
author94117nl <rttededersixtwo@gmail.com>2017-07-31 07:26:04 -0500
committerJoram Wilander <jwawilander@gmail.com>2017-07-31 08:26:04 -0400
commit6ec24867bc75057fa58c11e80a6b28334473983b (patch)
tree637c40df9dd49ec4beec99df9b78e90a867846d2 /webapp
parentf740698dbe06816921d2a20eea876c9ca7b515ed (diff)
downloadchat-6ec24867bc75057fa58c11e80a6b28334473983b.tar.gz
chat-6ec24867bc75057fa58c11e80a6b28334473983b.tar.bz2
chat-6ec24867bc75057fa58c11e80a6b28334473983b.zip
GH-6448 Migrate edit_command.jsx to be pure and use Redux (#6858)
* Migrate edit_command.jsx to be pure and use Redux, add basic test * Update newCommand to reference modified command * Fix typo * Remove unnecessary re-renders
Diffstat (limited to 'webapp')
-rw-r--r--webapp/components/integrations/components/edit_command/edit_command.jsx (renamed from webapp/components/integrations/components/edit_command.jsx)193
-rw-r--r--webapp/components/integrations/components/edit_command/index.js31
-rw-r--r--webapp/routes/route_integrations.jsx2
-rw-r--r--webapp/tests/components/integrations/__snapshots__/edit_command.test.jsx.snap424
-rw-r--r--webapp/tests/components/integrations/edit_command.test.jsx36
5 files changed, 586 insertions, 100 deletions
diff --git a/webapp/components/integrations/components/edit_command.jsx b/webapp/components/integrations/components/edit_command/edit_command.jsx
index 817eb7367..588047fb3 100644
--- a/webapp/components/integrations/components/edit_command.jsx
+++ b/webapp/components/integrations/components/edit_command/edit_command.jsx
@@ -4,57 +4,62 @@
import React from 'react';
import PropTypes from 'prop-types';
-import IntegrationStore from 'stores/integration_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
+import {FormattedMessage} from 'react-intl';
+import {browserHistory, Link} from 'react-router/es6';
+import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
-import {loadTeamCommands, editCommand} from 'actions/integration_actions.jsx';
-import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
-import {FormattedMessage} from 'react-intl';
import FormError from 'components/form_error.jsx';
-import {browserHistory, Link} from 'react-router/es6';
import SpinnerButton from 'components/spinner_button.jsx';
-import Constants from 'utils/constants.jsx';
import ConfirmModal from 'components/confirm_modal.jsx';
+import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
const REQUEST_POST = 'P';
const REQUEST_GET = 'G';
-export default class EditCommand extends React.Component {
- static get propTypes() {
- return {
- team: PropTypes.object,
- location: PropTypes.object
- };
+export default class EditCommand extends React.PureComponent {
+ static propTypes = {
+
+ /**
+ * The current team
+ */
+ team: PropTypes.object.isRequired,
+
+ /**
+ * The id of the command to edit
+ */
+ commandId: PropTypes.string.isRequired,
+
+ /**
+ * Installed slash commands to display
+ */
+ commands: PropTypes.object,
+
+ /**
+ * The request state for editCommand action. Contains status and error
+ */
+ editCommandRequest: PropTypes.object.isRequired,
+
+ actions: PropTypes.shape({
+
+ /**
+ * The function to call to fetch team commands
+ */
+ getCustomTeamCommands: PropTypes.func.isRequired,
+
+ /**
+ * The function to call to edit command
+ */
+ editCommand: PropTypes.func.isRequired
+ }).isRequired
}
constructor(props) {
super(props);
- this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
-
- this.submitCommand = this.submitCommand.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleUpdate = this.handleUpdate.bind(this);
- this.handleConfirmModal = this.handleConfirmModal.bind(this);
- this.confirmModalDismissed = this.confirmModalDismissed.bind(this);
-
- this.updateDisplayName = this.updateDisplayName.bind(this);
- this.updateDescription = this.updateDescription.bind(this);
- this.updateTrigger = this.updateTrigger.bind(this);
- this.updateUrl = this.updateUrl.bind(this);
- this.updateMethod = this.updateMethod.bind(this);
- this.updateUsername = this.updateUsername.bind(this);
- this.updateIconUrl = this.updateIconUrl.bind(this);
- this.updateAutocomplete = this.updateAutocomplete.bind(this);
- this.updateAutocompleteHint = this.updateAutocompleteHint.bind(this);
- this.updateAutocompleteDescription = this.updateAutocompleteDescription.bind(this);
-
this.originalCommand = null;
this.newCommand = null;
- const teamId = TeamStore.getCurrentId();
-
this.state = {
displayName: '',
description: '',
@@ -70,81 +75,68 @@ export default class EditCommand extends React.Component {
serverError: '',
clientError: null,
showConfirmModal: false,
- commands: IntegrationStore.getCommands(teamId),
- loading: !IntegrationStore.hasReceivedCommands(teamId)
+ loading: true
};
}
componentDidMount() {
- IntegrationStore.addChangeListener(this.handleIntegrationChange);
-
if (window.mm_config.EnableCommands === 'true') {
- loadTeamCommands();
+ this.props.actions.getCustomTeamCommands(this.props.team.id).then(
+ () => {
+ this.originalCommand = Object.values(this.props.commands).filter((command) => command.id === this.props.commandId)[0];
+ this.setState({
+ displayName: this.originalCommand.display_name,
+ description: this.originalCommand.description,
+ trigger: this.originalCommand.trigger,
+ url: this.originalCommand.url,
+ method: this.originalCommand.method,
+ username: this.originalCommand.username,
+ iconUrl: this.originalCommand.icon_url,
+ autocomplete: this.originalCommand.auto_complete,
+ autocompleteHint: this.originalCommand.auto_complete_hint,
+ autocompleteDescription: this.originalCommand.auto_complete_desc,
+ loading: false
+ });
+ }
+ );
}
}
- componentWillUnmount() {
- IntegrationStore.removeChangeListener(this.handleIntegrationChange);
- }
-
- handleConfirmModal() {
+ handleConfirmModal = () => {
this.setState({showConfirmModal: true});
}
- confirmModalDismissed() {
+ confirmModalDismissed = () => {
this.setState({showConfirmModal: false});
}
- submitCommand() {
- editCommand(
- this.newCmd,
- browserHistory.push('/' + this.props.team.name + '/integrations/commands'),
- (err) => {
- this.setState({
- saving: false,
- serverError: err.message
- });
- }
- );
+ submitCommand = async () => {
+ const data = await this.props.actions.editCommand(this.newCommand);
+
+ if (data) {
+ browserHistory.push(`/${this.props.team.name}/integrations/commands`);
+ return;
+ }
+
+ if (this.props.editCommandRequest.error) {
+ this.setState({
+ saving: false,
+ serverError: this.props.editCommandRequest.error.message
+ });
+ }
}
- handleUpdate() {
+ handleUpdate = async () => {
this.setState({
saving: true,
serverError: '',
clientError: ''
});
- this.submitCommand();
- }
-
- handleIntegrationChange() {
- const teamId = TeamStore.getCurrentId();
-
- this.setState({
- commands: IntegrationStore.getCommands(teamId),
- loading: !IntegrationStore.hasReceivedCommands(teamId)
- });
-
- if (!this.state.loading) {
- this.originalCommand = this.state.commands.filter((command) => command.id === this.props.location.query.id)[0];
-
- this.setState({
- displayName: this.originalCommand.display_name,
- description: this.originalCommand.description,
- trigger: this.originalCommand.trigger,
- url: this.originalCommand.url,
- method: this.originalCommand.method,
- username: this.originalCommand.username,
- iconUrl: this.originalCommand.icon_url,
- autocomplete: this.originalCommand.auto_complete,
- autocompleteHint: this.originalCommand.auto_complete_hint,
- autocompleteDescription: this.originalCommand.auto_complete_desc
- });
- }
+ await this.submitCommand();
}
- handleSubmit(e) {
+ handleSubmit = async (e) => {
e.preventDefault();
if (this.state.saving) {
@@ -224,7 +216,8 @@ export default class EditCommand extends React.Component {
return;
}
- if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) {
+ if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH ||
+ command.trigger.length > Constants.MAX_TRIGGER_LENGTH) {
this.setState({
saving: false,
clientError: (
@@ -256,73 +249,75 @@ export default class EditCommand extends React.Component {
return;
}
- this.newCmd = command;
+ this.newCommand = command;
- if (this.originalCommand.url !== this.newCmd.url || this.originalCommand.trigger !== this.newCmd.trigger || this.originalCommand.method !== this.newCmd.method) {
+ if (this.originalCommand.url !== this.newCommand.url ||
+ this.originalCommand.trigger !== this.newCommand.trigger ||
+ this.originalCommand.method !== this.newCommand.method) {
this.handleConfirmModal();
this.setState({
saving: false
});
} else {
- this.submitCommand();
+ await this.submitCommand();
}
}
- updateDisplayName(e) {
+ updateDisplayName = (e) => {
this.setState({
displayName: e.target.value
});
}
- updateDescription(e) {
+ updateDescription = (e) => {
this.setState({
description: e.target.value
});
}
- updateTrigger(e) {
+ updateTrigger = (e) => {
this.setState({
trigger: e.target.value
});
}
- updateUrl(e) {
+ updateUrl = (e) => {
this.setState({
url: e.target.value
});
}
- updateMethod(e) {
+ updateMethod = (e) => {
this.setState({
method: e.target.value
});
}
- updateUsername(e) {
+ updateUsername = (e) => {
this.setState({
username: e.target.value
});
}
- updateIconUrl(e) {
+ updateIconUrl = (e) => {
this.setState({
iconUrl: e.target.value
});
}
- updateAutocomplete(e) {
+ updateAutocomplete = (e) => {
this.setState({
autocomplete: e.target.checked
});
}
- updateAutocompleteHint(e) {
+ updateAutocompleteHint = (e) => {
this.setState({
autocompleteHint: e.target.value
});
}
- updateAutocompleteDescription(e) {
+ updateAutocompleteDescription = (e) => {
this.setState({
autocompleteDescription: e.target.value
});
diff --git a/webapp/components/integrations/components/edit_command/index.js b/webapp/components/integrations/components/edit_command/index.js
new file mode 100644
index 000000000..2a8257113
--- /dev/null
+++ b/webapp/components/integrations/components/edit_command/index.js
@@ -0,0 +1,31 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
+import {getCustomTeamCommands, editCommand} from 'mattermost-redux/actions/integrations';
+import {getCommands} from 'mattermost-redux/selectors/entities/integrations';
+
+import EditCommand from './edit_command.jsx';
+
+function mapStateToProps(state, ownProps) {
+ const commandId = ownProps.location.query.id;
+
+ return {
+ ...ownProps,
+ commandId,
+ commands: getCommands(state),
+ editCommandRequest: state.requests.integrations.editCommand
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ getCustomTeamCommands,
+ editCommand
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EditCommand);
diff --git a/webapp/routes/route_integrations.jsx b/webapp/routes/route_integrations.jsx
index 169d374c6..b7e08fda4 100644
--- a/webapp/routes/route_integrations.jsx
+++ b/webapp/routes/route_integrations.jsx
@@ -80,7 +80,7 @@ export default {
{
path: 'edit',
getComponents: (location, callback) => {
- System.import('components/integrations/components/edit_command.jsx').then(RouteUtils.importComponentSuccess(callback));
+ System.import('components/integrations/components/edit_command').then(RouteUtils.importComponentSuccess(callback));
}
},
{
diff --git a/webapp/tests/components/integrations/__snapshots__/edit_command.test.jsx.snap b/webapp/tests/components/integrations/__snapshots__/edit_command.test.jsx.snap
new file mode 100644
index 000000000..dd4fcffef
--- /dev/null
+++ b/webapp/tests/components/integrations/__snapshots__/edit_command.test.jsx.snap
@@ -0,0 +1,424 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components/integrations/EditCommand should match snapshot 1`] = `
+<div
+ className="backstage-content row"
+>
+ <BackstageHeader>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/test/integrations/commands"
+ >
+ <FormattedMessage
+ defaultMessage="Slash Commands"
+ id="installed_command.header"
+ values={Object {}}
+ />
+ </Link>
+ <FormattedMessage
+ defaultMessage="Edit"
+ id="integrations.edit"
+ values={Object {}}
+ />
+ </BackstageHeader>
+ <div
+ className="backstage-form"
+ >
+ <form
+ className="form-horizontal"
+ onSubmit={[Function]}
+ >
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="displayName"
+ >
+ <FormattedMessage
+ defaultMessage="Display Name"
+ id="add_command.displayName"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <input
+ className="form-control"
+ id="displayName"
+ maxLength="64"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="Display name for your slash command made of up to 64 characters."
+ id="add_command.displayName.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="description"
+ >
+ <FormattedMessage
+ defaultMessage="Description"
+ id="add_command.description"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <input
+ className="form-control"
+ id="description"
+ maxLength="128"
+ onChange={[Function]}
+ type="text"
+ value=""
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="Description for your incoming webhook."
+ id="add_command.description.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="trigger"
+ >
+ <FormattedMessage
+ defaultMessage="Command Trigger Word"
+ id="add_command.trigger"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <input
+ className="form-control"
+ id="trigger"
+ maxLength={128}
+ onChange={[Function]}
+ placeholder="Command trigger e.g. \\"hello\\" not including the slash"
+ type="text"
+ value=""
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="Trigger word must be unique, and cannot begin with a slash or contain any spaces."
+ id="add_command.trigger.help"
+ values={Object {}}
+ />
+ </div>
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="Examples: client, employee, patient, weather"
+ id="add_command.trigger.helpExamples"
+ values={Object {}}
+ />
+ </div>
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="Reserved: {link}"
+ id="add_command.trigger.helpReserved"
+ values={
+ Object {
+ "link": <a
+ href="https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <FormattedMessage
+ defaultMessage="see list of built-in slash commands"
+ id="add_command.trigger.helpReservedLinkText"
+ values={Object {}}
+ />
+ </a>,
+ }
+ }
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="url"
+ >
+ <FormattedMessage
+ defaultMessage="Request URL"
+ id="add_command.url"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <input
+ className="form-control"
+ id="url"
+ maxLength="1024"
+ onChange={[Function]}
+ placeholder="Must start with http:// or https://"
+ type="text"
+ value=""
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="The callback URL to receive the HTTP POST or GET event request when the slash command is run."
+ id="add_command.url.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="method"
+ >
+ <FormattedMessage
+ defaultMessage="Request Method"
+ id="add_command.method"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <select
+ className="form-control"
+ id="method"
+ onChange={[Function]}
+ value="P"
+ >
+ <option
+ value="P"
+ >
+ POST
+ </option>
+ <option
+ value="G"
+ >
+ GET
+ </option>
+ </select>
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="The type of command request issued to the Request URL."
+ id="add_command.method.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="username"
+ >
+ <FormattedMessage
+ defaultMessage="Response Username"
+ id="add_command.username"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <input
+ className="form-control"
+ id="username"
+ maxLength="64"
+ onChange={[Function]}
+ placeholder="Username"
+ type="text"
+ value=""
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="(Optional) Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and they symbols \\"-\\", \\"_\\", and \\".\\" ."
+ id="add_command.username.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="iconUrl"
+ >
+ <FormattedMessage
+ defaultMessage="Response Icon"
+ id="add_command.iconUrl"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8"
+ >
+ <input
+ className="form-control"
+ id="iconUrl"
+ maxLength="1024"
+ onChange={[Function]}
+ placeholder="https://www.example.com/myicon.png"
+ type="text"
+ value=""
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="(Optional) Choose a profile picture override for the post responses to this slash command. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels."
+ id="add_command.iconUrl.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <label
+ className="control-label col-sm-4"
+ htmlFor="autocomplete"
+ >
+ <FormattedMessage
+ defaultMessage="Autocomplete"
+ id="add_command.autocomplete"
+ values={Object {}}
+ />
+ </label>
+ <div
+ className="col-md-5 col-sm-8 checkbox"
+ >
+ <input
+ checked={false}
+ id="autocomplete"
+ onChange={[Function]}
+ type="checkbox"
+ />
+ <div
+ className="form__help"
+ >
+ <FormattedMessage
+ defaultMessage="(Optional) Show slash command in autocomplete list."
+ id="add_command.autocomplete.help"
+ values={Object {}}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="backstage-form__footer"
+ >
+ <FormError
+ error={null}
+ errors={
+ Array [
+ "",
+ null,
+ ]
+ }
+ type="backstage"
+ />
+ <Link
+ className="btn btn-sm"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to="/test/integrations/commands"
+ >
+ <FormattedMessage
+ defaultMessage="Cancel"
+ id="add_command.cancel"
+ values={Object {}}
+ />
+ </Link>
+ <SpinnerButton
+ className="btn btn-primary"
+ disabled={true}
+ onClick={[Function]}
+ spinning={false}
+ type="submit"
+ >
+ <FormattedMessage
+ defaultMessage="Update"
+ id="edit_command.save"
+ values={Object {}}
+ />
+ </SpinnerButton>
+ <ConfirmModal
+ confirmButtonClass="btn btn-primary"
+ confirmButtonText={
+ <FormattedMessage
+ defaultMessage="Update"
+ id="update_command.update"
+ values={Object {}}
+ />
+ }
+ message={
+ <FormattedMessage
+ defaultMessage="Your changes may break the existing slash command. Are you sure you would like to update it?"
+ id="update_command.question"
+ values={Object {}}
+ />
+ }
+ onCancel={[Function]}
+ onConfirm={[Function]}
+ show={false}
+ title={
+ <FormattedMessage
+ defaultMessage="Edit Slash Command"
+ id="update_command.confirm"
+ values={Object {}}
+ />
+ }
+ />
+ </div>
+ </form>
+ </div>
+</div>
+`;
diff --git a/webapp/tests/components/integrations/edit_command.test.jsx b/webapp/tests/components/integrations/edit_command.test.jsx
new file mode 100644
index 000000000..6b919cb86
--- /dev/null
+++ b/webapp/tests/components/integrations/edit_command.test.jsx
@@ -0,0 +1,36 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import EditCommand from 'components/integrations/components/edit_command/edit_command.jsx';
+
+describe('components/integrations/EditCommand', () => {
+ test('should match snapshot', () => {
+ const emptyFunction = jest.fn();
+ const id = 'r5tpgt4iepf45jt768jz84djic';
+ global.window.mm_config = {};
+ global.window.mm_config.EnableCommands = 'true';
+
+ const wrapper = shallow(
+ <EditCommand
+ team={{
+ id,
+ name: 'test'
+ }}
+ commandId={id}
+ commands={[]}
+ editCommandRequest={{
+ status: 'not_started',
+ error: null
+ }}
+ actions={{
+ getCustomTeamCommands: emptyFunction,
+ editCommand: emptyFunction
+ }}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+}); \ No newline at end of file