diff options
author | Harrison Healey <harrisonmhealey@gmail.com> | 2016-07-05 11:58:18 -0400 |
---|---|---|
committer | Joram Wilander <jwawilander@gmail.com> | 2016-07-05 11:58:18 -0400 |
commit | dc2f2a800105b77e665ec2a00c6290f35b1a2ba3 (patch) | |
tree | 82f23c2e72a7c785f55c2d6c1c35c10c16994918 /webapp/components/backstage | |
parent | a65f1fc266f15eaa8f79541d4d11440c3d356bb6 (diff) | |
download | chat-dc2f2a800105b77e665ec2a00c6290f35b1a2ba3.tar.gz chat-dc2f2a800105b77e665ec2a00c6290f35b1a2ba3.tar.bz2 chat-dc2f2a800105b77e665ec2a00c6290f35b1a2ba3.zip |
PLT-3145 Custom Emojis (#3381)
* Reorganized Backstage code to use a view controller and separated it from integrations code
* Renamed InstalledIntegrations component to BackstageList
* Added EmojiList page
* Added AddEmoji page
* Added custom emoji to autocomplete and text formatter
* Moved system emoji to EmojiStore
* Stopped trying to get emoji before logging in
* Rerender posts when emojis change
* Fixed submit handler on backstage pages to properly support enter
* Removed debugging code
* Updated javascript driver
* Fixed unit tests
* Fixed backstage routes
* Added clientside validation to prevent users from creating an emoji with the same name as a system one
* Fixed AddEmoji page to properly redirect when an emoji is created successfully
* Fixed updating emoji list when an emoji is deleted
* Added type prop to BackstageList to properly support using a table for the list
* Added help text to EmojiList
* Fixed backstage on smaller screen sizes
* Disable custom emoji by default
* Improved restrictions on creating emojis
* Fixed non-admin users seeing the option to delete each other's emojis
* Fixing gofmt
* Fixed emoji unit tests
* Fixed trying to get emoji from the server when it's disabled
Diffstat (limited to 'webapp/components/backstage')
19 files changed, 248 insertions, 2163 deletions
diff --git a/webapp/components/backstage/add_command.jsx b/webapp/components/backstage/add_command.jsx deleted file mode 100644 index 91af0416b..000000000 --- a/webapp/components/backstage/add_command.jsx +++ /dev/null @@ -1,558 +0,0 @@ -// 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 * 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 {browserHistory, Link} from 'react-router/es6'; -import SpinnerButton from 'components/spinner_button.jsx'; -import Constants from 'utils/constants.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' - /> - ) - }); - - return; - } - - if (command.trigger.indexOf('/') === 0) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_command.triggerInvalidSlash' - defaultMessage='A trigger word cannot begin with a /' - /> - ) - }); - - return; - } - - if (command.trigger.indexOf(' ') !== -1) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_command.triggerInvalidSpace' - defaultMessage='A trigger word must not contain spaces' - /> - ) - }); - return; - } - - if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_command.triggerInvalidLength' - defaultMessage='A trigger word must contain between {min} and {max} characters' - values={{ - min: Constants.MIN_TRIGGER_LENGTH, - max: Constants.MAX_TRIGGER_LENGTH - }} - /> - ) - }); - - return; - } - - if (!command.url) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_command.urlRequired' - defaultMessage='A request URL is required' - /> - ) - }); - - return; - } - - AsyncClient.addCommand( - command, - () => { - browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'); - }, - (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-4' - htmlFor='autocompleteHint' - > - <FormattedMessage - id='add_command.autocompleteHint' - defaultMessage='Autocomplete Hint' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='autocompleteHint' - type='text' - maxLength='1024' - className='form-control' - value={this.state.autocompleteHint} - onChange={this.updateAutocompleteHint} - placeholder={Utils.localizeMessage('add_command.autocompleteHint.placeholder', 'Example: [Patient Name]')} - /> - <div className='form__help'> - <FormattedMessage - id='add_command.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-4' - htmlFor='autocompleteDescription' - > - <FormattedMessage - id='add_command.autocompleteDescription' - defaultMessage='Autocomplete Description' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='description' - type='text' - maxLength='128' - className='form-control' - value={this.state.autocompleteDescription} - onChange={this.updateAutocompleteDescription} - placeholder={Utils.localizeMessage('add_command.autocompleteDescription.placeholder', 'Example: "Returns search results for patient records"')} - /> - <div className='form__help'> - <FormattedMessage - id='add_command.autocompleteDescription.help' - defaultMessage='Optional short description of slash command for the autocomplete list.' - /> - </div> - </div> - </div> - )]; - } - - return ( - <div className='backstage-content row'> - <BackstageHeader> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/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'> - <label - className='control-label col-sm-4' - htmlFor='displayName' - > - <FormattedMessage - id='add_command.displayName' - defaultMessage='Display Name' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='displayName' - type='text' - maxLength='64' - className='form-control' - value={this.state.displayName} - onChange={this.updateDisplayName} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='description' - > - <FormattedMessage - id='add_command.description' - defaultMessage='Description' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='description' - type='text' - maxLength='128' - className='form-control' - value={this.state.description} - onChange={this.updateDescription} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='trigger' - > - <FormattedMessage - id='add_command.trigger' - defaultMessage='Command Trigger Word' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='trigger' - type='text' - maxLength={Constants.MAX_TRIGGER_LENGTH} - className='form-control' - value={this.state.trigger} - onChange={this.updateTrigger} - placeholder={Utils.localizeMessage('add_command.trigger.placeholder', 'Command trigger e.g. "hello" not including the slash')} - /> - <div className='form__help'> - <FormattedMessage - id='add_command.trigger.help1' - defaultMessage='Examples: /patient, /client /employee' - /> - </div> - <div className='form__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-4' - htmlFor='url' - > - <FormattedMessage - id='add_command.url' - defaultMessage='Request URL' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='url' - type='text' - maxLength='1024' - className='form-control' - value={this.state.url} - onChange={this.updateUrl} - placeholder={Utils.localizeMessage('add_command.url.placeholder', 'Must start with http:// or https://')} - /> - <div className='form__help'> - <FormattedMessage - id='add_command.url.help' - defaultMessage='The callback URL to receive the HTTP POST or GET event request when the slash command is run.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='method' - > - <FormattedMessage - id='add_command.method' - defaultMessage='Request Method' - /> - </label> - <div className='col-md-5 col-sm-8'> - <select - id='method' - className='form-control' - value={this.state.method} - onChange={this.updateMethod} - > - <option value={REQUEST_POST}> - {Utils.localizeMessage('add_command.method.post', 'POST')} - </option> - <option value={REQUEST_GET}> - {Utils.localizeMessage('add_command.method.get', 'GET')} - </option> - </select> - <div className='form__help'> - <FormattedMessage - id='add_command.method.help' - defaultMessage='The type of command request issued to the Request URL.' - /> - </div> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='username' - > - <FormattedMessage - id='add_command.username' - defaultMessage='Response Username' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='username' - type='text' - maxLength='64' - className='form-control' - value={this.state.username} - onChange={this.updateUsername} - placholder={Utils.localizeMessage('add_command.username.placeholder', 'Username')} - /> - <div className='form__help'> - <FormattedMessage - id='add_command.username.help' - defaultMessage='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-4' - htmlFor='iconUrl' - > - <FormattedMessage - id='add_command.iconUrl' - defaultMessage='Response Icon' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='iconUrl' - type='text' - maxLength='1024' - className='form-control' - value={this.state.iconUrl} - onChange={this.updateIconUrl} - placeholder={Utils.localizeMessage('add_command.iconUrl.placeholder', 'https://www.example.com/myicon.png')} - /> - <div className='form__help'> - <FormattedMessage - id='add_command.iconUrl.help' - defaultMessage='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 padding-bottom'> - <div className='col-sm-12'> - <div className='checkbox'> - <input - type='checkbox' - checked={this.state.autocomplete} - onChange={this.updateAutocomplete} - /> - <FormattedMessage - id='add_command.autocomplete' - defaultMessage='Autocomplete' - /> - </div> - <div className='form__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={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'} - > - <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_incoming_webhook.jsx b/webapp/components/backstage/add_incoming_webhook.jsx deleted file mode 100644 index 528f03377..000000000 --- a/webapp/components/backstage/add_incoming_webhook.jsx +++ /dev/null @@ -1,208 +0,0 @@ -// 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 * as Utils from 'utils/utils.jsx'; - -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'; -import {browserHistory, Link} from 'react-router/es6'; -import SpinnerButton from 'components/spinner_button.jsx'; - -export default class AddIncomingWebhook 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.updateChannelId = this.updateChannelId.bind(this); - - this.state = { - displayName: '', - description: '', - channelId: '', - saving: false, - serverError: '', - clientError: null - }; - } - - handleSubmit(e) { - e.preventDefault(); - - if (this.state.saving) { - return; - } - - this.setState({ - saving: true, - serverError: '', - clientError: '' - }); - - if (!this.state.channelId) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_incoming_webhook.channelRequired' - defaultMessage='A valid channel is required' - /> - ) - }); - - return; - } - - const hook = { - channel_id: this.state.channelId, - display_name: this.state.displayName, - description: this.state.description - }; - - AsyncClient.addIncomingHook( - hook, - () => { - browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'); - }, - (err) => { - this.setState({ - saving: false, - serverError: err.message - }); - } - ); - } - - updateDisplayName(e) { - this.setState({ - displayName: e.target.value - }); - } - - updateDescription(e) { - this.setState({ - description: e.target.value - }); - } - - updateChannelId(e) { - this.setState({ - channelId: e.target.value - }); - } - - render() { - return ( - <div className='backstage-content'> - <BackstageHeader> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/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'> - <label - className='control-label col-sm-4' - htmlFor='displayName' - > - <FormattedMessage - id='add_incoming_webhook.displayName' - defaultMessage='Display Name' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='displayName' - type='text' - maxLength='64' - className='form-control' - value={this.state.displayName} - onChange={this.updateDisplayName} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='description' - > - <FormattedMessage - id='add_incoming_webhook.description' - defaultMessage='Description' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='description' - type='text' - maxLength='128' - className='form-control' - value={this.state.description} - onChange={this.updateDescription} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='channelId' - > - <FormattedMessage - id='add_incoming_webhook.channel' - defaultMessage='Channel' - /> - </label> - <div className='col-md-5 col-sm-8'> - <ChannelSelect - id='channelId' - value={this.state.channelId} - onChange={this.updateChannelId} - selectOpen={true} - selectPrivate={true} - /> - </div> - </div> - <div className='backstage-form__footer'> - <FormError errors={[this.state.serverError, this.state.clientError]}/> - <Link - className='btn btn-sm' - to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'} - > - <FormattedMessage - id='add_incoming_webhook.cancel' - defaultMessage='Cancel' - /> - </Link> - <SpinnerButton - className='btn btn-primary' - type='submit' - spinning={this.state.saving} - onClick={this.handleSubmit} - > - <FormattedMessage - id='add_incoming_webhook.save' - defaultMessage='Save' - /> - </SpinnerButton> - </div> - </form> - </div> - </div> - ); - } -} diff --git a/webapp/components/backstage/add_outgoing_webhook.jsx b/webapp/components/backstage/add_outgoing_webhook.jsx deleted file mode 100644 index 5f9d96249..000000000 --- a/webapp/components/backstage/add_outgoing_webhook.jsx +++ /dev/null @@ -1,341 +0,0 @@ -// 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 * as Utils from 'utils/utils.jsx'; - -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'; -import {browserHistory, Link} from 'react-router/es6'; -import SpinnerButton from 'components/spinner_button.jsx'; - -export default class AddOutgoingWebhook 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.updateContentType = this.updateContentType.bind(this); - this.updateChannelId = this.updateChannelId.bind(this); - this.updateTriggerWords = this.updateTriggerWords.bind(this); - this.updateCallbackUrls = this.updateCallbackUrls.bind(this); - - this.state = { - displayName: '', - description: '', - contentType: 'application/x-www-form-urlencoded', - channelId: '', - triggerWords: '', - callbackUrls: '', - saving: false, - serverError: '', - clientError: null - }; - } - - handleSubmit(e) { - e.preventDefault(); - - if (this.state.saving) { - return; - } - - this.setState({ - saving: true, - serverError: '', - clientError: '' - }); - - const triggerWords = []; - if (this.state.triggerWords) { - for (let triggerWord of this.state.triggerWords.split('\n')) { - triggerWord = triggerWord.trim(); - - if (triggerWord.length > 0) { - triggerWords.push(triggerWord); - } - } - } - - if (!this.state.channelId && triggerWords.length === 0) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_outgoing_webhook.triggerWordsOrChannelRequired' - defaultMessage='A valid channel or a list of trigger words is required' - /> - ) - }); - - return; - } - - const callbackUrls = []; - for (let callbackUrl of this.state.callbackUrls.split('\n')) { - callbackUrl = callbackUrl.trim(); - - if (callbackUrl.length > 0) { - callbackUrls.push(callbackUrl); - } - } - - if (callbackUrls.length === 0) { - this.setState({ - saving: false, - clientError: ( - <FormattedMessage - id='add_outgoing_webhook.callbackUrlsRequired' - defaultMessage='One or more callback URLs are required' - /> - ) - }); - - return; - } - - const hook = { - channel_id: this.state.channelId, - trigger_words: triggerWords, - callback_urls: callbackUrls, - display_name: this.state.displayName, - content_type: this.state.contentType, - description: this.state.description - }; - - AsyncClient.addOutgoingHook( - hook, - () => { - browserHistory.push('/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'); - }, - (err) => { - this.setState({ - saving: false, - serverError: err.message - }); - } - ); - } - - updateDisplayName(e) { - this.setState({ - displayName: e.target.value - }); - } - - updateDescription(e) { - this.setState({ - description: e.target.value - }); - } - - updateContentType(e) { - this.setState({ - contentType: e.target.value - }); - } - - updateChannelId(e) { - this.setState({ - channelId: e.target.value - }); - } - - updateTriggerWords(e) { - this.setState({ - triggerWords: e.target.value - }); - } - - updateCallbackUrls(e) { - this.setState({ - callbackUrls: e.target.value - }); - } - - render() { - const contentTypeOption1 = 'application/x-www-form-urlencoded'; - const contentTypeOption2 = 'application/json'; - return ( - <div className='backstage-content'> - <BackstageHeader> - <Link to={'/' + Utils.getTeamNameFromUrl() + '/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'> - <label - className='control-label col-sm-4' - htmlFor='displayName' - > - <FormattedMessage - id='add_outgoing_webhook.displayName' - defaultMessage='Display Name' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='displayName' - type='text' - maxLength='64' - className='form-control' - value={this.state.displayName} - onChange={this.updateDisplayName} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='description' - > - <FormattedMessage - id='add_outgoing_webhook.description' - defaultMessage='Description' - /> - </label> - <div className='col-md-5 col-sm-8'> - <input - id='description' - type='text' - maxLength='128' - className='form-control' - value={this.state.description} - onChange={this.updateDescription} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='contentType' - > - <FormattedMessage - id='add_outgoing_webhook.content_Type' - defaultMessage='Content Type' - /> - </label> - <div className='col-md-5 col-sm-8'> - <select - className='form-control' - value={this.state.contentType} - onChange={this.updateContentType} - > - <option - value={contentTypeOption1} - > - {contentTypeOption1} - </option> - <option - value={contentTypeOption2} - > - {contentTypeOption2} - </option> - </select> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='channelId' - > - <FormattedMessage - id='add_outgoing_webhook.channel' - defaultMessage='Channel' - /> - </label> - <div className='col-md-5 col-sm-8'> - <ChannelSelect - id='channelId' - value={this.state.channelId} - onChange={this.updateChannelId} - selectOpen={true} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='triggerWords' - > - <FormattedMessage - id='add_outgoing_webhook.triggerWords' - defaultMessage='Trigger Words (One Per Line)' - /> - </label> - <div className='col-md-5 col-sm-8'> - <textarea - id='triggerWords' - rows='3' - maxLength='1000' - className='form-control' - value={this.state.triggerWords} - onChange={this.updateTriggerWords} - /> - </div> - </div> - <div className='form-group'> - <label - className='control-label col-sm-4' - htmlFor='callbackUrls' - > - <FormattedMessage - id='add_outgoing_webhook.callbackUrls' - defaultMessage='Callback URLs (One Per Line)' - /> - </label> - <div className='col-md-5 col-sm-8'> - <textarea - id='callbackUrls' - rows='3' - maxLength='1000' - className='form-control' - value={this.state.callbackUrls} - onChange={this.updateCallbackUrls} - /> - </div> - </div> - <div className='backstage-form__footer'> - <FormError errors={[this.state.serverError, this.state.clientError]}/> - <Link - className='btn btn-sm' - to={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'} - > - <FormattedMessage - id='add_outgoing_webhook.cancel' - defaultMessage='Cancel' - /> - </Link> - <SpinnerButton - className='btn btn-primary' - type='submit' - spinning={this.state.saving} - onClick={this.handleSubmit} - > - <FormattedMessage - id='add_outgoing_webhook.save' - defaultMessage='Save' - /> - </SpinnerButton> - </div> - </form> - </div> - </div> - ); - } -} diff --git a/webapp/components/backstage/backstage_controller.jsx b/webapp/components/backstage/backstage_controller.jsx new file mode 100644 index 000000000..690880071 --- /dev/null +++ b/webapp/components/backstage/backstage_controller.jsx @@ -0,0 +1,71 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import TeamStore from 'stores/team_store.jsx'; + +import BackstageSidebar from './components/backstage_sidebar.jsx'; +import BackstageNavbar from './components/backstage_navbar.jsx'; +import ErrorBar from 'components/error_bar.jsx'; + +export default class BackstageController extends React.Component { + static get propTypes() { + return { + children: React.PropTypes.node.isRequired, + params: React.PropTypes.object.isRequired, + user: React.PropTypes.user.isRequired + }; + } + + constructor(props) { + super(props); + + this.onTeamChange = this.onTeamChange.bind(this); + + this.state = { + team: props.params.team ? TeamStore.getByName(props.params.team) : TeamStore.getCurrent() + }; + } + + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + } + + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + + onTeamChange() { + this.state = { + team: this.props.params.team ? TeamStore.getByName(this.props.params.team) : TeamStore.getCurrent() + }; + } + + render() { + return ( + <div className='backstage'> + <ErrorBar/> + <BackstageNavbar team={this.state.team}/> + <div className='backstage-body'> + <BackstageSidebar + team={this.state.team} + user={this.props.user} + /> + { + React.Children.map(this.props.children, (child) => { + if (!child) { + return child; + } + + return React.cloneElement(child, { + team: this.state.team, + user: this.props.user + }); + }) + } + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/webapp/components/backstage/backstage_category.jsx b/webapp/components/backstage/components/backstage_category.jsx index 1d4b11ca3..74dcf3476 100644 --- a/webapp/components/backstage/backstage_category.jsx +++ b/webapp/components/backstage/components/backstage_category.jsx @@ -59,6 +59,7 @@ export default class BackstageCategory extends React.Component { to={link} className='category-title' activeClassName='category-title--active' + onlyActiveOnIndex={true} > <i className={'fa ' + icon}/> <span className='category-title__text'> diff --git a/webapp/components/backstage/backstage_header.jsx b/webapp/components/backstage/components/backstage_header.jsx index 37b4be349..37b4be349 100644 --- a/webapp/components/backstage/backstage_header.jsx +++ b/webapp/components/backstage/components/backstage_header.jsx diff --git a/webapp/components/backstage/components/backstage_list.jsx b/webapp/components/backstage/components/backstage_list.jsx new file mode 100644 index 000000000..81b8ec4d9 --- /dev/null +++ b/webapp/components/backstage/components/backstage_list.jsx @@ -0,0 +1,108 @@ +// 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 {Link} from 'react-router'; +import LoadingScreen from 'components/loading_screen.jsx'; + +export default class BackstageList extends React.Component { + static propTypes = { + children: React.PropTypes.node, + header: React.PropTypes.node.isRequired, + addLink: React.PropTypes.string, + addText: React.PropTypes.node, + emptyText: React.PropTypes.node, + loading: React.PropTypes.bool.isRequired, + searchPlaceholder: React.PropTypes.string + } + + static defaultProps = { + searchPlaceholder: Utils.localizeMessage('backstage.search', 'Search') + } + + constructor(props) { + super(props); + + this.updateFilter = this.updateFilter.bind(this); + + this.state = { + filter: '' + }; + } + + updateFilter(e) { + this.setState({ + filter: e.target.value + }); + } + + render() { + const filter = this.state.filter.toLowerCase(); + + let children; + if (this.props.loading) { + children = <LoadingScreen/>; + } else { + children = React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, {filter}); + }); + + if (children.length === 0 && this.props.emptyText) { + children = ( + <span className='backstage-list__item backstage-list__empty'> + {this.props.emptyText} + </span> + ); + } + } + + let addLink = null; + if (this.props.addLink && this.props.addText) { + addLink = ( + <Link + className='add-link' + to={this.props.addLink} + > + <button + type='button' + className='btn btn-primary' + > + <span> + {this.props.addText} + </span> + </button> + </Link> + ); + } + + return ( + <div className='backstage-content'> + <div className='backstage-header'> + <h1> + {this.props.header} + </h1> + {addLink} + </div> + <div className='backstage-filters'> + <div className='backstage-filter__search'> + <i className='fa fa-search'></i> + <input + type='search' + className='form-control' + placeholder={this.props.searchPlaceholder} + value={this.state.filter} + onChange={this.updateFilter} + style={{flexGrow: 0, flexShrink: 0}} + /> + </div> + </div> + <div className='backstage-list'> + {children} + </div> + </div> + ); + } +} diff --git a/webapp/components/backstage/backstage_navbar.jsx b/webapp/components/backstage/components/backstage_navbar.jsx index 26ab44c87..7bccfc9f7 100644 --- a/webapp/components/backstage/backstage_navbar.jsx +++ b/webapp/components/backstage/components/backstage_navbar.jsx @@ -1,52 +1,28 @@ // Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import $ from 'jquery'; - import React from 'react'; -import TeamStore from 'stores/team_store.jsx'; - import {FormattedMessage} from 'react-intl'; import {Link} from 'react-router/es6'; export default class BackstageNavbar extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - - this.state = { - team: TeamStore.getCurrent() + static get propTypes() { + return { + team: React.propTypes.object.isRequired }; } - componentDidMount() { - TeamStore.addChangeListener(this.handleChange); - $('body').addClass('backstage'); - } - - componentWillUnmount() { - TeamStore.removeChangeListener(this.handleChange); - $('body').removeClass('backstage'); - } - - handleChange() { - this.setState({ - team: TeamStore.getCurrent() - }); - } - render() { - if (!this.state.team) { + if (!this.props.team) { return null; } return ( - <div className='backstage-navbar row'> + <div className='backstage-navbar'> <Link className='backstage-navbar__back' - to={`/${this.state.team.name}/channels/town-square`} + to={`/${this.props.team.name}/channels/town-square`} > <i className='fa fa-angle-left'/> <span> diff --git a/webapp/components/backstage/backstage_section.jsx b/webapp/components/backstage/components/backstage_section.jsx index c8b63af18..c8b63af18 100644 --- a/webapp/components/backstage/backstage_section.jsx +++ b/webapp/components/backstage/components/backstage_section.jsx diff --git a/webapp/components/backstage/backstage_sidebar.jsx b/webapp/components/backstage/components/backstage_sidebar.jsx index 4d8d8337d..a17d830b0 100644 --- a/webapp/components/backstage/backstage_sidebar.jsx +++ b/webapp/components/backstage/components/backstage_sidebar.jsx @@ -3,13 +3,51 @@ import React from 'react'; -import * as Utils from 'utils/utils.jsx'; +import TeamStore from 'stores/team_store.jsx'; + import BackstageCategory from './backstage_category.jsx'; import BackstageSection from './backstage_section.jsx'; import {FormattedMessage} from 'react-intl'; export default class BackstageSidebar extends React.Component { - render() { + static get propTypes() { + return { + team: React.PropTypes.object.isRequired, + user: React.PropTypes.object.isRequired + }; + } + + renderCustomEmoji() { + if (window.mm_config.EnableCustomEmoji !== 'true') { + return null; + } + + return ( + <BackstageCategory + name='emoji' + parentLink={'/' + this.props.team.name} + icon='fa-smile-o' + title={ + <FormattedMessage + id='backstage_sidebar.emoji' + defaultMessage='Custom Emoji' + /> + } + /> + ); + } + + renderIntegrations() { + if (window.mm_config.EnableIncomingWebhooks !== 'true' && + window.mm_config.EnableOutgoingWebhooks !== 'true' && + window.mm_config.EnableCommands !== 'true') { + return null; + } + + if (window.mm_config.RestrictCustomEmojiCreation !== 'all' && !TeamStore.isTeamAdmin(this.props.user.id, this.props.team.id)) { + return null; + } + let incomingWebhooks = null; if (window.mm_config.EnableIncomingWebhooks === 'true') { incomingWebhooks = ( @@ -56,23 +94,30 @@ export default class BackstageSidebar extends React.Component { } return ( + <BackstageCategory + name='integrations' + parentLink={'/' + this.props.team.name} + icon='fa-link' + title={ + <FormattedMessage + id='backstage_sidebar.integrations' + defaultMessage='Integrations' + /> + } + > + {incomingWebhooks} + {outgoingWebhooks} + {commands} + </BackstageCategory> + ); + } + + render() { + return ( <div className='backstage-sidebar'> <ul> - <BackstageCategory - name='integrations' - parentLink={'/' + Utils.getTeamNameFromUrl() + '/settings'} - icon='fa-link' - title={ - <FormattedMessage - id='backstage_sidebar.integrations' - defaultMessage='Integrations' - /> - } - > - {incomingWebhooks} - {outgoingWebhooks} - {commands} - </BackstageCategory> + {this.renderCustomEmoji()} + {this.renderIntegrations()} </ul> </div> ); diff --git a/webapp/components/backstage/installed_command.jsx b/webapp/components/backstage/installed_command.jsx deleted file mode 100644 index 88f43f674..000000000 --- a/webapp/components/backstage/installed_command.jsx +++ /dev/null @@ -1,146 +0,0 @@ -// 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, - filter: React.PropTypes.string - }; - } - - constructor(props) { - super(props); - - this.handleRegenToken = this.handleRegenToken.bind(this); - this.handleDelete = this.handleDelete.bind(this); - - this.matchesFilter = this.matchesFilter.bind(this); - } - - handleRegenToken(e) { - e.preventDefault(); - - this.props.onRegenToken(this.props.command); - } - - handleDelete(e) { - e.preventDefault(); - - 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> - ); - } - - let trigger = '- /' + command.trigger; - if (command.auto_complete && command.auto_complete_hint) { - trigger += ' ' + command.auto_complete_hint; - } - - return ( - <div className='backstage-list__item'> - <div className='item-details'> - <div className='item-details__row'> - <span className='item-details__name'> - {name} - </span> - <span className='item-details__trigger'> - {trigger} - </span> - </div> - {description} - <div className='item-details__row'> - <span className='item-details__token'> - <FormattedMessage - id='installed_integrations.token' - defaultMessage='Token: {token}' - values={{ - token: command.token - }} - /> - </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='Regenerate Token' - /> - </a> - {' - '} - <a - href='#' - onClick={this.handleDelete} - > - <FormattedMessage - id='installed_integrations.delete' - defaultMessage='Delete' - /> - </a> - </div> - </div> - ); - } -} diff --git a/webapp/components/backstage/installed_commands.jsx b/webapp/components/backstage/installed_commands.jsx deleted file mode 100644 index df1f56687..000000000 --- a/webapp/components/backstage/installed_commands.jsx +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import IntegrationStore from 'stores/integration_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import {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); - - const teamId = TeamStore.getCurrentId(); - - this.state = { - commands: IntegrationStore.getCommands(teamId), - loading: !IntegrationStore.hasReceivedCommands(teamId) - }; - } - - componentDidMount() { - IntegrationStore.addChangeListener(this.handleIntegrationChange); - - if (window.mm_config.EnableCommands === 'true') { - AsyncClient.listTeamCommands(); - } - } - - componentWillUnmount() { - IntegrationStore.removeChangeListener(this.handleIntegrationChange); - } - - handleIntegrationChange() { - const teamId = TeamStore.getCurrentId(); - - this.setState({ - commands: IntegrationStore.getCommands(teamId), - loading: !IntegrationStore.hasReceivedCommands(teamId) - }); - } - - 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_commands.header' - defaultMessage='Installed Slash Commands' - /> - } - addText={ - <FormattedMessage - id='installed_commands.add' - defaultMessage='Add Slash Command' - /> - } - addLink={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands/add'} - emptyText={ - <FormattedMessage - id='installed_commands.empty' - defaultMessage='No slash commands found' - /> - } - loading={this.state.loading} - > - {commands} - </InstalledIntegrations> - ); - } -} diff --git a/webapp/components/backstage/installed_incoming_webhook.jsx b/webapp/components/backstage/installed_incoming_webhook.jsx deleted file mode 100644 index afa6e9958..000000000 --- a/webapp/components/backstage/installed_incoming_webhook.jsx +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import ChannelStore from 'stores/channel_store.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage} from 'react-intl'; - -export default class InstalledIncomingWebhook extends React.Component { - static get propTypes() { - return { - incomingWebhook: React.PropTypes.object.isRequired, - onDelete: React.PropTypes.func.isRequired, - filter: React.PropTypes.string - }; - } - - constructor(props) { - super(props); - - this.handleDelete = this.handleDelete.bind(this); - } - - handleDelete(e) { - e.preventDefault(); - - 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); - - 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'> - {displayName} - </span> - </div> - {description} - <div className='item-details__row'> - <span className='item-details__url'> - <FormattedMessage - id='installed_integrations.url' - defaultMessage='URL: {url}' - values={{ - url: Utils.getWindowLocationOrigin() + '/hooks/' + incomingWebhook.id - }} - /> - </span> - </div> - <div className='tem-details__row'> - <span className='item-details__creation'> - <FormattedMessage - id='installed_integrations.creation' - defaultMessage='Created by {creator} on {createAt, date, full}' - values={{ - creator: Utils.displayUsername(incomingWebhook.user_id), - createAt: incomingWebhook.create_at - }} - /> - </span> - </div> - </div> - <div className='item-actions'> - <a - href='#' - onClick={this.handleDelete} - > - <FormattedMessage - id='installed_integrations.delete' - defaultMessage='Delete' - /> - </a> - </div> - </div> - ); - } -} diff --git a/webapp/components/backstage/installed_incoming_webhooks.jsx b/webapp/components/backstage/installed_incoming_webhooks.jsx deleted file mode 100644 index 0a38a6ab5..000000000 --- a/webapp/components/backstage/installed_incoming_webhooks.jsx +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import IntegrationStore from 'stores/integration_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import {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); - - const teamId = TeamStore.getCurrentId(); - - this.state = { - incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) - }; - } - - componentDidMount() { - IntegrationStore.addChangeListener(this.handleIntegrationChange); - - if (window.mm_config.EnableIncomingWebhooks === 'true') { - AsyncClient.listIncomingHooks(); - } - } - - componentWillUnmount() { - IntegrationStore.removeChangeListener(this.handleIntegrationChange); - } - - handleIntegrationChange() { - const teamId = TeamStore.getCurrentId(); - - this.setState({ - incomingWebhooks: IntegrationStore.getIncomingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedIncomingWebhooks(teamId) - }); - } - - 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={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks/add'} - emptyText={ - <FormattedMessage - id='installed_incoming_webhooks.empty' - defaultMessage='No incoming webhooks found' - /> - } - loading={this.state.loading} - > - {incomingWebhooks} - </InstalledIntegrations> - ); - } -} diff --git a/webapp/components/backstage/installed_integrations.jsx b/webapp/components/backstage/installed_integrations.jsx deleted file mode 100644 index f6de8bc11..000000000 --- a/webapp/components/backstage/installed_integrations.jsx +++ /dev/null @@ -1,101 +0,0 @@ -// 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 {Link} from 'react-router/es6'; -import LoadingScreen from 'components/loading_screen.jsx'; - -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, - emptyText: React.PropTypes.node.isRequired, - loading: React.PropTypes.bool.isRequired - }; - } - - constructor(props) { - super(props); - - this.updateFilter = this.updateFilter.bind(this); - - this.state = { - filter: '' - }; - } - - updateFilter(e) { - this.setState({ - filter: e.target.value - }); - } - - render() { - const filter = this.state.filter.toLowerCase(); - - let children; - - if (this.props.loading) { - children = <LoadingScreen/>; - } else { - children = React.Children.map(this.props.children, (child) => { - return React.cloneElement(child, {filter}); - }); - - if (children.length === 0) { - children = ( - <span className='backstage-list__item backstage-list_empty'> - {this.props.emptyText} - </span> - ); - } - } - - return ( - <div className='backstage-content'> - <div className='installed-integrations'> - <div className='backstage-header'> - <h1> - {this.props.header} - </h1> - <Link - className='add-integrations-link' - to={this.props.addLink} - > - <button - type='button' - className='btn btn-primary' - > - <span> - {this.props.addText} - </span> - </button> - </Link> - </div> - <div className='backstage-filters'> - <div className='backstage-filter__search'> - <i className='fa fa-search'></i> - <input - type='search' - className='form-control' - placeholder={Utils.localizeMessage('installed_integrations.search', 'Search Integrations')} - value={this.state.filter} - onChange={this.updateFilter} - style={{flexGrow: 0, flexShrink: 0}} - /> - </div> - </div> - <div className='backstage-list'> - {children} - </div> - </div> - </div> - ); - } -} diff --git a/webapp/components/backstage/installed_outgoing_webhook.jsx b/webapp/components/backstage/installed_outgoing_webhook.jsx deleted file mode 100644 index 99f2439ec..000000000 --- a/webapp/components/backstage/installed_outgoing_webhook.jsx +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import ChannelStore from 'stores/channel_store.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import {FormattedMessage} from 'react-intl'; - -export default class InstalledOutgoingWebhook extends React.Component { - static get propTypes() { - return { - outgoingWebhook: React.PropTypes.object.isRequired, - onRegenToken: React.PropTypes.func.isRequired, - onDelete: React.PropTypes.func.isRequired, - filter: React.PropTypes.string - }; - } - - 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.outgoingWebhook); - } - - handleDelete(e) { - e.preventDefault(); - - 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); - - 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> - ); - } - - let triggerWords = null; - if (outgoingWebhook.trigger_words && outgoingWebhook.trigger_words.length > 0) { - triggerWords = ( - <div className='item-details__row'> - <span className='item-details__trigger-words'> - <FormattedMessage - id='installed_integrations.triggerWords' - defaultMessage='Trigger Words: {triggerWords}' - values={{ - triggerWords: outgoingWebhook.trigger_words.join(', ') - }} - /> - </span> - </div> - ); - } - - let urls = ( - <div className='item-details__row'> - <span className='item-details__url'> - <FormattedMessage - id='installed_integrations.callback_urls' - defaultMessage='Callback URLs: {urls}' - values={{ - urls: outgoingWebhook.callback_urls.join(', ') - }} - /> - </span> - </div> - ); - - return ( - <div className='backstage-list__item'> - <div className='item-details'> - <div className='item-details__row'> - <span className='item-details__name'> - {displayName} - </span> - </div> - {description} - <div className='item-details__row'> - <span className='item-details__content_type'> - <FormattedMessage - id='installed_integrations.content_type' - defaultMessage='Content-Type: {contentType}' - values={{ - contentType: outgoingWebhook.content_type || 'application/x-www-form-urlencoded' - }} - /> - </span> - </div> - {triggerWords} - <div className='item-details__row'> - <span className='item-details__token'> - <FormattedMessage - id='installed_integrations.token' - defaultMessage='Token: {token}' - values={{ - token: outgoingWebhook.token - }} - /> - </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(outgoingWebhook.creator_id), - createAt: outgoingWebhook.create_at - }} - /> - </span> - </div> - {urls} - </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_outgoing_webhooks.jsx b/webapp/components/backstage/installed_outgoing_webhooks.jsx deleted file mode 100644 index b79bc3530..000000000 --- a/webapp/components/backstage/installed_outgoing_webhooks.jsx +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import * as AsyncClient from 'utils/async_client.jsx'; -import IntegrationStore from 'stores/integration_store.jsx'; -import TeamStore from 'stores/team_store.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import {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); - - const teamId = TeamStore.getCurrentId(); - - this.state = { - outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId) - }; - } - - componentDidMount() { - IntegrationStore.addChangeListener(this.handleIntegrationChange); - - if (window.mm_config.EnableOutgoingWebhooks === 'true') { - AsyncClient.listOutgoingHooks(); - } - } - - componentWillUnmount() { - IntegrationStore.removeChangeListener(this.handleIntegrationChange); - } - - handleIntegrationChange() { - const teamId = TeamStore.getCurrentId(); - - this.setState({ - outgoingWebhooks: IntegrationStore.getOutgoingWebhooks(teamId), - loading: !IntegrationStore.hasReceivedOutgoingWebhooks(teamId) - }); - } - - 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={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks/add'} - emptyText={ - <FormattedMessage - id='installed_outgoing_webhooks.empty' - defaultMessage='No outgoing webhooks found' - /> - } - loading={this.state.loading} - > - {outgoingWebhooks} - </InstalledIntegrations> - ); - } -} diff --git a/webapp/components/backstage/integration_option.jsx b/webapp/components/backstage/integration_option.jsx deleted file mode 100644 index 483e6a888..000000000 --- a/webapp/components/backstage/integration_option.jsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import {Link} from 'react-router/es6'; - -export default class IntegrationOption extends React.Component { - static get propTypes() { - return { - image: React.PropTypes.string.isRequired, - title: React.PropTypes.node.isRequired, - description: React.PropTypes.node.isRequired, - link: React.PropTypes.string.isRequired - }; - } - - render() { - const {image, title, description, link} = this.props; - - return ( - <Link - to={link} - className='integration-option' - > - <img - className='integration-option__image' - src={image} - /> - <div className='integration-option__title'> - {title} - </div> - <div className='integration-option__description'> - {description} - </div> - </Link> - ); - } -} diff --git a/webapp/components/backstage/integrations.jsx b/webapp/components/backstage/integrations.jsx deleted file mode 100644 index fdd75026a..000000000 --- a/webapp/components/backstage/integrations.jsx +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; - -import {FormattedMessage} from 'react-intl'; -import IntegrationOption from './integration_option.jsx'; -import * as Utils from 'utils/utils.jsx'; - -import WebhookIcon from 'images/webhook_icon.jpg'; - -export default class Integrations extends React.Component { - render() { - const options = []; - - if (window.mm_config.EnableIncomingWebhooks === 'true') { - options.push( - <IntegrationOption - key='incomingWebhook' - image={WebhookIcon} - title={ - <FormattedMessage - id='integrations.incomingWebhook.title' - defaultMessage='Incoming Webhook' - /> - } - description={ - <FormattedMessage - id='integrations.incomingWebhook.description' - defaultMessage='Incoming webhooks allow external integrations to send messages' - /> - } - link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/incoming_webhooks'} - /> - ); - } - - if (window.mm_config.EnableOutgoingWebhooks === 'true') { - options.push( - <IntegrationOption - key='outgoingWebhook' - image={WebhookIcon} - title={ - <FormattedMessage - id='integrations.outgoingWebhook.title' - defaultMessage='Outgoing Webhook' - /> - } - description={ - <FormattedMessage - id='integrations.outgoingWebhook.description' - defaultMessage='Outgoing webhooks allow external integrations to receive and respond to messages' - /> - } - link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/outgoing_webhooks'} - /> - ); - } - - if (window.mm_config.EnableCommands === 'true') { - options.push( - <IntegrationOption - key='command' - image={WebhookIcon} - title={ - <FormattedMessage - id='integrations.command.title' - defaultMessage='Slash Command' - /> - } - description={ - <FormattedMessage - id='integrations.command.description' - defaultMessage='Slash commands send events to an external integration' - /> - } - link={'/' + Utils.getTeamNameFromUrl() + '/settings/integrations/commands'} - /> - ); - } - - return ( - <div className='backstage-content row'> - <div className='backstage-header'> - <h1> - <FormattedMessage - id='integrations.header' - defaultMessage='Integrations' - /> - </h1> - </div> - <div> - {options} - </div> - </div> - ); - } -} - |