diff options
author | Harrison Healey <harrisonmhealey@gmail.com> | 2016-04-08 11:51:28 -0400 |
---|---|---|
committer | Christopher Speller <crspeller@gmail.com> | 2016-04-08 11:51:28 -0400 |
commit | 77ee1ce7fee698847e211dc15d4673300901aa48 (patch) | |
tree | 115391ae591f7e008cf357238be612e7482742fc /webapp | |
parent | 742d611ba4c08dbc4d30d3ef7a40a872186bd9eb (diff) | |
download | chat-77ee1ce7fee698847e211dc15d4673300901aa48.tar.gz chat-77ee1ce7fee698847e211dc15d4673300901aa48.tar.bz2 chat-77ee1ce7fee698847e211dc15d4673300901aa48.zip |
PLT-2553 Updated backstage page navigation (#2661)
* Updated integrations list based on feedback
* Reorganized Integrations pages
* Repurposed AddIntegration page as a landing page for Integrations
* Moved backstage breadcrumb header into its own component
* Removed unnecessary prop
* Fixed Save links on AddIntegration pages
Diffstat (limited to 'webapp')
18 files changed, 656 insertions, 493 deletions
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/backstage/add_command.jsx index b6f01b4d8..2eb7bdb21 100644 --- a/webapp/components/backstage/add_command.jsx +++ b/webapp/components/backstage/add_command.jsx @@ -7,6 +7,7 @@ import * as AsyncClient from 'utils/async_client.jsx'; import {browserHistory} from 'react-router'; import * as Utils from 'utils/utils.jsx'; +import BackstageHeader from './backstage_header.jsx'; import {FormattedMessage} from 'react-intl'; import FormError from 'components/form_error.jsx'; import {Link} from 'react-router'; @@ -105,7 +106,7 @@ export default class AddCommand extends React.Component { AsyncClient.addCommand( command, () => { - browserHistory.push('/settings/integrations/installed'); + browserHistory.push('/settings/integrations/commands'); }, (err) => { this.setState({ @@ -249,16 +250,18 @@ export default class AddCommand extends React.Component { 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> + <BackstageHeader> + <Link to={'/settings/integrations/commands'}> + <FormattedMessage + id='installed_command.header' + defaultMessage='Slash Commands' + /> + </Link> + <FormattedMessage + id='add_command.header' + defaultMessage='Add' + /> + </BackstageHeader> <div className='backstage-form'> <form className='form-horizontal'> <div className='form-group'> @@ -479,7 +482,7 @@ export default class AddCommand extends React.Component { <FormError errors={[this.state.serverError, this.state.clientError]}/> <Link className='btn btn-sm' - to={'/settings/integrations/add'} + to={'/settings/integrations/commands'} > <FormattedMessage id='add_command.cancel' diff --git a/webapp/components/backstage/add_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx index b0c16b9ff..f68a263be 100644 --- a/webapp/components/backstage/add_incoming_webhook.jsx +++ b/webapp/components/backstage/add_incoming_webhook.jsx @@ -6,6 +6,7 @@ import React from 'react'; import * as AsyncClient from 'utils/async_client.jsx'; import {browserHistory} from 'react-router'; +import BackstageHeader from './backstage_header.jsx'; import ChannelSelect from 'components/channel_select.jsx'; import {FormattedMessage} from 'react-intl'; import FormError from 'components/form_error.jsx'; @@ -68,7 +69,7 @@ export default class AddIncomingWebhook extends React.Component { AsyncClient.addIncomingHook( hook, () => { - browserHistory.push('/settings/integrations/installed'); + browserHistory.push('/settings/integrations/incoming_webhooks'); }, (err) => { this.setState({ @@ -99,17 +100,19 @@ export default class AddIncomingWebhook extends React.Component { render() { return ( - <div className='backstage-content row'> - <div className='add-incoming-webhook'> - <div className='backstage-header'> - <h1> - <FormattedMessage - id='add_incoming_webhook.header' - defaultMessage='Add Incoming Webhook' - /> - </h1> - </div> - </div> + <div className='backstage-content'> + <BackstageHeader> + <Link to={'/settings/integrations/incoming_webhooks'}> + <FormattedMessage + id='installed_incoming_webhooks.header' + defaultMessage='Incoming Webhooks' + /> + </Link> + <FormattedMessage + id='add_incoming_webhook.header' + defaultMessage='Add' + /> + </BackstageHeader> <div className='backstage-form'> <form className='form-horizontal'> <div className='form-group'> @@ -176,7 +179,7 @@ export default class AddIncomingWebhook extends React.Component { <FormError errors={[this.state.serverError, this.state.clientError]}/> <Link className='btn btn-sm' - to={'/settings/integrations/add'} + to={'/settings/integrations/incoming_webhooks'} > <FormattedMessage id='add_incoming_webhook.cancel' diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx index 9d1f79e5d..acdd98ba8 100644 --- a/webapp/components/backstage/add_outgoing_webhook.jsx +++ b/webapp/components/backstage/add_outgoing_webhook.jsx @@ -6,6 +6,7 @@ import React from 'react'; import * as AsyncClient from 'utils/async_client.jsx'; import {browserHistory} from 'react-router'; +import BackstageHeader from './backstage_header.jsx'; import ChannelSelect from 'components/channel_select.jsx'; import {FormattedMessage} from 'react-intl'; import FormError from 'components/form_error.jsx'; @@ -88,7 +89,7 @@ export default class AddOutgoingWebhook extends React.Component { AsyncClient.addOutgoingHook( hook, () => { - browserHistory.push('/settings/integrations/installed'); + browserHistory.push('/settings/integrations/outgoing_webhooks'); }, (err) => { this.setState({ @@ -131,17 +132,19 @@ export default class AddOutgoingWebhook extends React.Component { render() { return ( - <div className='backstage-content row'> - <div className='add-outgoing-webhook'> - <div className='backstage-header'> - <h1> - <FormattedMessage - id='add_outgoing_webhook.header' - defaultMessage='Add Outgoing Webhook' - /> - </h1> - </div> - </div> + <div className='backstage-content'> + <BackstageHeader> + <Link to={'/settings/integrations/outgoing_webhooks'}> + <FormattedMessage + id='installed_outgoing_webhooks.header' + defaultMessage='Outgoing Webhooks' + /> + </Link> + <FormattedMessage + id='add_outgoing_webhook.header' + defaultMessage='Add' + /> + </BackstageHeader> <div className='backstage-form'> <form className='form-horizontal'> <div className='form-group'> @@ -250,7 +253,7 @@ export default class AddOutgoingWebhook extends React.Component { <FormError errors={[this.state.serverError, this.state.clientError]}/> <Link className='btn btn-sm' - to={'/settings/integrations/add'} + to={'/settings/integrations/outgoing_webhooks'} > <FormattedMessage id='add_outgoing_webhook.cancel' diff --git a/webapp/components/backstage/backstage_header.jsx b/webapp/components/backstage/backstage_header.jsx new file mode 100644 index 000000000..95b35d7a9 --- /dev/null +++ b/webapp/components/backstage/backstage_header.jsx @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +export default class BackstageHeader extends React.Component { + static get propTypes() { + return { + children: React.PropTypes.node + }; + } + + render() { + const children = []; + + React.Children.forEach(this.props.children, (child, index) => { + if (index !== 0) { + children.push( + <span + key={'divider' + index} + className='backstage-header__divider' + > + {'>'} + </span> + ); + } + + children.push(child); + }); + + return ( + <div className='backstage-header'> + <h1> + {children} + </h1> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/backstage_section.jsx index d6ce2b258..120e956b0 100644 --- a/webapp/components/backstage/backstage_section.jsx +++ b/webapp/components/backstage/backstage_section.jsx @@ -65,7 +65,6 @@ export default class BackstageSection extends React.Component { <Link className={`${className}-title`} activeClassName={`${className}-title--active`} - onlyActiveOnIndex={true} onClick={this.handleClick} to={link} > diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/backstage_sidebar.jsx index 172119b32..eb84709a3 100644 --- a/webapp/components/backstage/backstage_sidebar.jsx +++ b/webapp/components/backstage/backstage_sidebar.jsx @@ -24,51 +24,32 @@ export default class BackstageSidebar extends React.Component { } > <BackstageSection - name='installed' + name='incoming_webhooks' title={( <FormattedMessage - id='backstage_sidebar.integrations.installed' - defaultMessage='Installed Integrations' + id='backstage_sidebar.integrations.incoming_webhooks' + defaultMessage='Incoming Webhooks' /> )} /> <BackstageSection - name='add' + name='outgoing_webhooks' title={( <FormattedMessage - id='backstage_sidebar.integrations.add' - defaultMessage='Add Integration' + id='backstage_sidebar.integrations.outgoing_webhooks' + defaultMessage='Outgoing Webhooks' /> )} - > - <BackstageSection - name='incoming_webhook' - title={( - <FormattedMessage - id='backstage_sidebar.integrations.add.incomingWebhook' - defaultMessage='Incoming Webhook' - /> - )} - /> - <BackstageSection - name='outgoing_webhook' - title={( - <FormattedMessage - id='backstage_sidebar.integrations.add.outgoingWebhook' - defaultMessage='Outgoing Webhook' - /> - )} - /> - <BackstageSection - name='command' - title={( - <FormattedMessage - id='backstage_sidebar.integrations.add.command' - defaultMessage='Slash Command' - /> - )} - /> - </BackstageSection> + /> + <BackstageSection + name='commands' + title={( + <FormattedMessage + id='backstage_sidebar.integrations.commands' + defaultMessage='Slash Commands' + /> + )} + /> </BackstageCategory> </ul> </div> diff --git a/webapp/components/backstage/installed_command.jsx b/webapp/components/backstage/installed_command.jsx index 51adce160..8b56ed595 100644 --- a/webapp/components/backstage/installed_command.jsx +++ b/webapp/components/backstage/installed_command.jsx @@ -12,7 +12,8 @@ export default class InstalledCommand extends React.Component { return { command: React.PropTypes.object.isRequired, onRegenToken: React.PropTypes.func.isRequired, - onDelete: React.PropTypes.func.isRequired + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string }; } @@ -21,6 +22,8 @@ export default class InstalledCommand extends React.Component { this.handleRegenToken = this.handleRegenToken.bind(this); this.handleDelete = this.handleDelete.bind(this); + + this.matchesFilter = this.matchesFilter.bind(this); } handleRegenToken(e) { @@ -35,26 +38,67 @@ export default class InstalledCommand extends React.Component { this.props.onDelete(this.props.command); } + matchesFilter(command, filter) { + if (!filter) { + return true; + } + + return command.display_name.toLowerCase().indexOf(filter) !== -1 || + command.description.toLowerCase().indexOf(filter) !== -1 || + command.trigger.toLowerCase().indexOf(filter) !== -1; + } + render() { const command = this.props.command; + if (!this.matchesFilter(command, this.props.filter)) { + return null; + } + + let name; + if (command.display_name) { + name = command.display_name; + } else { + name = ( + <FormattedMessage + id='installed_integraions.unnamed_command' + defaultMessage='Unnamed Slash Command' + /> + ); + } + + let description = null; + if (command.description) { + description = ( + <div className='item-details__row'> + <span className='item-details__description'> + {command.description} + </span> + </div> + ); + } + return ( <div className='backstage-list__item'> <div className='item-details'> <div className='item-details__row'> <span className='item-details__name'> - {command.display_name} + {name} </span> - <span className='item-details__type'> - <FormattedMessage - id='installed_integrations.commandType' - defaultMessage='(Slash Command)' - /> + <span className='item-details__trigger'> + {'- /' + command.trigger} </span> </div> + {description} <div className='item-details__row'> - <span className='item-details__description'> - {command.description} + <span className='item-details__token'> + <FormattedMessage + id='installed_integrations.token' + defaultMessage='Token: {token}' + values={{ + token: command.token + }} + /> </span> </div> <div className='item-details__row'> @@ -63,7 +107,7 @@ export default class InstalledCommand extends React.Component { id='installed_integrations.creation' defaultMessage='Created by {creator} on {createAt, date, full}' values={{ - creator: Utils.displayUsername(command.creator_Id), + creator: Utils.displayUsername(command.creator_id), createAt: command.create_at }} /> diff --git a/webapp/components/backstage/installed_commands.jsx b/webapp/components/backstage/installed_commands.jsx new file mode 100644 index 000000000..ead2f9850 --- /dev/null +++ b/webapp/components/backstage/installed_commands.jsx @@ -0,0 +1,93 @@ +// 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 {FormattedMessage} from 'react-intl'; +import InstalledCommand from './installed_command.jsx'; +import InstalledIntegrations from './installed_integrations.jsx'; + +export default class InstalledCommands extends React.Component { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + + this.regenCommandToken = this.regenCommandToken.bind(this); + this.deleteCommand = this.deleteCommand.bind(this); + + this.state = { + commands: [] + }; + } + + componentWillMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableCommands === 'true') { + if (IntegrationStore.hasReceivedCommands()) { + this.setState({ + commands: IntegrationStore.getCommands() + }); + } else { + AsyncClient.listTeamCommands(); + } + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + const commands = IntegrationStore.getCommands(); + + this.setState({ + commands + }); + } + + regenCommandToken(command) { + AsyncClient.regenCommandToken(command.id); + } + + deleteCommand(command) { + AsyncClient.deleteCommand(command.id); + } + + render() { + const commands = this.state.commands.map((command) => { + return ( + <InstalledCommand + key={command.id} + command={command} + onRegenToken={this.regenCommandToken} + onDelete={this.deleteCommand} + /> + ); + }); + + return ( + <InstalledIntegrations + header={ + <FormattedMessage + id='installed_integrations.commands' + defaultMessage='Installed Commands' + /> + } + addText={ + <FormattedMessage + id='installed_integrations.add_command' + defaultMessage='Add Command' + /> + } + addLink='/settings/integrations/commands/add' + > + {commands} + </InstalledIntegrations> + ); + } +} diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx index cd9a6d761..58d318310 100644 --- a/webapp/components/backstage/installed_incoming_webhook.jsx +++ b/webapp/components/backstage/installed_incoming_webhook.jsx @@ -12,7 +12,8 @@ export default class InstalledIncomingWebhook extends React.Component { static get propTypes() { return { incomingWebhook: React.PropTypes.object.isRequired, - onDelete: React.PropTypes.func.isRequired + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string }; } @@ -28,31 +29,67 @@ export default class InstalledIncomingWebhook extends React.Component { this.props.onDelete(this.props.incomingWebhook); } + matchesFilter(incomingWebhook, channel, filter) { + if (!filter) { + return true; + } + + if (incomingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 || + incomingWebhook.description.toLowerCase().indexOf(filter) !== -1) { + return true; + } + + if (incomingWebhook.channel_id) { + if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + return false; + } + render() { const incomingWebhook = this.props.incomingWebhook; - const channel = ChannelStore.get(incomingWebhook.channel_id); - const channelName = channel ? channel.display_name : 'cannot find channel'; + + if (!this.matchesFilter(incomingWebhook, channel, this.props.filter)) { + return null; + } + + let displayName; + if (incomingWebhook.display_name) { + displayName = incomingWebhook.display_name; + } else if (channel) { + displayName = channel.display_name; + } else { + displayName = ( + <FormattedMessage + id='installed_incoming_webhooks.unknown_channel' + defaultMessage='A Private Webhook' + /> + ); + } + + let description = null; + if (incomingWebhook.description) { + description = ( + <div className='item-details__row'> + <span className='item-details__description'> + {incomingWebhook.description} + </span> + </div> + ); + } return ( <div className='backstage-list__item'> <div className='item-details'> <div className='item-details__row'> <span className='item-details__name'> - {incomingWebhook.display_name || channelName} - </span> - <span className='item-details__type'> - <FormattedMessage - id='installed_integrations.incomingWebhookType' - defaultMessage='(Incoming Webhook)' - /> - </span> - </div> - <div className='item-details__row'> - <span className='item-details__description'> - {incomingWebhook.description} + {displayName} </span> </div> + {description} <div className='tem-details__row'> <span className='item-details__creation'> <FormattedMessage diff --git a/webapp/components/backstage/installed_incoming_webhooks.jsx b/webapp/components/backstage/installed_incoming_webhooks.jsx new file mode 100644 index 000000000..de7154afe --- /dev/null +++ b/webapp/components/backstage/installed_incoming_webhooks.jsx @@ -0,0 +1,85 @@ +// 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 {FormattedMessage} from 'react-intl'; +import InstalledIncomingWebhook from './installed_incoming_webhook.jsx'; +import InstalledIntegrations from './installed_integrations.jsx'; + +export default class InstalledIncomingWebhooks extends React.Component { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + + this.deleteIncomingWebhook = this.deleteIncomingWebhook.bind(this); + + this.state = { + incomingWebhooks: [] + }; + } + + componentWillMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableIncomingWebhooks === 'true') { + if (IntegrationStore.hasReceivedIncomingWebhooks()) { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks() + }); + } else { + AsyncClient.listIncomingHooks(); + } + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + this.setState({ + incomingWebhooks: IntegrationStore.getIncomingWebhooks() + }); + } + + deleteIncomingWebhook(incomingWebhook) { + AsyncClient.deleteIncomingHook(incomingWebhook.id); + } + + render() { + const incomingWebhooks = this.state.incomingWebhooks.map((incomingWebhook) => { + return ( + <InstalledIncomingWebhook + key={incomingWebhook.id} + incomingWebhook={incomingWebhook} + onDelete={this.deleteIncomingWebhook} + /> + ); + }); + + return ( + <InstalledIntegrations + header={ + <FormattedMessage + id='installed_incoming_webhooks.header' + defaultMessage='Installed Incoming Webhooks' + /> + } + addText={ + <FormattedMessage + id='installed_incoming_webhooks.add' + defaultMessage='Add Incoming Webhook' + /> + } + addLink='/settings/integrations/incoming_webhooks/add' + > + {incomingWebhooks} + </InstalledIntegrations> + ); + } +} diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx index e353b7f29..baf74447f 100644 --- a/webapp/components/backstage/installed_integrations.jsx +++ b/webapp/components/backstage/installed_integrations.jsx @@ -3,366 +3,65 @@ import React from 'react'; -import * as AsyncClient from 'utils/async_client.jsx'; -import ChannelStore from 'stores/channel_store.jsx'; -import IntegrationStore from 'stores/integration_store.jsx'; 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 { + static get propTypes() { + return { + children: React.PropTypes.node, + header: React.PropTypes.node.isRequired, + addLink: React.PropTypes.string.isRequired, + addText: React.PropTypes.node.isRequired + }; + } + constructor(props) { super(props); - this.handleIntegrationChange = this.handleIntegrationChange.bind(this); this.updateFilter = this.updateFilter.bind(this); - this.updateTypeFilter = this.updateTypeFilter.bind(this); - - 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: '' }; } - componentWillMount() { - IntegrationStore.addChangeListener(this.handleIntegrationChange); - - if (window.mm_config.EnableIncomingWebhooks === 'true') { - if (IntegrationStore.hasReceivedIncomingWebhooks()) { - this.setState({ - incomingWebhooks: IntegrationStore.getIncomingWebhooks() - }); - } else { - AsyncClient.listIncomingHooks(); - } - } - - if (window.mm_config.EnableOutgoingWebhooks === 'true') { - if (IntegrationStore.hasReceivedOutgoingWebhooks()) { - this.setState({ - outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() - }); - } else { - AsyncClient.listOutgoingHooks(); - } - } - - if (window.mm_config.EnableCommands === 'true') { - if (IntegrationStore.hasReceivedCommands()) { - this.setState({ - commands: IntegrationStore.getCommands() - }); - } else { - AsyncClient.listTeamCommands(); - } - } - } - - componentWillUnmount() { - IntegrationStore.removeChangeListener(this.handleIntegrationChange); - } - - handleIntegrationChange() { - const incomingWebhooks = IntegrationStore.getIncomingWebhooks(); - const outgoingWebhooks = IntegrationStore.getOutgoingWebhooks(); - const commands = IntegrationStore.getCommands(); - - this.setState({ - 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) { - e.preventDefault(); - - this.setState({ - typeFilter - }); - } - updateFilter(e) { this.setState({ filter: e.target.value }); } - deleteIncomingWebhook(incomingWebhook) { - AsyncClient.deleteIncomingHook(incomingWebhook.id); - } - - regenOutgoingWebhookToken(outgoingWebhook) { - AsyncClient.regenOutgoingHookToken(outgoingWebhook.id); - } - - deleteOutgoingWebhook(outgoingWebhook) { - AsyncClient.deleteOutgoingHook(outgoingWebhook.id); - } - - 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 || commands.length > 0) { - let filterClassName = 'filter-sort'; - if (this.state.typeFilter === '') { - filterClassName += ' filter-sort--active'; - } - - fields.push( - <a - key='allFilter' - className={filterClassName} - href='#' - onClick={(e) => this.updateTypeFilter(e, '')} - > - <FormattedMessage - id='installed_integrations.allFilter' - defaultMessage='All ({count})' - values={{ - count: incomingWebhooks.length + outgoingWebhooks.length - }} - /> - </a> - ); - } - - if (incomingWebhooks.length > 0) { - fields.push( - <span - key='incomingWebhooksDivider' - className='divider' - > - {'|'} - </span> - ); - - let filterClassName = 'filter-sort'; - if (this.state.typeFilter === 'incomingWebhooks') { - filterClassName += ' filter-sort--active'; - } - - fields.push( - <a - key='incomingWebhooksFilter' - className={filterClassName} - href='#' - onClick={(e) => this.updateTypeFilter(e, 'incomingWebhooks')} - > - <FormattedMessage - id='installed_integrations.incomingWebhooksFilter' - defaultMessage='Incoming Webhooks ({count})' - values={{ - count: incomingWebhooks.length - }} - /> - </a> - ); - } - - if (outgoingWebhooks.length > 0) { - fields.push( - <span - key='outgoingWebhooksDivider' - className='divider' - > - {'|'} - </span> - ); - - let filterClassName = 'filter-sort'; - if (this.state.typeFilter === 'outgoingWebhooks') { - filterClassName += ' filter-sort--active'; - } - - fields.push( - <a - key='outgoingWebhooksFilter' - className={filterClassName} - href='#' - onClick={(e) => this.updateTypeFilter(e, 'outgoingWebhooks')} - > - <FormattedMessage - id='installed_integrations.outgoingWebhooksFilter' - defaultMessage='Outgoing Webhooks ({count})' - values={{ - count: outgoingWebhooks.length - }} - /> - </a> - ); - } - - 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} - </div> - ); - } - 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 = []; - if (!this.state.typeFilter || this.state.typeFilter === 'incomingWebhooks') { - for (const incomingWebhook of incomingWebhooks) { - if (filter) { - const channel = ChannelStore.get(incomingWebhook.channel_id); - - if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) { - continue; - } - } - - integrations.push( - <InstalledIncomingWebhook - key={incomingWebhook.id} - incomingWebhook={incomingWebhook} - onDelete={this.deleteIncomingWebhook} - /> - ); - } - } - - if (!this.state.typeFilter || this.state.typeFilter === 'outgoingWebhooks') { - for (const outgoingWebhook of outgoingWebhooks) { - if (filter) { - const channel = ChannelStore.get(outgoingWebhook.channel_id); - - if (!channel || channel.name.toLowerCase().indexOf(filter) === -1) { - continue; - } - } - - integrations.push( - <InstalledOutgoingWebhook - key={outgoingWebhook.id} - outgoingWebhook={outgoingWebhook} - onRegenToken={this.regenOutgoingWebhookToken} - onDelete={this.deleteOutgoingWebhook} - /> - ); - } - } - - 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} - /> - ); - } - } + const children = React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, {filter}); + }); return ( - <div className='backstage-content row'> + <div className='backstage-content'> <div className='installed-integrations'> <div className='backstage-header'> <h1> - <FormattedMessage - id='installed_integrations.header' - defaultMessage='Installed Integrations' - /> + {this.props.header} </h1> <Link className='add-integrations-link' - to={'/settings/integrations/add'} + to={this.props.addLink} > <button type='button' className='btn btn-primary' > <span> - <FormattedMessage - id='installed_integrations.add' - defaultMessage='Add Integration' - /> + {this.props.addText} </span> </button> </Link> </div> <div className='backstage-filters'> - {this.renderTypeFilters(incomingWebhooks, outgoingWebhooks, commands)} <div className='backstage-filter__search'> <i className='fa fa-search'></i> <input @@ -376,7 +75,7 @@ export default class InstalledIntegrations extends React.Component { </div> </div> <div className='backstage-list'> - {integrations} + {children} </div> </div> </div> diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx index 530474dc3..b8704ccef 100644 --- a/webapp/components/backstage/installed_outgoing_webhook.jsx +++ b/webapp/components/backstage/installed_outgoing_webhook.jsx @@ -13,7 +13,8 @@ export default class InstalledOutgoingWebhook extends React.Component { return { outgoingWebhook: React.PropTypes.object.isRequired, onRegenToken: React.PropTypes.func.isRequired, - onDelete: React.PropTypes.func.isRequired + onDelete: React.PropTypes.func.isRequired, + filter: React.PropTypes.string }; } @@ -36,29 +37,82 @@ export default class InstalledOutgoingWebhook extends React.Component { this.props.onDelete(this.props.outgoingWebhook); } + matchesFilter(outgoingWebhook, channel, filter) { + if (!filter) { + return true; + } + + if (outgoingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 || + outgoingWebhook.description.toLowerCase().indexOf(filter) !== -1) { + return true; + } + + for (const trigger of outgoingWebhook.trigger_words) { + if (trigger.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + if (channel) { + if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + return false; + } + render() { const outgoingWebhook = this.props.outgoingWebhook; - const channel = ChannelStore.get(outgoingWebhook.channel_id); - const channelName = channel ? channel.display_name : 'cannot find channel'; + + if (!this.matchesFilter(outgoingWebhook, channel, this.props.filter)) { + return null; + } + + let displayName; + if (outgoingWebhook.display_name) { + displayName = outgoingWebhook.display_name; + } else if (channel) { + displayName = channel.display_name; + } else { + displayName = ( + <FormattedMessage + id='installed_outgoing_webhooks.unknown_channel' + defaultMessage='A Private Webhook' + /> + ); + } + + let description = null; + if (outgoingWebhook.description) { + description = ( + <div className='item-details__row'> + <span className='item-details__description'> + {outgoingWebhook.description} + </span> + </div> + ); + } return ( <div className='backstage-list__item'> <div className='item-details'> <div className='item-details__row'> <span className='item-details__name'> - {outgoingWebhook.display_name || channelName} - </span> - <span className='item-details__type'> - <FormattedMessage - id='installed_integrations.outgoingWebhookType' - defaultMessage='(Outgoing Webhook)' - /> + {displayName} </span> </div> + {description} <div className='item-details__row'> - <span className='item-details__description'> - {outgoingWebhook.description} + <span className='item-details__token'> + <FormattedMessage + id='installed_integrations.token' + defaultMessage='Token: {token}' + values={{ + token: outgoingWebhook.token + }} + /> </span> </div> <div className='item-details__row'> @@ -98,4 +152,21 @@ export default class InstalledOutgoingWebhook extends React.Component { </div> ); } + + static matches(outgoingWebhook, filter) { + if (outgoingWebhook.display_name.toLowerCase().indexOf(filter) !== -1 || + outgoingWebhook.description.toLowerCase().indexOf(filter) !== -1) { + return true; + } + + if (outgoingWebhook.channel_id) { + const channel = ChannelStore.get(outgoingWebhook.channel_id); + + if (channel && channel.name.toLowerCase().indexOf(filter) !== -1) { + return true; + } + } + + return false; + } } diff --git a/webapp/components/backstage/installed_outgoing_webhooks.jsx b/webapp/components/backstage/installed_outgoing_webhooks.jsx new file mode 100644 index 000000000..15d927a41 --- /dev/null +++ b/webapp/components/backstage/installed_outgoing_webhooks.jsx @@ -0,0 +1,91 @@ +// 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 {FormattedMessage} from 'react-intl'; +import InstalledOutgoingWebhook from './installed_outgoing_webhook.jsx'; +import InstalledIntegrations from './installed_integrations.jsx'; + +export default class InstalledOutgoingWebhooks extends React.Component { + constructor(props) { + super(props); + + this.handleIntegrationChange = this.handleIntegrationChange.bind(this); + + this.regenOutgoingWebhookToken = this.regenOutgoingWebhookToken.bind(this); + this.deleteOutgoingWebhook = this.deleteOutgoingWebhook.bind(this); + + this.state = { + outgoingWebhooks: [] + }; + } + + componentWillMount() { + IntegrationStore.addChangeListener(this.handleIntegrationChange); + + if (window.mm_config.EnableOutgoingWebhooks === 'true') { + if (IntegrationStore.hasReceivedOutgoingWebhooks()) { + this.setState({ + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } else { + AsyncClient.listOutgoingHooks(); + } + } + } + + componentWillUnmount() { + IntegrationStore.removeChangeListener(this.handleIntegrationChange); + } + + handleIntegrationChange() { + this.setState({ + outgoingWebhooks: IntegrationStore.getOutgoingWebhooks() + }); + } + + regenOutgoingWebhookToken(outgoingWebhook) { + AsyncClient.regenOutgoingHookToken(outgoingWebhook.id); + } + + deleteOutgoingWebhook(outgoingWebhook) { + AsyncClient.deleteOutgoingHook(outgoingWebhook.id); + } + + render() { + const outgoingWebhooks = this.state.outgoingWebhooks.map((outgoingWebhook) => { + return ( + <InstalledOutgoingWebhook + key={outgoingWebhook.id} + outgoingWebhook={outgoingWebhook} + onRegenToken={this.regenOutgoingWebhookToken} + onDelete={this.deleteOutgoingWebhook} + /> + ); + }); + + return ( + <InstalledIntegrations + header={ + <FormattedMessage + id='installed_outgoing_webhooks.header' + defaultMessage='Installed Outgoing Webhooks' + /> + } + addText={ + <FormattedMessage + id='installed_outgoing_webhooks.add' + defaultMessage='Add Outgoing Webhook' + /> + } + addLink='/settings/integrations/outgoing_webhooks/add' + > + {outgoingWebhooks} + </InstalledIntegrations> + ); + } +} diff --git a/webapp/components/backstage/add_integration_option.jsx b/webapp/components/backstage/integration_option.jsx index b17ebb185..dd7cc0c4c 100644 --- a/webapp/components/backstage/add_integration_option.jsx +++ b/webapp/components/backstage/integration_option.jsx @@ -5,7 +5,7 @@ import React from 'react'; import {Link} from 'react-router'; -export default class AddIntegrationOption extends React.Component { +export default class IntegrationOption extends React.Component { static get propTypes() { return { image: React.PropTypes.string.isRequired, @@ -21,16 +21,16 @@ export default class AddIntegrationOption extends React.Component { return ( <Link to={link} - className='add-integration' + className='integration-option' > <img - className='add-integration__image' + className='integration-option__image' src={image} /> - <div className='add-integration__title'> + <div className='integration-option__title'> {title} </div> - <div className='add-integration__description'> + <div className='integration-option__description'> {description} </div> </Link> diff --git a/webapp/components/backstage/add_integration.jsx b/webapp/components/backstage/integrations.jsx index 0ab36e101..71232ea45 100644 --- a/webapp/components/backstage/add_integration.jsx +++ b/webapp/components/backstage/integrations.jsx @@ -4,76 +4,76 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -import AddIntegrationOption from './add_integration_option.jsx'; +import IntegrationOption from './integration_option.jsx'; import WebhookIcon from 'images/webhook_icon.jpg'; -export default class AddIntegration extends React.Component { +export default class Integrations extends React.Component { render() { const options = []; if (window.mm_config.EnableIncomingWebhooks === 'true') { options.push( - <AddIntegrationOption + <IntegrationOption key='incomingWebhook' image={WebhookIcon} title={ <FormattedMessage - id='add_integration.incomingWebhook.title' + id='integrations.incomingWebhook.title' defaultMessage='Incoming Webhook' /> } description={ <FormattedMessage - id='add_integration.incomingWebhook.description' - defaultMessage='Create webhook URLs for use in external integrations.' + id='integrations.incomingWebhook.description' + defaultMessage='Incoming webhooks allow external integrations to send messages' /> } - link={'/settings/integrations/add/incoming_webhook'} + link={'/settings/integrations/incoming_webhooks'} /> ); } if (window.mm_config.EnableOutgoingWebhooks === 'true') { options.push( - <AddIntegrationOption + <IntegrationOption key='outgoingWebhook' image={WebhookIcon} title={ <FormattedMessage - id='add_integration.outgoingWebhook.title' + id='integrations.outgoingWebhook.title' defaultMessage='Outgoing Webhook' /> } description={ <FormattedMessage - id='add_integration.outgoingWebhook.description' - defaultMessage='Create webhooks to send new message events to an external integration.' + id='integrations.outgoingWebhook.description' + defaultMessage='Outgoing webhooks allow external integrations to receive and respond to messages' /> } - link={'/settings/integrations/add/outgoing_webhook'} + link={'/settings/integrations/outgoing_webhooks'} /> ); } if (window.mm_config.EnableCommands === 'true') { options.push( - <AddIntegrationOption + <IntegrationOption key='command' image={WebhookIcon} title={ <FormattedMessage - id='add_integration.command.title' + id='integrations.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.' + id='integrations.command.description' + defaultMessage='Slash commands send events to an external integration' /> } - link={'/settings/integrations/add/command'} + link={'/settings/integrations/commands'} /> ); } @@ -83,8 +83,8 @@ export default class AddIntegration extends React.Component { <div className='backstage-header'> <h1> <FormattedMessage - id='add_integration.header' - defaultMessage='Add Integration' + id='integrations.header' + defaultMessage='Integrations' /> </h1> </div> diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 12671284a..fd8f44c36 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -37,7 +37,7 @@ "add_command.autocompleteHint.placeholder": "Example: [Patient Name]", "add_command.description": "Description", "add_command.displayName": "Display Name", - "add_command.header": "Add Slash Command", + "add_command.header": "Add", "add_command.iconUrl": "Response Icon", "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.iconUrl.placeholder": "https://www.example.com/myicon.png", @@ -61,22 +61,15 @@ "add_incoming_webhook.channel": "Channel", "add_incoming_webhook.channelRequired": "A valid channel is required", "add_incoming_webhook.description": "Description", - "add_incoming_webhook.header": "Add Incoming Webhook", + "add_incoming_webhook.header": "Add", "add_incoming_webhook.name": "Name", "add_incoming_webhook.save": "Save", - "add_integration.command.description": "Create slash commands to send events to external integrations and receive a response.", - "add_integration.command.title": "Slash Command", - "add_integration.header": "Add Integration", - "add_integration.incomingWebhook.description": "Create webhook URLs for use in external integrations.", - "add_integration.incomingWebhook.title": "Incoming Webhook", - "add_integration.outgoingWebhook.description": "Create webhooks to send new message events to an external integration.", - "add_integration.outgoingWebhook.title": "Outgoing Webhook", "add_outgoing_webhook.callbackUrls": "Callback URLs (One Per Line)", "add_outgoing_webhook.callbackUrlsRequired": "One or more callback URLs are required", "add_outgoing_webhook.cancel": "Cancel", "add_outgoing_webhook.channel": "Channel", "add_outgoing_webhook.description": "Description", - "add_outgoing_webhook.header": "Add Outgoing Webhook", + "add_outgoing_webhook.header": "Add", "add_outgoing_webhook.name": "Name", "add_outgoing_webhook.save": "Save", "add_outgoing_webhook.triggerWOrds": "Trigger Words (One Per Line)", @@ -624,11 +617,9 @@ "authorize.title": "An application would like to connect to your {teamName} account", "backstage_navbar.backToMattermost": "Back to {siteName}", "backstage_sidebar.integrations": "Integrations", - "backstage_sidebar.integrations.add": "Add Integration", - "backstage_sidebar.integrations.add.command": "Slash Command", - "backstage_sidebar.integrations.add.incomingWebhook": "Incoming Webhook", - "backstage_sidebar.integrations.add.outgoingWebhook": "Outgoing Webhook", - "backstage_sidebar.integrations.installed": "Installed Integrations", + "backstage_sidebar.integrations.incoming_webhooks": "Incoming Webhooks", + "backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks", + "backstage_sidebar.integrations.commands": "Commands", "center_panel.recent": "Click here to jump to recent messages. ", "chanel_header.addMembers": "Add Members", "change_url.close": "Close", @@ -850,19 +841,24 @@ "get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it's regenerated in Team Settings by a Team Admin.", "get_team_invite_link_modal.helpDisabled": "User creation has been disabled for your team. Please ask your team administrator for details.", "get_team_invite_link_modal.title": "Team Invite Link", - "installed_integrations.add": "Add Integration", - "installed_integrations.allFilter": "All ({count})", - "installed_integrations.commandType": "(Slash Command)", - "installed_integrations.commandsFilter": "Slash Commands ({count})", + "installed_commands.add": "Add Slash Command", + "installed_commands.header": "Slash Commands", + "installed_incoming_webhooks.add": "Add Incoming Webhook", + "installed_incoming_webhooks.header": "Incoming Webhooks", "installed_integrations.creation": "Created by {creator} on {createAt, date, full}", "installed_integrations.delete": "Delete", - "installed_integrations.header": "Installed Integrations", - "installed_integrations.incomingWebhookType": "(Incoming Webhook)", - "installed_integrations.incomingWebhooksFilter": "Incoming Webhooks ({count})", - "installed_integrations.outgoingWebhookType": "(Outgoing Webhook)", - "installed_integrations.outgoingWebhooksFilter": "Outgoing Webhooks ({count})", "installed_integrations.regenToken": "Regen Token", "installed_integrations.search": "Search Integrations", + "installed_integrations.token": "Token: {token}", + "installed_outgoing_webhooks.add": "Add Outgoing Webhook", + "installed_outgoing_webhooks.header": "Outgoing Webhooks", + "integrations.command.description": "Slash commands send events to external integrations", + "integrations.command.title": "Slash Command", + "integrations.header": "Integrations", + "integrations.incomingWebhook.description": "Incoming webhooks allow external integrations to send messages", + "integrations.incomingWebhook.title": "Incoming Webhook", + "integrations.outgoingWebhook.description": "Outgoing webhooks allow external integrations to receive and respond to messages", + "integrations.outgoingWebhook.title": "Outgoing Webhook", "intro_messages.DM": "This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.", "intro_messages.anyMember": " Any member can join and read this channel.", "intro_messages.beginning": "Beginning of {name}", diff --git a/webapp/root.jsx b/webapp/root.jsx index a76f7cf7e..9268643f3 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -38,8 +38,10 @@ import AdminConsole from 'components/admin_console/admin_controller.jsx'; import TutorialView from 'components/tutorial/tutorial_view.jsx'; import BackstageNavbar from 'components/backstage/backstage_navbar.jsx'; import BackstageSidebar from 'components/backstage/backstage_sidebar.jsx'; -import InstalledIntegrations from 'components/backstage/installed_integrations.jsx'; -import AddIntegration from 'components/backstage/add_integration.jsx'; +import Integrations from 'components/backstage/integrations.jsx'; +import InstalledIncomingWebhooks from 'components/backstage/installed_incoming_webhooks.jsx'; +import InstalledOutgoingWebhooks from 'components/backstage/installed_outgoing_webhooks.jsx'; +import InstalledCommands from 'components/backstage/installed_commands.jsx'; import AddIncomingWebhook from 'components/backstage/add_incoming_webhook.jsx'; import AddOutgoingWebhook from 'components/backstage/add_outgoing_webhook.jsx'; import AddCommand from 'components/backstage/add_command.jsx'; @@ -253,41 +255,57 @@ function renderRootComponent() { onEnter={onLoggedOut} /> <Route path='settings/integrations'> - <IndexRedirect to='installed'/> - <Route - path='installed' + <IndexRoute components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, - center: InstalledIntegrations + center: Integrations }} /> - <Route path='add'> + <Route path='incoming_webhooks'> <IndexRoute components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, - center: AddIntegration + center: InstalledIncomingWebhooks }} /> <Route - path='incoming_webhook' + path='add' components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, center: AddIncomingWebhook }} /> + </Route> + <Route path='outgoing_webhooks'> + <IndexRoute + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: InstalledOutgoingWebhooks + }} + /> <Route - path='outgoing_webhook' + path='add' components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, center: AddOutgoingWebhook }} /> + </Route> + <Route path='commands'> + <IndexRoute + components={{ + navbar: BackstageNavbar, + sidebar: BackstageSidebar, + center: InstalledCommands + }} + /> <Route - path='command' + path='add' components={{ navbar: BackstageNavbar, sidebar: BackstageSidebar, diff --git a/webapp/sass/routes/_backstage.scss b/webapp/sass/routes/_backstage.scss index 4b0c07b7e..f6e0a8ac0 100644 --- a/webapp/sass/routes/_backstage.scss +++ b/webapp/sass/routes/_backstage.scss @@ -207,11 +207,12 @@ body { font-weight: 600; } - .item-details__type { + .item-details__trigger { margin-left: 6px; } .item-details__description, + .item-details__token, .item-details__creation { color: $dark-gray; display: inline-block; @@ -283,7 +284,7 @@ body { } } -.add-integration { +.integration-option { background-color: $white; border: 1px solid $light-gray; display: inline-block; @@ -300,16 +301,16 @@ body { } } -.add-integration__image { +.integration-option__image { height: 80px; width: 80px; } -.add-integration__title { +.integration-option__title { color: $black; margin-bottom: 10px; } -.add-integration__description { +.integration-option__description { color: $dark-gray; } |