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
Этот коммит содержится в:
94117nl
2017-07-31 07:26:04 -05:00
коммит произвёл Joram Wilander
родитель f740698dbe
Коммит 6ec24867bc
5 изменённых файлов: 586 добавлений и 100 удалений

Просмотреть файл

@@ -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();
await 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
});
}
}
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
});

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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));
}
},
{

Просмотреть файл

@@ -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>
`;

Просмотреть файл

@@ -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();
});
});