From dc2f2a800105b77e665ec2a00c6290f35b1a2ba3 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 5 Jul 2016 11:58:18 -0400 Subject: 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 --- .../integrations/components/add_command.jsx | 567 +++++++++++++++++++++ .../components/add_incoming_webhook.jsx | 216 ++++++++ .../components/add_outgoing_webhook.jsx | 349 +++++++++++++ .../integrations/components/installed_command.jsx | 147 ++++++ .../integrations/components/installed_commands.jsx | 107 ++++ .../components/installed_incoming_webhook.jsx | 132 +++++ .../components/installed_incoming_webhooks.jsx | 101 ++++ .../components/installed_outgoing_webhook.jsx | 200 ++++++++ .../components/installed_outgoing_webhooks.jsx | 107 ++++ .../integrations/components/integration_option.jsx | 39 ++ .../integrations/components/integrations.jsx | 104 ++++ 11 files changed, 2069 insertions(+) create mode 100644 webapp/components/integrations/components/add_command.jsx create mode 100644 webapp/components/integrations/components/add_incoming_webhook.jsx create mode 100644 webapp/components/integrations/components/add_outgoing_webhook.jsx create mode 100644 webapp/components/integrations/components/installed_command.jsx create mode 100644 webapp/components/integrations/components/installed_commands.jsx create mode 100644 webapp/components/integrations/components/installed_incoming_webhook.jsx create mode 100644 webapp/components/integrations/components/installed_incoming_webhooks.jsx create mode 100644 webapp/components/integrations/components/installed_outgoing_webhook.jsx create mode 100644 webapp/components/integrations/components/installed_outgoing_webhooks.jsx create mode 100644 webapp/components/integrations/components/integration_option.jsx create mode 100644 webapp/components/integrations/components/integrations.jsx (limited to 'webapp/components/integrations') diff --git a/webapp/components/integrations/components/add_command.jsx b/webapp/components/integrations/components/add_command.jsx new file mode 100644 index 000000000..e72670e47 --- /dev/null +++ b/webapp/components/integrations/components/add_command.jsx @@ -0,0 +1,567 @@ +// 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 'components/backstage/components/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 { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + 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: ( + + ) + }); + + return; + } + + if (command.trigger.indexOf('/') === 0) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + if (command.trigger.indexOf(' ') !== -1) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + return; + } + + if (command.trigger.length < Constants.MIN_TRIGGER_LENGTH || command.trigger.length > Constants.MAX_TRIGGER_LENGTH) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + if (!command.url) { + this.setState({ + saving: false, + clientError: ( + + ) + }); + + return; + } + + AsyncClient.addCommand( + command, + () => { + browserHistory.push('/' + this.props.team.name + '/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 = [( +
+ +
+ +
+ +
+
+
+ ), + ( +
+ +
+ +
+ +
+
+
+ )]; + } + + return ( +
+ + + + + + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+ {autocompleteFields} +
+ + + + + + + +
+
+
+
+ ); + } +} diff --git a/webapp/components/integrations/components/add_incoming_webhook.jsx b/webapp/components/integrations/components/add_incoming_webhook.jsx new file mode 100644 index 000000000..122600c90 --- /dev/null +++ b/webapp/components/integrations/components/add_incoming_webhook.jsx @@ -0,0 +1,216 @@ +// 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 BackstageHeader from 'components/backstage/components/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 { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + 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: ( + + ) + }); + + return; + } + + const hook = { + channel_id: this.state.channelId, + display_name: this.state.displayName, + description: this.state.description + }; + + AsyncClient.addIncomingHook( + hook, + () => { + browserHistory.push('/' + this.props.team.name + '/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 ( +
+ + + + + + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + + + + + +
+
+
+
+ ); + } +} diff --git a/webapp/components/integrations/components/add_outgoing_webhook.jsx b/webapp/components/integrations/components/add_outgoing_webhook.jsx new file mode 100644 index 000000000..bd49fedc9 --- /dev/null +++ b/webapp/components/integrations/components/add_outgoing_webhook.jsx @@ -0,0 +1,349 @@ +// 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 BackstageHeader from 'components/backstage/components/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 { + static get propTypes() { + return { + team: React.propTypes.object.isRequired + }; + } + + 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: ( + + ) + }); + + 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: ( + + ) + }); + + 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('/' + this.props.team.name + '/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 ( +
+ + + + + + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+