Commit 5cde2eeb authored by Harrison Healey's avatar Harrison Healey Committed by Christopher Speller
Browse files

PLT-1750 Moved slash commands to backstage

* Added slash commands to InstalledIntegrations page

* Reset installed integration type filter if there is no longer any integrations of the selected type

* Added pages to backstage to add slash commands

* Cleaned up internationalization for slash commands

* Added ability to regen slash command tokens from backstage

* Removed Integrations tab from UserSettings
parent 21d6adbb
This diff is collapsed.
......@@ -56,6 +56,28 @@ export default class AddIntegration extends React.Component {
);
}
if (window.mm_config.EnableCommands === 'true') {
options.push(
<AddIntegrationOption
key='command'
image={WebhookIcon}
title={
<FormattedMessage
id='add_integration.command.title'
defaultMessage='Slash Command'
/>
}
description={
<FormattedMessage
id='add_integration.command.description'
defaultMessage='Create slash commands to send events to external integrations and receive a response.'
/>
}
link={'/settings/integrations/add/command'}
/>
);
}
return (
<div className='backstage-content row'>
<div className='backstage-header'>
......
......@@ -59,6 +59,15 @@ export default class BackstageSidebar extends React.Component {
/>
)}
/>
<BackstageSection
name='command'
title={(
<FormattedMessage
id='backstage_sidebar.integrations.add.command'
defaultMessage='Slash Command'
/>
)}
/>
</BackstageSection>
</BackstageCategory>
</ul>
......
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
export default class InstalledCommand extends React.Component {
static get propTypes() {
return {
command: React.PropTypes.object.isRequired,
onRegenToken: React.PropTypes.func.isRequired,
onDelete: React.PropTypes.func.isRequired
};
}
constructor(props) {
super(props);
this.handleRegenToken = this.handleRegenToken.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
handleRegenToken(e) {
e.preventDefault();
this.props.onRegenToken(this.props.command);
}
handleDelete(e) {
e.preventDefault();
this.props.onDelete(this.props.command);
}
render() {
const command = this.props.command;
return (
<div className='backstage-list__item'>
<div className='item-details'>
<div className='item-details__row'>
<span className='item-details__name'>
{command.display_name}
</span>
<span className='item-details__type'>
<FormattedMessage
id='installed_integrations.commandType'
defaultMessage='(Slash Command)'
/>
</span>
</div>
<div className='item-details__row'>
<span className='item-details__description'>
{command.description}
</span>
</div>
<div className='item-details__row'>
<span className='item-details__creation'>
<FormattedMessage
id='installed_integrations.creation'
defaultMessage='Created by {creator} on {createAt, date, full}'
values={{
creator: Utils.displayUsername(command.creator_Id),
createAt: command.create_at
}}
/>
</span>
</div>
</div>
<div className='item-actions'>
<a
href='#'
onClick={this.handleRegenToken}
>
<FormattedMessage
id='installed_integrations.regenToken'
defaultMessage='Regen Token'
/>
</a>
{' - '}
<a
href='#'
onClick={this.handleDelete}
>
<FormattedMessage
id='installed_integrations.delete'
defaultMessage='Delete'
/>
</a>
</div>
</div>
);
}
}
......@@ -12,20 +12,20 @@ export default class InstalledIncomingWebhook extends React.Component {
static get propTypes() {
return {
incomingWebhook: React.PropTypes.object.isRequired,
onDeleteClick: React.PropTypes.func.isRequired
onDelete: React.PropTypes.func.isRequired
};
}
constructor(props) {
super(props);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
this.handleDelete = this.handleDelete.bind(this);
}
handleDeleteClick(e) {
handleDelete(e) {
e.preventDefault();
this.props.onDeleteClick(this.props.incomingWebhook);
this.props.onDelete(this.props.incomingWebhook);
}
render() {
......@@ -69,7 +69,7 @@ export default class InstalledIncomingWebhook extends React.Component {
<div className='item-actions'>
<a
href='#'
onClick={this.handleDeleteClick}
onClick={this.handleDelete}
>
<FormattedMessage
id='installed_integrations.delete'
......
......@@ -11,6 +11,7 @@ import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
import InstalledIncomingWebhook from './installed_incoming_webhook.jsx';
import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx';
import InstalledCommand from './installed_command.jsx';
import {Link} from 'react-router';
export default class InstalledIntegrations extends React.Component {
......@@ -24,10 +25,13 @@ export default class InstalledIntegrations extends React.Component {
this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this);
this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this);
this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this);
this.regenCommandToken = this.regenCommandToken.bind(this);
this.deleteCommand = this.deleteCommand.bind(this);
this.state = {
incomingWebhooks: [],
outgoingWebhooks: [],
commands: [],
typeFilter: '',
filter: ''
};
......@@ -55,6 +59,16 @@ export default class InstalledIntegrations extends React.Component {
AsyncClient.listOutgoingHooks();
}
}
if (window.mm_config.EnableCommands === 'true') {
if (IntegrationStore.hasReceivedCommands()) {
this.setState({
commands: IntegrationStore.getCommands()
});
} else {
AsyncClient.listTeamCommands();
}
}
}
componentWillUnmount() {
......@@ -62,10 +76,24 @@ export default class InstalledIntegrations extends React.Component {
}
handleIntegrationChange() {
const incomingWebhooks = IntegrationStore.getIncomingWebhooks();
const outgoingWebhooks = IntegrationStore.getOutgoingWebhooks();
const commands = IntegrationStore.getCommands();
this.setState({
incomingWebhooks: IntegrationStore.getIncomingWebhooks(),
outgoingWebhooks: IntegrationStore.getOutgoingWebhooks()
incomingWebhooks,
outgoingWebhooks,
commands
});
// reset the type filter if we were viewing a category that is now empty
if ((this.state.typeFilter === 'incomingWebhooks' && incomingWebhooks.length === 0) ||
(this.state.typeFilter === 'outgoingWebhooks' && outgoingWebhooks.length === 0) ||
(this.state.typeFilter === 'commands' && commands.length === 0)) {
this.setState({
typeFilter: ''
});
}
}
updateTypeFilter(e, typeFilter) {
......@@ -94,10 +122,18 @@ export default class InstalledIntegrations extends React.Component {
AsyncClient.deleteOutgoingHook(outgoingWebhook.id);
}
renderTypeFilters(incomingWebhooks, outgoingWebhooks) {
regenCommandToken(command) {
AsyncClient.regenCommandToken(command.id);
}
deleteCommand(command) {
AsyncClient.deleteCommand(command.id);
}
renderTypeFilters(incomingWebhooks, outgoingWebhooks, commands) {
const fields = [];
if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0) {
if (incomingWebhooks.length > 0 || outgoingWebhooks.length > 0 || commands.length > 0) {
let filterClassName = 'filter-sort';
if (this.state.typeFilter === '') {
filterClassName += ' filter-sort--active';
......@@ -187,6 +223,39 @@ export default class InstalledIntegrations extends React.Component {
);
}
if (commands.length > 0) {
fields.push(
<span
key='commandsDivider'
className='divider'
>
{'|'}
</span>
);
let filterClassName = 'filter-sort';
if (this.state.typeFilter === 'commands') {
filterClassName += ' filter-sort--active';
}
fields.push(
<a
key='commandsFilter'
className={filterClassName}
href='#'
onClick={(e) => this.updateTypeFilter(e, 'commands')}
>
<FormattedMessage
id='installed_integrations.commandsFilter'
defaultMessage='Slash Commands ({count})'
values={{
count: commands.length
}}
/>
</a>
);
}
return (
<div className='backstage-filters__sort'>
{fields}
......@@ -197,7 +266,9 @@ export default class InstalledIntegrations extends React.Component {
render() {
const incomingWebhooks = this.state.incomingWebhooks;
const outgoingWebhooks = this.state.outgoingWebhooks;
const commands = this.state.commands;
// TODO description, name, creator filtering
const filter = this.state.filter.toLowerCase();
const integrations = [];
......@@ -215,7 +286,7 @@ export default class InstalledIntegrations extends React.Component {
<InstalledIncomingWebhook
key={incomingWebhook.id}
incomingWebhook={incomingWebhook}
onDeleteClick={this.deleteIncomingWebhook}
onDelete={this.deleteIncomingWebhook}
/>
);
}
......@@ -242,6 +313,27 @@ export default class InstalledIntegrations extends React.Component {
}
}
if (!this.state.typeFilter || this.state.typeFilter === 'commands') {
for (const command of commands) {
if (filter) {
const channel = ChannelStore.get(command.channel_id);
if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) {
continue;
}
}
integrations.push(
<InstalledCommand
key={command.id}
command={command}
onRegenToken={this.regenCommandToken}
onDelete={this.deleteCommand}
/>
);
}
}
return (
<div className='backstage-content row'>
<div className='installed-integrations'>
......@@ -270,7 +362,7 @@ export default class InstalledIntegrations extends React.Component {
</Link>
</div>
<div className='backstage-filters'>
{this.renderTypeFilters(this.state.incomingWebhooks, this.state.outgoingWebhooks)}
{this.renderTypeFilters(incomingWebhooks, outgoingWebhooks, commands)}
<div className='backstage-filter__search'>
<i className='fa fa-search'></i>
<input
......
This diff is collapsed.
......@@ -7,7 +7,6 @@ import NotificationsTab from './user_settings_notifications.jsx';
import SecurityTab from './user_settings_security.jsx';
import GeneralTab from './user_settings_general.jsx';
import DeveloperTab from './user_settings_developer.jsx';
import IntegrationsTab from './user_settings_integrations.jsx';
import DisplayTab from './user_settings_display.jsx';
import AdvancedTab from './user_settings_advanced.jsx';
......@@ -98,20 +97,6 @@ export default class UserSettings extends React.Component {
/>
</div>
);
} else if (this.props.activeTab === 'integrations') {
return (
<div>
<IntegrationsTab
ref='activeTab'
user={this.state.user}
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
updateTab={this.props.updateTab}
closeModal={this.props.closeModal}
collapseModal={this.props.collapseModal}
/>
</div>
);
} else if (this.props.activeTab === 'display') {
return (
<div>
......
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import $ from 'jquery';
import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import ManageCommandHooks from './manage_command_hooks.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
const holders = defineMessages({
cmdName: {
id: 'user.settings.integrations.commands',
defaultMessage: 'Slash Commands'
},
cmdDesc: {
id: 'user.settings.integrations.commandsDescription',
defaultMessage: 'Manage your slash commands'
}
});
import React from 'react';
class UserSettingsIntegrationsTab extends React.Component {
constructor(props) {
super(props);
this.updateSection = this.updateSection.bind(this);
this.state = {};
}
updateSection(section) {
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
this.props.updateSection(section);
}
render() {
let commandHooksSection;
var inputs = [];
const {formatMessage} = this.props.intl;
if (global.window.mm_config.EnableCommands === 'true') {
if (this.props.activeSection === 'command-hooks') {
inputs.push(
<ManageCommandHooks key='command-hook-ui'/>
);
commandHooksSection = (
<SettingItemMax
title={formatMessage(holders.cmdName)}
width='medium'
inputs={inputs}
updateSection={(e) => {
this.updateSection('');
e.preventDefault();
}}
/>
);
} else {
commandHooksSection = (
<SettingItemMin
title={formatMessage(holders.cmdName)}
width='medium'
describe={formatMessage(holders.cmdDesc)}
updateSection={() => {
this.updateSection('command-hooks');
}}
/>
);
}
}
return (
<div>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
onClick={this.props.closeModal}
>
<span aria-hidden='true'>{'×'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
<div className='modal-back'>
<i
className='fa fa-angle-left'
onClick={this.props.collapseModal}
/>
</div>
<FormattedMessage
id='user.settings.integrations.title'
defaultMessage='Integration Settings'
/>
</h4>
</div>
<div className='user-settings'>
<h3 className='tab-header'>
<FormattedMessage
id='user.settings.integrations.title'
defaultMessage='Integration Settings'
/>
</h3>
<div className='divider-dark first'/>
{commandHooksSection}
<div className='divider-dark'/>
</div>
</div>
);
}
}
UserSettingsIntegrationsTab.propTypes = {
intl: intlShape.isRequired,
user: React.PropTypes.object,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func,
activeSection: React.PropTypes.string,
closeModal: React.PropTypes.func.isRequired,
collapseModal: React.PropTypes.func.isRequired
};
export default injectIntl(UserSettingsIntegrationsTab);
......@@ -31,10 +31,6 @@ const holders = defineMessages({
id: 'user.settings.modal.developer',
defaultMessage: 'Developer'
},
integrations: {
id: 'user.settings.modal.integrations',
defaultMessage: 'Integrations'
},
display: {
id: 'user.settings.modal.display',
defaultMessage: 'Display'
......@@ -227,7 +223,6 @@ class UserSettingsModal extends React.Component {
if (this.state.currentUser == null) {
return (<div/>);
}
var isAdmin = Utils.isAdmin(this.state.currentUser.roles);
var tabs = [];
tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'});
......@@ -237,18 +232,6 @@ class UserSettingsModal extends React.Component {
tabs.push({name: 'developer', uiName: formatMessage(holders.developer), icon: 'glyphicon glyphicon-th'});
}
if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true' || global.window.mm_config.EnableCommands === 'true') {
var show = global.window.mm_config.EnableOnlyAdminIntegrations !== 'true';
if (global.window.mm_config.EnableOnlyAdminIntegrations === 'true' && isAdmin) {
show = true;
}
if (show) {
tabs.push({name: 'integrations', uiName: formatMessage(holders.integrations), icon: 'glyphicon glyphicon-transfer'});
}
}
tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'glyphicon glyphicon-eye-open'});
tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'glyphicon glyphicon-list-alt'});
......
......@@ -27,6 +27,36 @@
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android Native App",
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
"add_command.autocomplete": "Autocomplete",
"add_command.autocomplete.help": " Show this command in the autocomplete list.",
"add_command.autocompleteDescription": "Autocomplete Description",
"add_command.autocompleteDescription.help": "Optional short description of slash command for the autocomplete list.",
"add_command.autocompleteDescription.placeholder": "Example: \"Returns search results for patient records\"",
"add_command.autocompleteHint": "Autocomplete Hint",
"add_command.autocompleteHint.help": "Optional hint in the autocomplete list about parameters needed for command.",
"add_command.autocompleteHint.placeholder": "Example: [Patient Name]",
"add_command.description": "Description",
"add_command.displayName": "Display Name",
"add_command.header": "Add Slash Command",
"add_command.iconUrl": "Response Icon",
"add_command.iconUrl.placeholder": "https://www.example.com/myicon.png",
"add_command.iconUrl.help": "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.",
"add_command.method": "Request Method",
"add_command.method.get": "GET",
"add_command.method.help": "The type of command request issued to the Request URL.",
"add_command.method.post": "POST",
"add_command.trigger": "Command Trigger Word",
"add_command.trigger.help1": "Examples: /patient, /client, /employee",
"add_command.trigger.help2": "Reserved: /echo, /join, /logout, /me, /shrug",
"add_command.trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash",
"add_command.triggerRequired": "A trigger word is required",
"add_command.username": "Response Username",
"add_command.username.help": "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 \".\" .",
"add_command.username.placeholder": "Username",
"add_command.url": "Request URL",
"add_command.url.help": "The callback URL to receive the HTTP POST or GET event request when the slash command is run.",
"add_command.url.placeholder": "Must start with http:// or https://",
"add_command.urlRequired": "A request URL is required",
"add_incoming_webhook.cancel": "Cancel",
"add_incoming_webhook.channel": "Channel",
"add_incoming_webhook.channelRequired": "A valid channel is required",
......