summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/create_post.jsx7
-rw-r--r--web/react/components/user_settings/manage_command_hooks.jsx442
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx35
-rw-r--r--web/react/utils/async_client.jsx20
-rw-r--r--web/react/utils/client.jsx76
-rw-r--r--web/web_test.go4
6 files changed, 570 insertions, 14 deletions
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index de971c43f..76f63d4b3 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -133,15 +133,10 @@ export default class CreatePost extends React.Component {
post.message,
false,
(data) => {
- if (data.response === 'not implemented') {
- this.sendMessage(post);
- return;
- }
-
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
- if (data.goto_location.length > 0) {
+ if (data.goto_location && data.goto_location.length > 0) {
window.location.href = data.goto_location;
}
},
diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx
new file mode 100644
index 000000000..1fb3bf599
--- /dev/null
+++ b/web/react/components/user_settings/manage_command_hooks.jsx
@@ -0,0 +1,442 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from '../loading_screen.jsx';
+
+import * as Client from '../../utils/client.jsx';
+
+export default class ManageCommandCmds extends React.Component {
+ constructor() {
+ super();
+
+ this.getCmds = this.getCmds.bind(this);
+ this.addNewCmd = this.addNewCmd.bind(this);
+ this.emptyCmd = this.emptyCmd.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.updateDisplayName = this.updateDisplayName.bind(this);
+ this.updateAutoComplete = this.updateAutoComplete.bind(this);
+ this.updateAutoCompleteDesc = this.updateAutoCompleteDesc.bind(this);
+ this.updateAutoCompleteHint = this.updateAutoCompleteHint.bind(this);
+
+ this.state = {cmds: [], cmd: this.emptyCmd(), getCmdsComplete: false};
+ }
+
+ emptyCmd() {
+ var cmd = {};
+ cmd.url = '';
+ cmd.trigger = '';
+ cmd.method = 'P';
+ cmd.username = '';
+ cmd.icon_url = '';
+ cmd.auto_complete = false;
+ cmd.auto_complete_desc = '';
+ cmd.auto_complete_hint = '';
+ cmd.display_name = '';
+ return cmd;
+ }
+
+ componentDidMount() {
+ this.getCmds();
+ }
+
+ addNewCmd(e) {
+ e.preventDefault();
+
+ if (this.state.cmd.trigger === '' || this.state.cmd.url === '') {
+ return;
+ }
+
+ var cmd = this.state.cmd;
+ if (cmd.trigger.length !== 0) {
+ cmd.trigger = cmd.trigger.trim();
+ }
+ cmd.url = cmd.url.trim();
+
+ Client.addCommand(
+ cmd,
+ (data) => {
+ let cmds = Object.assign([], this.state.cmds);
+ if (!cmds) {
+ cmds = [];
+ }
+ cmds.push(data);
+ this.setState({cmds, addError: null, cmd: this.emptyCmd()});
+ },
+ (err) => {
+ this.setState({addError: err.message});
+ }
+ );
+ }
+
+ removeCmd(id) {
+ const data = {};
+ data.id = id;
+
+ Client.deleteCommand(
+ data,
+ () => {
+ const cmds = this.state.cmds;
+ let index = -1;
+ for (let i = 0; i < cmds.length; i++) {
+ if (cmds[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ cmds.splice(index, 1);
+ }
+
+ this.setState({cmds});
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ regenToken(id) {
+ const regenData = {};
+ regenData.id = id;
+
+ Client.regenCommandToken(
+ regenData,
+ (data) => {
+ const cmds = Object.assign([], this.state.cmds);
+ for (let i = 0; i < cmds.length; i++) {
+ if (cmds[i].id === id) {
+ cmds[i] = data;
+ break;
+ }
+ }
+
+ this.setState({cmds, editError: null});
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ getCmds() {
+ Client.listTeamCommands(
+ (data) => {
+ if (data) {
+ this.setState({cmds: data, getCmdsComplete: true, editError: null});
+ }
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ updateTrigger(e) {
+ var cmd = this.state.cmd;
+ cmd.trigger = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateURL(e) {
+ var cmd = this.state.cmd;
+ cmd.url = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateMethod(e) {
+ var cmd = this.state.cmd;
+ cmd.method = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateUsername(e) {
+ var cmd = this.state.cmd;
+ cmd.username = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateIconURL(e) {
+ var cmd = this.state.cmd;
+ cmd.icon_url = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateDisplayName(e) {
+ var cmd = this.state.cmd;
+ cmd.display_name = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateAutoComplete(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete = e.target.checked;
+ this.setState(cmd);
+ }
+
+ updateAutoCompleteDesc(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete_desc = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateAutoCompleteHint(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete_hint = e.target.value;
+ this.setState(cmd);
+ }
+
+ render() {
+ let addError;
+ if (this.state.addError) {
+ addError = <label className='has-error'>{this.state.addError}</label>;
+ }
+
+ let editError;
+ if (this.state.editError) {
+ addError = <label className='has-error'>{this.state.editError}</label>;
+ }
+
+ const cmds = [];
+ this.state.cmds.forEach((cmd) => {
+ let triggerDiv;
+ if (cmd.trigger && cmd.trigger.length !== 0) {
+ triggerDiv = (
+ <div className='padding-top'>
+ <strong>{'Trigger: '}</strong>{cmd.trigger}
+ </div>
+ );
+ }
+
+ cmds.push(
+ <div
+ key={cmd.id}
+ className='webcmd__item'
+ >
+ <div className='padding-top x2'>
+ <strong>{'Display Name: '}</strong><span className='word-break--all'>{cmd.display_name}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Username: '}</strong><span className='word-break--all'>{cmd.username}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Icon URL: '}</strong><span className='word-break--all'>{cmd.icon_url}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Auto Complete: '}</strong><span className='word-break--all'>{cmd.auto_complete ? 'yes' : 'no'}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Auto Complete Description: '}</strong><span className='word-break--all'>{cmd.auto_complete_desc}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Auto Complete Hint: '}</strong><span className='word-break--all'>{cmd.auto_complete_hint}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>{'Request Type: '}</strong><span className='word-break--all'>{cmd.method === 'P' ? 'POST' : 'GET'}</span>
+ </div>
+ <div className='padding-top x2 webcmd__url'>
+ <strong>{'URL: '}</strong><span className='word-break--all'>{cmd.url}</span>
+ </div>
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>{'Token: '}</strong>{cmd.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, cmd.id)}
+ >
+ {'Regen Token'}
+ </a>
+ <a
+ className='webcmd__remove'
+ href='#'
+ onClick={this.removeCmd.bind(this, cmd.id)}
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </a>
+ </div>
+ <div className='padding-top x2 divider-light'></div>
+ </div>
+ );
+ });
+
+ let displayCmds;
+ if (!this.state.getCmdsComplete) {
+ displayCmds = <LoadingScreen/>;
+ } else if (cmds.length > 0) {
+ displayCmds = cmds;
+ } else {
+ displayCmds = <div className='padding-top x2'>{'None'}</div>;
+ }
+
+ const existingCmds = (
+ <div className='webcmds__container'>
+ <label className='control-label padding-top x2'>{'Existing commands'}</label>
+ <div className='padding-top divider-light'></div>
+ <div className='webcmds__list'>
+ {displayCmds}
+ </div>
+ </div>
+ );
+
+ const disableButton = this.state.cmd.trigger === '' || this.state.cmd.url === '';
+
+ return (
+ <div key='addCommandCmd'>
+ {'Create commands to send new message events to an external integration. Please see '}
+ <a
+ href='http://mattermost.org/commands'
+ target='_blank'
+ >
+ {'http://mattermost.org/commands'}
+ </a>
+ {' to learn more.'}
+ <div><label className='control-label padding-top x2'>{'Add a new command'}</label></div>
+ <div className='padding-top divider-light'></div>
+ <div className='padding-top'>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Display Name:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='displayName'
+ className='form-control'
+ value={this.state.cmd.display_name}
+ onChange={this.updateDisplayName}
+ placeholder='Display Name'
+ />
+ </div>
+ <div className='padding-top'>{'Command display name.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Username:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='username'
+ className='form-control'
+ value={this.state.cmd.username}
+ onChange={this.updateUsername}
+ placeholder='Username'
+ />
+ </div>
+ <div className='padding-top'>{'The username to use when overriding the post.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Icon URL:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='iconURL'
+ className='form-control'
+ value={this.state.cmd.icon_url}
+ onChange={this.updateIconURL}
+ placeholder='https://www.example.com/myicon.png'
+ />
+ </div>
+ <div className='padding-top'>{'URL to an icon'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Trigger:'}</label>
+ <div className='padding-top'>
+ {'/'}
+ <input
+ ref='trigger'
+ className='form-control'
+ value={this.state.cmd.trigger}
+ onChange={this.updateTrigger}
+ placeholder='Command trigger e.g. "hello" not including the slash'
+ />
+ </div>
+ <div className='padding-top'>{'Word to trigger on'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Autocomplete:'}</label>
+ <div className='padding-top'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.cmd.auto_complete}
+ onChange={this.updateAutoComplete}
+ />
+ {'A short description of what this commands does'}
+ </label>
+ </div>
+ <div className='padding-top'>{'Show this command in autocomplete list.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Autocomplete Description:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteDesc'
+ className='form-control'
+ value={this.state.cmd.auto_complete_desc}
+ onChange={this.updateAutoCompleteDesc}
+ placeholder='A short description of what this commands does.'
+ />
+ </div>
+ <div className='padding-top'>{'A short description of what this commands does.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Autocomplete Hint:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteHint'
+ className='form-control'
+ value={this.state.cmd.auto_complete_hint}
+ onChange={this.updateAutoCompleteHint}
+ placeholder='[zipcode]'
+ />
+ </div>
+ <div className='padding-top'>{'List parameters to be passed to the command.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Request Type:'}</label>
+ <div className='padding-top'>
+ <select
+ ref='method'
+ className='form-control'
+ value={this.state.cmd.method}
+ onChange={this.updateMethod}
+ >
+ <option value='P'>{'POST'}</option>
+ <option value='G'>{'GET'}</option>
+ </select>
+ </div>
+ <div className='padding-top'>{'Command request type issued to the callback URL.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>{'Callback URL:'}</label>
+ <div className='padding-top'>
+ <input
+ ref='URL'
+ className='form-control'
+ value={this.state.cmd.url}
+ rows={1}
+ onChange={this.updateURL}
+ placeholder='Must start with http:// or https://'
+ />
+ </div>
+ <div className='padding-top'>{'URL that will receive the HTTP POST or GET event'}</div>
+ {addError}
+ </div>
+ <div className='padding-top padding-bottom'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ disabled={disableButton}
+ onClick={this.addNewCmd}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingCmds}
+ {editError}
+ </div>
+ );
+ }
+}
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index abd04a301..cd9903878 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -5,6 +5,7 @@ import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import ManageIncomingHooks from './manage_incoming_hooks.jsx';
import ManageOutgoingHooks from './manage_outgoing_hooks.jsx';
+import ManageCommandHooks from './manage_command_hooks.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -41,6 +42,7 @@ class UserSettingsIntegrationsTab extends React.Component {
render() {
let incomingHooksSection;
let outgoingHooksSection;
+ let commandHooksSection;
var inputs = [];
const {formatMessage} = this.props.intl;
@@ -106,6 +108,37 @@ class UserSettingsIntegrationsTab extends React.Component {
}
}
+ if (global.window.mm_config.EnableCommands === 'true') {
+ if (this.props.activeSection === 'command-hooks') {
+ inputs.push(
+ <ManageCommandHooks key='command-hook-ui' />
+ );
+
+ commandHooksSection = (
+ <SettingItemMax
+ title='Commands'
+ width='medium'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ commandHooksSection = (
+ <SettingItemMin
+ title='Commands'
+ width='medium'
+ describe='Manage your commands'
+ updateSection={() => {
+ this.updateSection('command-hooks');
+ }}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -144,6 +177,8 @@ class UserSettingsIntegrationsTab extends React.Component {
<div className='divider-light'/>
{outgoingHooksSection}
<div className='divider-dark'/>
+ {commandHooksSection}
+ <div className='divider-dark'/>
</div>
</div>
);
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index 0ee89b9fa..5378a2ba6 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -748,20 +748,28 @@ export function savePreferences(preferences, success, error) {
}
export function getSuggestedCommands(command, suggestionId, component) {
- client.executeCommand(
- '',
- command,
- true,
+ client.listCommands(
(data) => {
+ var matches = [];
+ data.forEach((cmd) => {
+ if (('/' + cmd.trigger).indexOf(command) === 0) {
+ matches.push({
+ suggestion: '/' + cmd.trigger + ' ' + cmd.auto_complete_hint,
+ description: cmd.auto_complete_desc
+ });
+ }
+ });
+
// pull out the suggested commands from the returned data
- const terms = data.suggestions.map((suggestion) => suggestion.suggestion);
+ //const terms = matches.map((suggestion) => suggestion.trigger);
+ const terms = matches.map((suggestion) => suggestion.suggestion);
AppDispatcher.handleServerAction({
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
id: suggestionId,
matchedPretext: command,
terms,
- items: data.suggestions,
+ items: matches,
component
});
},
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 09cd4162a..855de3fc2 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -872,11 +872,11 @@ export function getChannelExtraInfo(id, memberLimit, success, error) {
export function executeCommand(channelId, command, suggest, success, error) {
$.ajax({
- url: '/api/v1/command',
+ url: '/api/v1/commands/execute',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}),
+ data: JSON.stringify({channelId, command, suggest: '' + suggest}),
success,
error: function onError(xhr, status, err) {
var e = handleError('executeCommand', xhr, status, err);
@@ -885,6 +885,78 @@ export function executeCommand(channelId, command, suggest, success, error) {
});
}
+export function addCommand(cmd, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(cmd),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteCommand(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listTeamCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list_team_commands',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listTeamCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenCommandToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenCommandToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('listCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getPostsPage(channelId, offset, limit, success, error, complete) {
$.ajax({
cache: false,
diff --git a/web/web_test.go b/web/web_test.go
index cc7d22559..7617ae54a 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -194,6 +194,10 @@ func TestIncomingWebhook(t *testing.T) {
user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User)
store.Must(api.Srv.Store.User().VerifyEmail(user.Id))
+ c := &api.Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
ApiClient.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}