summaryrefslogtreecommitdiffstats
path: root/webapp/components/backstage
diff options
context:
space:
mode:
authorHarrison Healey <harrisonmhealey@gmail.com>2016-04-05 09:29:01 -0400
committerChristopher Speller <crspeller@gmail.com>2016-04-05 09:29:01 -0400
commitb3edd32aee47a0b123870de58664600acc17087b (patch)
tree41840177672480ff428f279437a5a08a6eccaeb6 /webapp/components/backstage
parentc12d997f248c143b7746d07a3c2ce9b58a3ecd5e (diff)
downloadchat-b3edd32aee47a0b123870de58664600acc17087b.tar.gz
chat-b3edd32aee47a0b123870de58664600acc17087b.tar.bz2
chat-b3edd32aee47a0b123870de58664600acc17087b.zip
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
Diffstat (limited to 'webapp/components/backstage')
-rw-r--r--webapp/components/backstage/add_command.jsx509
-rw-r--r--webapp/components/backstage/add_integration.jsx22
-rw-r--r--webapp/components/backstage/backstage_sidebar.jsx9
-rw-r--r--webapp/components/backstage/installed_command.jsx97
-rw-r--r--webapp/components/backstage/installed_incoming_webhook.jsx10
-rw-r--r--webapp/components/backstage/installed_integrations.jsx104
6 files changed, 740 insertions, 11 deletions
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/backstage/add_command.jsx
new file mode 100644
index 000000000..93ff66271
--- /dev/null
+++ b/webapp/components/backstage/add_command.jsx
@@ -0,0 +1,509 @@
+// 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 {browserHistory} from 'react-router';
+import * as Utils from 'utils/utils.jsx';
+
+import {FormattedMessage} from 'react-intl';
+import FormError from 'components/form_error.jsx';
+import {Link} from 'react-router';
+import SpinnerButton from 'components/spinner_button.jsx';
+
+const REQUEST_POST = 'P';
+const REQUEST_GET = 'G';
+
+export default class AddCommand extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.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.state = {
+ displayName: '',
+ description: '',
+ trigger: '',
+ url: '',
+ method: REQUEST_POST,
+ username: '',
+ iconUrl: '',
+ autocomplete: false,
+ autocompleteHint: '',
+ autocompleteDescription: '',
+ saving: false,
+ serverError: '',
+ clientError: null
+ };
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+
+ if (this.state.saving) {
+ return;
+ }
+
+ this.setState({
+ saving: true,
+ serverError: '',
+ clientError: ''
+ });
+
+ const command = {
+ display_name: this.state.displayName,
+ description: this.state.description,
+ trigger: this.state.trigger.trim(),
+ url: this.state.url.trim(),
+ method: this.state.method,
+ username: this.state.username,
+ icon_url: this.state.iconUrl,
+ auto_complete: this.state.autocomplete
+ };
+
+ 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'
+ />
+ )
+ });
+ }
+
+ if (!command.url) {
+ this.setState({
+ saving: false,
+ clientError: (
+ <FormattedMessage
+ id='add_command.urlRequired'
+ defaultMessage='A request URL is required'
+ />
+ )
+ });
+ }
+
+ AsyncClient.addCommand(
+ command,
+ () => {
+ browserHistory.push('/settings/integrations/installed');
+ },
+ (err) => {
+ this.setState({
+ saving: false,
+ serverError: err.message
+ });
+ }
+ );
+ }
+
+ 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() {
+ let autocompleteFields = null;
+ if (this.state.autocomplete) {
+ autocompleteFields = [(
+ <div
+ key='autocompleteHint'
+ className='form-group'
+ >
+ <label
+ className='control-label col-sm-3'
+ htmlFor='autocompleteHint'
+ >
+ <FormattedMessage
+ id='add_command.autocompleteHint'
+ defaultMessage='Autocomplete Hint'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <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='add-integration__help'>
+ <FormattedMessage
+ id='add_command.autocompleteDescription.help'
+ defaultMessage='Optional hint in the autocomplete list about command parameters'
+ />
+ </div>
+ </div>
+ </div>
+ ),
+ (
+ <div
+ key='autocompleteDescription'
+ className='form-group'
+ >
+ <label
+ className='control-label col-sm-3'
+ htmlFor='autocompleteDescription'
+ >
+ <FormattedMessage
+ id='add_command.autocompleteDescription'
+ defaultMessage='Autocomplete Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <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='add-integration__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'>
+ <div className='add-command'>
+ <div className='backstage-header'>
+ <h1>
+ <FormattedMessage
+ id='add_command.header'
+ defaultMessage='Add Slash Command'
+ />
+ </h1>
+ </div>
+ </div>
+ <div className='backstage-form'>
+ <form className='form-horizontal'>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='displayName'
+ >
+ <FormattedMessage
+ id='add_command.displayName'
+ defaultMessage='Display Name'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='displayName'
+ type='text'
+ maxLength='64'
+ className='form-control'
+ value={this.state.displayName}
+ onChange={this.updateDisplayName}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='description'
+ >
+ <FormattedMessage
+ id='add_command.description'
+ defaultMessage='Description'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='description'
+ type='text'
+ maxLength='128'
+ className='form-control'
+ value={this.state.description}
+ onChange={this.updateDescription}
+ />
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='trigger'
+ >
+ <FormattedMessage
+ id='add_command.trigger'
+ defaultMessage='Command Trigger Word'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ id='trigger'
+ type='text'
+ maxLength='128'
+ 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='add-integration__help'>
+ <FormattedMessage
+ id='add_command.trigger.help1'
+ defaultMessage='Examples: /patient, /client /employee'
+ />
+ </div>
+ <div className='add-integration__help'>
+ <FormattedMessage
+ id='add_command.trigger.help2'
+ defaultMessage='Reserved: /echo, /join, /logout, /me, /shrug'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='url'
+ >
+ <FormattedMessage
+ id='add_command.url'
+ defaultMessage='Request URL'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <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='add-integration__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-3'
+ htmlFor='method'
+ >
+ <FormattedMessage
+ id='add_command.method'
+ defaultMessage='Request Method'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <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='add-integration__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-lavel col-sm-3'
+ htmlFor='username'
+ >
+ <FormattedMessage
+ id='add_command.username'
+ defaultMessage='Response Username'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <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='add-integration__help'>
+ <FormattedMessage
+ id='add_command.username.help'
+ defaultMessage='Choose a username override for responses for this slash command. Usernames can consist of up to 22 characters consisting of lowercase letters, numbers and the symbols "-", "_", and ".".'
+ />
+ </div>
+ </div>
+ </div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-3'
+ htmlFor='iconUrl'
+ >
+ <FormattedMessage
+ id='add_command.iconUrl'
+ defaultMessage='Response Icon'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <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='add-integration__help'>
+ <FormattedMessage
+ id='add_command.iconUrl.help'
+ defaultMessage='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-3'
+ htmlFor='autocomplete'
+ >
+ <FormattedMessage
+ id='add_command.autocomplete'
+ defaultMessage='Autocomplete'
+ />
+ </label>
+ <div className='col-md-5 col-sm-9'>
+ <input
+ type='checkbox'
+ checked={this.state.autocomplete}
+ onChange={this.updateAutocomplete}
+ />
+ <div className='add-integration__help'>
+ <FormattedMessage
+ id='add_command.autocomplete.help'
+ defaultMessage='Show this command in the autocomplete list'
+ />
+ </div>
+ </div>
+ </div>
+ {autocompleteFields}
+ <div className='backstage-form__footer'>
+ <FormError errors={[this.state.serverError, this.state.clientError]}/>
+ <Link
+ className='btn btn-sm'
+ to={'/settings/integrations/add'}
+ >
+ <FormattedMessage
+ id='add_command.cancel'
+ defaultMessage='Cancel'
+ />
+ </Link>
+ <SpinnerButton
+ className='btn btn-primary'
+ type='submit'
+ spinning={this.state.saving}
+ onClick={this.handleSubmit}
+ >
+ <FormattedMessage
+ id='add_command.save'
+ defaultMessage='Save'
+ />
+ </SpinnerButton>
+ </div>
+ </form>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/add_integration.jsx
index 5f4a69bfe..0ab36e101 100644
--- a/webapp/components/backstage/add_integration.jsx
+++ b/webapp/components/backstage/add_integration.jsx
@@ -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'>
diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx
index 13c4f8b50..172119b32 100644
--- a/webapp/components/backstage/backstage_sidebar.jsx
+++ b/webapp/components/backstage/backstage_sidebar.jsx
@@ -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>
diff --git a/webapp/components/backstage/installed_command.jsx b/webapp/components/backstage/installed_command.jsx
new file mode 100644
index 000000000..51adce160
--- /dev/null
+++ b/webapp/components/backstage/installed_command.jsx
@@ -0,0 +1,97 @@
+// 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>
+ );
+ }
+}
diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx
index 95a303edc..cd9a6d761 100644
--- a/webapp/components/backstage/installed_incoming_webhook.jsx
+++ b/webapp/components/backstage/installed_incoming_webhook.jsx
@@ -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'
diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx
index fe84ae81a..e353b7f29 100644
--- a/webapp/components/backstage/installed_integrations.jsx
+++ b/webapp/components/backstage/installed_integrations.jsx
@@ -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