diff options
author | 94117nl <rttededersixtwo@gmail.com> | 2017-07-31 07:26:04 -0500 |
---|---|---|
committer | Joram Wilander <jwawilander@gmail.com> | 2017-07-31 08:26:04 -0400 |
commit | 6ec24867bc75057fa58c11e80a6b28334473983b (patch) | |
tree | 637c40df9dd49ec4beec99df9b78e90a867846d2 /webapp | |
parent | f740698dbe06816921d2a20eea876c9ca7b515ed (diff) | |
download | chat-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.js | 31 | ||||
-rw-r--r-- | webapp/routes/route_integrations.jsx | 2 | ||||
-rw-r--r-- | webapp/tests/components/integrations/__snapshots__/edit_command.test.jsx.snap | 424 | ||||
-rw-r--r-- | webapp/tests/components/integrations/edit_command.test.jsx | 36 |
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 |