summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/command.go60
-rw-r--r--api/command_test.go39
-rw-r--r--i18n/en.json8
-rw-r--r--model/client.go10
-rw-r--r--webapp/client/client.jsx12
-rw-r--r--webapp/components/integrations/components/edit_command.jsx731
-rw-r--r--webapp/components/integrations/components/installed_command.jsx9
-rw-r--r--webapp/i18n/en.json7
-rw-r--r--webapp/routes/route_integrations.jsx6
-rw-r--r--webapp/stores/integration_store.jsx9
-rw-r--r--webapp/tests/client_command.test.jsx28
-rw-r--r--webapp/utils/async_client.jsx23
12 files changed, 942 insertions, 0 deletions
diff --git a/api/command.go b/api/command.go
index e71661a67..ff0f72149 100644
--- a/api/command.go
+++ b/api/command.go
@@ -45,6 +45,7 @@ func InitCommand() {
BaseRoutes.Commands.Handle("/list", ApiUserRequired(listCommands)).Methods("GET")
BaseRoutes.Commands.Handle("/create", ApiUserRequired(createCommand)).Methods("POST")
+ BaseRoutes.Commands.Handle("/update", ApiUserRequired(updateCommand)).Methods("POST")
BaseRoutes.Commands.Handle("/list_team_commands", ApiUserRequired(listTeamCommands)).Methods("GET")
BaseRoutes.Commands.Handle("/regen_token", ApiUserRequired(regenCommandToken)).Methods("POST")
BaseRoutes.Commands.Handle("/delete", ApiUserRequired(deleteCommand)).Methods("POST")
@@ -319,6 +320,65 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !*utils.Cfg.ServiceSettings.EnableCommands {
+ c.Err = model.NewLocAppError("updateCommand", "api.command.disabled.app_error", nil, "")
+ c.Err.StatusCode = http.StatusNotImplemented
+ return
+ }
+
+ if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
+ c.Err = model.NewLocAppError("updateCommand", "api.command.admin_only.app_error", nil, "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ c.LogAudit("attempt")
+
+ cmd := model.CommandFromJson(r.Body)
+
+ if cmd == nil {
+ c.SetInvalidParam("updateCommand", "command")
+ return
+ }
+
+ cmd.Trigger = strings.ToLower(cmd.Trigger)
+
+ var oldCmd *model.Command
+ if result := <-Srv.Store.Command().Get(cmd.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ oldCmd = result.Data.(*model.Command)
+
+ if c.Session.UserId != oldCmd.CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) {
+ c.LogAudit("fail - inappropriate permissions")
+ c.Err = model.NewLocAppError("updateCommand", "api.command.update.app_error", nil, "user_id="+c.Session.UserId)
+ return
+ }
+
+ if c.TeamId != oldCmd.TeamId {
+ c.Err = model.NewLocAppError("updateCommand", "api.command.team_mismatch.app_error", nil, "user_id="+c.Session.UserId)
+ return
+ }
+
+ cmd.Id = oldCmd.Id
+ cmd.Token = oldCmd.Token
+ cmd.CreateAt = oldCmd.CreateAt
+ cmd.UpdateAt = model.GetMillis()
+ cmd.DeleteAt = oldCmd.DeleteAt
+ cmd.CreatorId = oldCmd.CreatorId
+ cmd.TeamId = oldCmd.TeamId
+ }
+
+ if result := <-Srv.Store.Command().Update(cmd); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(result.Data.(*model.Command).ToJson()))
+ }
+}
+
func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) {
if !*utils.Cfg.ServiceSettings.EnableCommands {
c.Err = model.NewLocAppError("listTeamCommands", "api.command.disabled.app_error", nil, "")
diff --git a/api/command_test.go b/api/command_test.go
index 7a78d350d..45268a9a5 100644
--- a/api/command_test.go
+++ b/api/command_test.go
@@ -120,6 +120,45 @@ func TestListTeamCommands(t *testing.T) {
}
}
+func TestUpdateCommand(t *testing.T) {
+ th := Setup().InitSystemAdmin()
+ Client := th.SystemAdminClient
+ user := th.SystemAdminUser
+ team := th.SystemAdminTeam
+
+ enableCommands := *utils.Cfg.ServiceSettings.EnableCommands
+ defer func() {
+ utils.Cfg.ServiceSettings.EnableCommands = &enableCommands
+ }()
+ *utils.Cfg.ServiceSettings.EnableCommands = true
+
+ cmd1 := &model.Command{
+ CreatorId: user.Id,
+ TeamId: team.Id,
+ URL: "http://nowhere.com",
+ Method: model.COMMAND_METHOD_POST,
+ Trigger: "trigger"}
+
+ cmd1 = Client.Must(Client.CreateCommand(cmd1)).Data.(*model.Command)
+
+ cmd2 := &model.Command{
+ CreatorId: user.Id,
+ TeamId: team.Id,
+ URL: "http://nowhere.com",
+ Method: model.COMMAND_METHOD_POST,
+ Trigger: "trigger2",
+ Token: cmd1.Token,
+ Id: cmd1.Id}
+
+ if result, err := Client.UpdateCommand(cmd2); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.Command).Trigger == cmd1.Trigger {
+ t.Fatal("update didn't work properly")
+ }
+ }
+}
+
func TestRegenToken(t *testing.T) {
th := Setup().InitSystemAdmin()
Client := th.SystemAdminClient
diff --git a/i18n/en.json b/i18n/en.json
index 4d5ccd525..a5a1e5928 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -440,6 +440,14 @@
"translation": "Inappropriate permissions to regenerate command token"
},
{
+ "id": "api.command.team_mismatch.app_error",
+ "translation": "Cannot update commands across teams"
+ },
+ {
+ "id": "api.command.update.app_error",
+ "translation": "Inappropriate permissions to update command"
+ },
+ {
"id": "api.command_away.desc",
"translation": "Set your status away"
},
diff --git a/model/client.go b/model/client.go
index 8a361c177..1624dc917 100644
--- a/model/client.go
+++ b/model/client.go
@@ -846,6 +846,16 @@ func (c *Client) CreateCommand(cmd *Command) (*Result, *AppError) {
}
}
+func (c *Client) UpdateCommand(cmd *Command) (*Result, *AppError) {
+ if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/update", cmd.ToJson()); err != nil {
+ return nil, err
+ } else {
+ defer closeBody(r)
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), CommandFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) RegenCommandToken(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetTeamRoute()+"/commands/regen_token", MapToJson(data)); err != nil {
return nil, err
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index 3ce6977f6..90ff75059 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -1457,6 +1457,18 @@ export default class Client {
this.track('api', 'api_integrations_created');
}
+ editCommand(command, success, error) {
+ request.
+ post(`${this.getCommandsRoute()}/update`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ send(command).
+ end(this.handleResponse.bind(this, 'editCommand', success, error));
+
+ this.track('api', 'api_integrations_created');
+ }
+
deleteCommand(commandId, success, error) {
request.
post(`${this.getCommandsRoute()}/delete`).
diff --git a/webapp/components/integrations/components/edit_command.jsx b/webapp/components/integrations/components/edit_command.jsx
new file mode 100644
index 000000000..395c977ca
--- /dev/null
+++ b/webapp/components/integrations/components/edit_command.jsx
@@ -0,0 +1,731 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import IntegrationStore from 'stores/integration_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import {loadTeamCommands} 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';
+
+const REQUEST_POST = 'P';
+const REQUEST_GET = 'G';
+
+export default class EditCommand extends React.Component {
+ static get propTypes() {
+ return {
+ team: React.propTypes.object.isRequired,
+ location: React.PropTypes.object
+ };
+ }
+
+ 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: '',
+ trigger: '',
+ url: '',
+ method: REQUEST_POST,
+ username: '',
+ iconUrl: '',
+ autocomplete: false,
+ autocompleteHint: '',
+ autocompleteDescription: '',
+ saving: false,
+ serverError: '',
+ clientError: null,
+ showConfirmModal: false,
+ commands: IntegrationStore.getCommands(teamId),
+ loading: !IntegrationStore.hasReceivedCommands(teamId)
+ };
+ }
+
+ componentDidMount() {
+ IntegrationStore.addChangeListener(this.handleIntegrationChange);
+
+ if (window.mm_config.EnableCommands === 'true') {
+ loadTeamCommands();
+ }
+ }
+
+ componentWillUnmount() {
+ IntegrationStore.removeChangeListener(this.handleIntegrationChange);
+ }
+
+ handleConfirmModal() {
+ this.setState({showConfirmModal: true});
+ }
+
+ confirmModalDismissed() {
+ this.setState({showConfirmModal: false});
+ }
+
+ submitCommand() {
+ AsyncClient.editCommand(
+ this.newCmd,
+ browserHistory.push('/' + this.props.team.name + '/integrations/commands'),
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ handleUpdate() {
+ 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
+ });
+ }
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ let triggerWord = this.state.trigger.trim().toLowerCase();
+ if (triggerWord.indexOf('/') === 0) {
+ triggerWord = triggerWord.substr(1);
+ }
+
+ const command = {
+ display_name: this.state.displayName,
+ description: this.state.description,
+ trigger: triggerWord,
+ url: this.state.url.trim(),
+ method: this.state.method,
+ username: this.state.username,
+ icon_url: this.state.iconUrl,
+ auto_complete: this.state.autocomplete
+ };
+
+ if (this.originalCommand.id) {
+ command.id = this.originalCommand.id;
+ }
+
+ if (command.auto_complete) {
+ command.auto_complete_desc = this.state.autocompleteDescription;
+ command.auto_complete_hint = this.state.autocompleteHint;
+ }
+
+ if (!command.trigger) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerRequired'
+ defaultMessage='A trigger word is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (command.trigger.indexOf('/') === 0) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSlash'
+ defaultMessage='A trigger word cannot begin with a /'
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (command.trigger.indexOf(' ') !== -1) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidSpace'
+ defaultMessage='A trigger word must not contain spaces'
+ />
+ )
+ });
+ return;
+ }
+
+ if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.triggerInvalidLength'
+ defaultMessage='A trigger word must contain between {min} and {max} characters'
+ values={{
+ min: Constants.MIN_TRIGGER_LENGTH,
+ max: Constants.MAX_TRIGGER_LENGTH
+ }}
+ />
+ )
+ });
+
+ return;
+ }
+
+ if (!command.url) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.urlRequired'
+ defaultMessage='A request URL is required'
+ />
+ )
+ });
+
+ return;
+ }
+
+ this.newCmd = command;
+
+ if (this.originalCommand.url !== this.newCmd.url || this.originalCommand.trigger !== this.newCmd.trigger || this.originalCommand.method !== this.newCmd.method) {
+ this.handleConfirmModal();
+ this.setState({
+ saving: false
+ });
+ } else {
+ this.submitCommand();
+ }
+ }
+
+ updateDisplayName(e) {
+ this.setState({
+ displayName: e.target.value
+ });
+ }
+
+ updateDescription(e) {
+ this.setState({
+ description: e.target.value
+ });
+ }
+
+ updateTrigger(e) {
+ this.setState({
+ trigger: e.target.value
+ });
+ }
+
+ updateUrl(e) {
+ this.setState({
+ url: e.target.value
+ });
+ }
+
+ updateMethod(e) {
+ this.setState({
+ method: e.target.value
+ });
+ }
+
+ updateUsername(e) {
+ this.setState({
+ username: e.target.value
+ });
+ }
+
+ updateIconUrl(e) {
+ this.setState({
+ iconUrl: e.target.value
+ });
+ }
+
+ updateAutocomplete(e) {
+ this.setState({
+ autocomplete: e.target.checked
+ });
+ }
+
+ updateAutocompleteHint(e) {
+ this.setState({
+ autocompleteHint: e.target.value
+ });
+ }
+
+ updateAutocompleteDescription(e) {
+ this.setState({
+ autocompleteDescription: e.target.value
+ });
+ }
+
+ render() {
+ const confirmButton = (
+ <FormattedMessage
+ id='update_command.update'
+ defaultMessage='Update'
+ />
+ );
+
+ const confirmTitle = (
+ <FormattedMessage
+ id='update_command.confirm'
+ defaultMessage='Edit Slash Command'
+ />
+ );
+
+ const confirmMessage = (
+ <FormattedMessage
+ id='update_command.question'
+ defaultMessage='Your changes may break the existing slash command. Are you sure you would like to update it?'
+ />
+ );
+
+ let autocompleteFields = null;
+ if (this.state.autocomplete) {
+ autocompleteFields = [(
+ <div
+ key='autocompleteHint'
+ className='form-group'
+ >
+ <label
+ className='control-label col-sm-4'
+ htmlFor='autocompleteHint'
+ >
+ <FormattedMessage
+ id='add_command.autocompleteHint'
+ defaultMessage='Autocomplete Hint'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='autocompleteHint'
+ type='text'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.autocompleteHint}
+ onChange={this.updateAutocompleteHint}
+ placeholder={Utils.localizeMessage('add_command.autocompleteHint.placeholder', 'Example: [Patient Name]')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.autocompleteHint.help'
+ defaultMessage='(Optional) Arguments associated with your slash command, displayed as help in the autocomplete list.'
+ />
+ </div>
+ </div>
+ </div>
+ ),
+ (
+ <div
+ key='autocompleteDescription'
+ className='form-group'
+ >
+ <label
+ className='control-label col-sm-4'
+ htmlFor='autocompleteDescription'
+ >
+ <FormattedMessage
+ id='add_command.autocompleteDescription'
+ defaultMessage='Autocomplete Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='description'
+ type='text'
+ maxLength='128'
+ className='form-control'
+ value={this.state.autocompleteDescription}
+ onChange={this.updateAutocompleteDescription}
+ placeholder={Utils.localizeMessage('add_command.autocompleteDescription.placeholder', 'Example: "Returns search results for patient records"')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.autocompleteDescription.help'
+ defaultMessage='(Optional) Short description of slash command for the autocomplete list.'
+ />
+ </div>
+ </div>
+ </div>
+ )];
+ }
+
+ return (
+ <div className='backstage-content row'>
+ <BackstageHeader>
+ <Link to={'/' + this.props.team.name + '/integrations/commands'}>
+ <FormattedMessage
+ id='installed_command.header'
+ defaultMessage='Slash Commands'
+ />
+ </Link>
+ <FormattedMessage
+ id='integrations.edit'
+ defaultMessage='Edit'
+ />
+ </BackstageHeader>
+ <div className='backstage-form'>
+ <form
+ className='form-horizontal'
+ onSubmit={this.handleSubmit}
+ >
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='displayName'
+ >
+ <FormattedMessage
+ id='add_command.displayName'
+ defaultMessage='Display Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='displayName'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.displayName}
+ onChange={this.updateDisplayName}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.displayName.help'
+ defaultMessage='Display name for your slash command made of up to 64 characters.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='add_command.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='description'
+ type='text'
+ maxLength='128'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.description.help'
+ defaultMessage='Description for your incoming webhook.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='trigger'
+ >
+ <FormattedMessage
+ id='add_command.trigger'
+ defaultMessage='Command Trigger Word'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='trigger'
+ type='text'
+ maxLength={Constants.MAX_TRIGGER_LENGTH}
+ className='form-control'
+ value={this.state.trigger}
+ onChange={this.updateTrigger}
+ placeholder={Utils.localizeMessage('add_command.trigger.placeholder', 'Command trigger e.g. "hello" not including the slash')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.trigger.help'
+ defaultMessage='Trigger word must be unique, and cannot begin with a slash or contain any spaces.'
+ />
+ </div>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.trigger.helpExamples'
+ defaultMessage='Examples: client, employee, patient, weather'
+ />
+ </div>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.trigger.helpReserved'
+ defaultMessage='Reserved: {link}'
+ values={{
+ link: (
+ <a
+ href='https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands'
+ target='_blank'
+ rel='noopener noreferrer'
+ >
+ <FormattedMessage
+ id='add_command.trigger.helpReservedLinkText'
+ defaultMessage='see list of built-in slash commands'
+ />
+ </a>
+ )
+ }}
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='url'
+ >
+ <FormattedMessage
+ id='add_command.url'
+ defaultMessage='Request URL'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='url'
+ type='text'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.url}
+ onChange={this.updateUrl}
+ placeholder={Utils.localizeMessage('add_command.url.placeholder', 'Must start with http:// or https://')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.url.help'
+ defaultMessage='The callback URL to receive the HTTP POST or GET event request when the slash command is run.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='method'
+ >
+ <FormattedMessage
+ id='add_command.method'
+ defaultMessage='Request Method'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <select
+ id='method'
+ className='form-control'
+ value={this.state.method}
+ onChange={this.updateMethod}
+ >
+ <option value={REQUEST_POST}>
+ {Utils.localizeMessage('add_command.method.post', 'POST')}
+ </option>
+ <option value={REQUEST_GET}>
+ {Utils.localizeMessage('add_command.method.get', 'GET')}
+ </option>
+ </select>
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.method.help'
+ defaultMessage='The type of command request issued to the Request URL.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='username'
+ >
+ <FormattedMessage
+ id='add_command.username'
+ defaultMessage='Response Username'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='username'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.username}
+ onChange={this.updateUsername}
+ placholder={Utils.localizeMessage('add_command.username.placeholder', 'Username')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.username.help'
+ 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 "." .'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='iconUrl'
+ >
+ <FormattedMessage
+ id='add_command.iconUrl'
+ defaultMessage='Response Icon'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8'>
+ <input
+ id='iconUrl'
+ type='text'
+ maxLength='1024'
+ className='form-control'
+ value={this.state.iconUrl}
+ onChange={this.updateIconUrl}
+ placeholder={Utils.localizeMessage('add_command.iconUrl.placeholder', 'https://www.example.com/myicon.png')}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.iconUrl.help'
+ 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.'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='autocomplete'
+ >
+ <FormattedMessage
+ id='add_command.autocomplete'
+ defaultMessage='Autocomplete'
+ />
+ </label>
+ <div className='col-md-5 col-sm-8 checkbox'>
+ <input
+ id='autocomplete'
+ type='checkbox'
+ checked={this.state.autocomplete}
+ onChange={this.updateAutocomplete}
+ />
+ <div className='form__help'>
+ <FormattedMessage
+ id='add_command.autocomplete.help'
+ defaultMessage='(Optional) Show slash command in autocomplete list.'
+ />
+ </div>
+ </div>
+ </div>
+ {autocompleteFields}
+ <div className='backstage-form__footer'>
+ <FormError
+ type='backstage'
+ errors={[this.state.serverError, this.state.clientError]}
+ />
+ <Link
+ className='btn btn-sm'
+ to={'/' + this.props.team.name + '/integrations/commands'}
+ >
+ <FormattedMessage
+ id='add_command.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ disabled={this.state.loading}
+ >
+ <FormattedMessage
+ id='edit_command.save'
+ defaultMessage='Update'
+ />
+ </SpinnerButton>
+ <ConfirmModal
+ title={confirmTitle}
+ message={confirmMessage}
+ confirmButton={confirmButton}
+ show={this.state.showConfirmModal}
+ onConfirm={this.handleUpdate}
+ onCancel={this.confirmModalDismissed}
+ />
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/integrations/components/installed_command.jsx b/webapp/components/integrations/components/installed_command.jsx
index f149a21ac..ecd7d9608 100644
--- a/webapp/components/integrations/components/installed_command.jsx
+++ b/webapp/components/integrations/components/installed_command.jsx
@@ -130,6 +130,15 @@ export default class InstalledCommand extends React.Component {
</a>
{' - '}
<a
+ href={'edit?id=' + command.id}
+ >
+ <FormattedMessage
+ id='installed_integrations.edit'
+ defaultMessage='Edit'
+ />
+ </a>
+ {' - '}
+ <a
href='#'
onClick={this.handleDelete}
>
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 532b44c23..f9d91e8e0 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1157,6 +1157,7 @@
"edit_channel_purpose_modal.save": "Save",
"edit_channel_purpose_modal.title1": "Edit Purpose",
"edit_channel_purpose_modal.title2": "Edit Purpose for ",
+ "edit_command.save": "Update",
"edit_post.cancel": "Cancel",
"edit_post.edit": "Edit {title}",
"edit_post.editPost": "Edit the post...",
@@ -1362,6 +1363,7 @@
"installed_integrations.content_type": "Content-Type: {contentType}",
"installed_integrations.creation": "Created by {creator} on {createAt, date, full}",
"installed_integrations.delete": "Delete",
+ "installed_integrations.edit": "Edit",
"installed_integrations.hideSecret": "Hide Secret",
"installed_integrations.regenSecret": "Regenerate Secret",
"installed_integrations.regenToken": "Regenerate Token",
@@ -1396,6 +1398,7 @@
"installed_outgoing_webhooks.search": "Search Outgoing Webhooks",
"installed_outgoing_webhooks.unknown_channel": "A Private Webhook",
"integrations.add": "Add",
+ "integrations.edit": "Edit",
"integrations.command.description": "Slash commands send events to external integrations",
"integrations.command.title": "Slash Command",
"integrations.done": "Done",
@@ -1788,6 +1791,10 @@
"tutorial_tip.ok": "Okay",
"tutorial_tip.out": "Opt out of these tips.",
"tutorial_tip.seen": "Seen this before? ",
+ "update_command.cancel": "Cancel",
+ "update_command.confirm": "Edit Slash Command",
+ "update_command.update": "Update",
+ "update_command.question": "Your changes may break the existing slash command. Are you sure you would like to update it?",
"upload_overlay.info": "Drop a file to upload it.",
"user.settings.advance.embed_preview": "Show experimental previews of link content, when available",
"user.settings.advance.embed_toggle": "Show toggle for all embed previews",
diff --git a/webapp/routes/route_integrations.jsx b/webapp/routes/route_integrations.jsx
index 0feb13bb7..7a4af7e7a 100644
--- a/webapp/routes/route_integrations.jsx
+++ b/webapp/routes/route_integrations.jsx
@@ -66,6 +66,12 @@ export default {
}
},
{
+ path: 'edit',
+ getComponents: (location, callback) => {
+ System.import('components/integrations/components/edit_command.jsx').then(RouteUtils.importComponentSuccess(callback));
+ }
+ },
+ {
path: 'confirm',
getComponents: (location, callback) => {
System.import('components/integrations/components/confirm_integration.jsx').then(RouteUtils.importComponentSuccess(callback));
diff --git a/webapp/stores/integration_store.jsx b/webapp/stores/integration_store.jsx
index 33680452b..ae818b443 100644
--- a/webapp/stores/integration_store.jsx
+++ b/webapp/stores/integration_store.jsx
@@ -137,6 +137,15 @@ class IntegrationStore extends EventEmitter {
this.setCommands(teamId, commands);
}
+ editCommand(command) {
+ const teamId = command.team_id;
+ const commands = this.getCommands(teamId);
+
+ commands.push(command);
+
+ this.setCommands(teamId, commands);
+ }
+
updateCommand(command) {
const teamId = command.team_id;
const commands = this.getCommands(teamId);
diff --git a/webapp/tests/client_command.test.jsx b/webapp/tests/client_command.test.jsx
index 769fa2fa0..7d39537f8 100644
--- a/webapp/tests/client_command.test.jsx
+++ b/webapp/tests/client_command.test.jsx
@@ -81,6 +81,34 @@ describe('Client.Commands', function() {
});
});
+ it('editCommand', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
+
+ var cmd = {};
+ cmd.url = 'http://www.gonowhere.com';
+ cmd.trigger = '/hello';
+ cmd.method = 'P';
+ cmd.username = '';
+ cmd.icon_url = '';
+ cmd.auto_complete = false;
+ cmd.auto_complete_desc = '';
+ cmd.auto_complete_hint = '';
+ cmd.display_name = 'Unit Test';
+
+ TestHelper.basicClient().editCommand(
+ cmd,
+ function() {
+ done(new Error('cmds not enabled'));
+ },
+ function(err) {
+ assert.equal(err.id, 'api.command.disabled.app_error');
+ done();
+ }
+ );
+ });
+ });
+
it('deleteCommand', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().enableLogErrorsToConsole(false); // Disabling since this unit test causes an error
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index efa9eeb2b..fe31d4ef8 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -1348,6 +1348,29 @@ export function addCommand(command, success, error) {
);
}
+export function editCommand(command, success, error) {
+ Client.editCommand(
+ command,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_COMMAND,
+ command: data
+ });
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ } else {
+ dispatchError(err, 'editCommand');
+ }
+ }
+ );
+}
+
export function deleteCommand(id) {
Client.deleteCommand(
id,