From 98186e5018bbc604796d4f9762c93f4f75e2913f Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Mon, 21 Sep 2015 14:22:23 -0400 Subject: Implement incoming webhooks. --- .../user_settings/manage_incoming_hooks.jsx | 177 ++++++ .../components/user_settings/user_settings.jsx | 112 ++++ .../user_settings/user_settings_appearance.jsx | 181 ++++++ .../user_settings/user_settings_developer.jsx | 93 +++ .../user_settings/user_settings_general.jsx | 562 +++++++++++++++++ .../user_settings/user_settings_integrations.jsx | 95 +++ .../user_settings/user_settings_modal.jsx | 96 +++ .../user_settings/user_settings_notifications.jsx | 697 +++++++++++++++++++++ .../user_settings/user_settings_security.jsx | 300 +++++++++ 9 files changed, 2313 insertions(+) create mode 100644 web/react/components/user_settings/manage_incoming_hooks.jsx create mode 100644 web/react/components/user_settings/user_settings.jsx create mode 100644 web/react/components/user_settings/user_settings_appearance.jsx create mode 100644 web/react/components/user_settings/user_settings_developer.jsx create mode 100644 web/react/components/user_settings/user_settings_general.jsx create mode 100644 web/react/components/user_settings/user_settings_integrations.jsx create mode 100644 web/react/components/user_settings/user_settings_modal.jsx create mode 100644 web/react/components/user_settings/user_settings_notifications.jsx create mode 100644 web/react/components/user_settings/user_settings_security.jsx (limited to 'web/react/components/user_settings') diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx new file mode 100644 index 000000000..df089a403 --- /dev/null +++ b/web/react/components/user_settings/manage_incoming_hooks.jsx @@ -0,0 +1,177 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var Client = require('../../utils/client.jsx'); +var Utils = require('../../utils/utils.jsx'); +var Constants = require('../../utils/constants.jsx'); +var ChannelStore = require('../../stores/channel_store.jsx'); +var LoadingScreen = require('../loading_screen.jsx'); + +export default class ManageIncomingHooks extends React.Component { + constructor() { + super(); + + this.getHooks = this.getHooks.bind(this); + this.addNewHook = this.addNewHook.bind(this); + this.updateChannelId = this.updateChannelId.bind(this); + + this.state = {hooks: [], channelId: ChannelStore.getByName(Constants.DEFAULT_CHANNEL).id, getHooksComplete: false}; + } + componentDidMount() { + this.getHooks(); + } + addNewHook() { + let hook = {}; //eslint-disable-line prefer-const + hook.channel_id = this.state.channelId; + + Client.addIncomingHook( + hook, + (data) => { + let hooks = this.state.hooks; + if (!hooks) { + hooks = []; + } + hooks.push(data); + this.setState({hooks}); + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + removeHook(id) { + let data = {}; //eslint-disable-line prefer-const + data.id = id; + + Client.deleteIncomingHook( + data, + () => { + let hooks = this.state.hooks; //eslint-disable-line prefer-const + let index = -1; + for (let i = 0; i < hooks.length; i++) { + if (hooks[i].id === id) { + index = i; + break; + } + } + + if (index !== -1) { + hooks.splice(index, 1); + } + + this.setState({hooks}); + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + getHooks() { + Client.listIncomingHooks( + (data) => { + let state = this.state; //eslint-disable-line prefer-const + + if (data) { + state.hooks = data; + } + + state.getHooksComplete = true; + this.setState(state); + }, + (err) => { + this.setState({serverError: err}); + } + ); + } + updateChannelId(e) { + this.setState({channelId: e.target.value}); + } + render() { + let serverError; + if (this.state.serverError) { + serverError = ; + } + + const channels = ChannelStore.getAll(); + let options = []; //eslint-disable-line prefer-const + channels.forEach((channel) => { + options.push(); + }); + + let disableButton = ''; + if (this.state.channelId === '') { + disableButton = ' disable'; + } + + let hooks = []; //eslint-disable-line prefer-const + this.state.hooks.forEach((hook) => { + const c = ChannelStore.get(hook.channel_id); + hooks.push( +
+
+ + {'URL: '}{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id} + +
+ + {'Channel: '}{c.name} + +
+ + {'Remove'} + +
+ ); + }); + + let displayHooks; + if (!this.state.getHooksComplete) { + displayHooks = ; + } else if (hooks.length > 0) { + displayHooks = hooks; + } else { + displayHooks = ; + } + + const existingHooks = ( +
+ +
+ {displayHooks} +
+ ); + + return ( +
+ +
+
+ +
+ {serverError} + + {'Add'} + +
+ {existingHooks} +
+ ); + } +} diff --git a/web/react/components/user_settings/user_settings.jsx b/web/react/components/user_settings/user_settings.jsx new file mode 100644 index 000000000..0eab333c4 --- /dev/null +++ b/web/react/components/user_settings/user_settings.jsx @@ -0,0 +1,112 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var UserStore = require('../../stores/user_store.jsx'); +var utils = require('../../utils/utils.jsx'); +var NotificationsTab = require('./user_settings_notifications.jsx'); +var SecurityTab = require('./user_settings_security.jsx'); +var GeneralTab = require('./user_settings_general.jsx'); +var AppearanceTab = require('./user_settings_appearance.jsx'); +var DeveloperTab = require('./user_settings_developer.jsx'); +var IntegrationsTab = require('./user_settings_integrations.jsx'); + +export default class UserSettings extends React.Component { + constructor(props) { + super(props); + + this.onListenerChange = this.onListenerChange.bind(this); + + this.state = {user: UserStore.getCurrentUser()}; + } + + componentDidMount() { + UserStore.addChangeListener(this.onListenerChange); + } + + componentWillUnmount() { + UserStore.removeChangeListener(this.onListenerChange); + } + + onListenerChange() { + var user = UserStore.getCurrentUser(); + if (!utils.areStatesEqual(this.state.user, user)) { + this.setState({user: user}); + } + } + + render() { + if (this.props.activeTab === 'general') { + return ( +
+ +
+ ); + } else if (this.props.activeTab === 'security') { + return ( +
+ +
+ ); + } else if (this.props.activeTab === 'notifications') { + return ( +
+ +
+ ); + } else if (this.props.activeTab === 'appearance') { + return ( +
+ +
+ ); + } else if (this.props.activeTab === 'developer') { + return ( +
+ +
+ ); + } else if (this.props.activeTab === 'integrations') { + return ( +
+ +
+ ); + } + + return
; + } +} + +UserSettings.propTypes = { + activeTab: React.PropTypes.string, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx new file mode 100644 index 000000000..aec3b319d --- /dev/null +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -0,0 +1,181 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var UserStore = require('../../stores/user_store.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var Client = require('../../utils/client.jsx'); +var Utils = require('../../utils/utils.jsx'); + +var ThemeColors = ['#2389d7', '#008a17', '#dc4fad', '#ac193d', '#0072c6', '#d24726', '#ff8f32', '#82ba00', '#03b3b2', '#008299', '#4617b4', '#8c0095', '#004b8b', '#004b8b', '#570000', '#380000', '#585858', '#000000']; + +export default class UserSettingsAppearance extends React.Component { + constructor(props) { + super(props); + + this.submitTheme = this.submitTheme.bind(this); + this.updateTheme = this.updateTheme.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = this.getStateFromStores(); + } + getStateFromStores() { + var user = UserStore.getCurrentUser(); + var theme = '#2389d7'; + if (ThemeColors != null) { + theme = ThemeColors[0]; + } + if (user.props && user.props.theme) { + theme = user.props.theme; + } + + return {theme: theme.toLowerCase()}; + } + submitTheme(e) { + e.preventDefault(); + var user = UserStore.getCurrentUser(); + if (!user.props) { + user.props = {}; + } + user.props.theme = this.state.theme; + + Client.updateUser(user, + function success() { + this.props.updateSection(''); + window.location.reload(); + }.bind(this), + function fail(err) { + var state = this.getStateFromStores(); + state.serverError = err; + this.setState(state); + }.bind(this) + ); + } + updateTheme(e) { + var hex = Utils.rgb2hex(e.target.style.backgroundColor); + this.setState({theme: hex.toLowerCase()}); + } + handleClose() { + this.setState({serverError: null}); + this.props.updateTab('general'); + } + componentDidMount() { + if (this.props.activeSection === 'theme') { + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); + } + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentDidUpdate() { + if (this.props.activeSection === 'theme') { + $('.color-btn').removeClass('active-border'); + $(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); + } + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); + } + render() { + var serverError; + if (this.state.serverError) { + serverError = this.state.serverError; + } + + var themeSection; + var self = this; + + if (ThemeColors != null) { + if (this.props.activeSection === 'theme') { + var themeButtons = []; + + for (var i = 0; i < ThemeColors.length; i++) { + themeButtons.push( + +

+ Appearance Settings +

+
+
+

Appearance Settings

+
+ {themeSection} +
+
+
+ ); + } +} + +UserSettingsAppearance.defaultProps = { + activeSection: '' +}; +UserSettingsAppearance.propTypes = { + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx new file mode 100644 index 000000000..1694aaa79 --- /dev/null +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -0,0 +1,93 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); + +export default class DeveloperTab extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + register() { + $('#user_settings1').modal('hide'); + $('#register_app').modal('show'); + } + render() { + var appSection; + var self = this; + if (this.props.activeSection === 'app') { + var inputs = []; + + inputs.push( +
+
+ + {'Register New Application'} + +
+
+ ); + + appSection = ( + + ); + } else { + appSection = ( + + ); + } + + return ( +
+
+ +

+ {'Developer Settings'} +

+
+
+

{'Developer Settings'}

+
+ {appSection} +
+
+
+ ); + } +} + +DeveloperTab.defaultProps = { + activeSection: '' +}; +DeveloperTab.propTypes = { + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func +}; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx new file mode 100644 index 000000000..5d9d9bfde --- /dev/null +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -0,0 +1,562 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var UserStore = require('../../stores/user_store.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var SettingPicture = require('../setting_picture.jsx'); +var client = require('../../utils/client.jsx'); +var AsyncClient = require('../../utils/async_client.jsx'); +var utils = require('../../utils/utils.jsx'); +var assign = require('object-assign'); + +export default class UserSettingsGeneralTab extends React.Component { + constructor(props) { + super(props); + this.submitActive = false; + + this.submitUsername = this.submitUsername.bind(this); + this.submitNickname = this.submitNickname.bind(this); + this.submitName = this.submitName.bind(this); + this.submitEmail = this.submitEmail.bind(this); + this.submitUser = this.submitUser.bind(this); + this.submitPicture = this.submitPicture.bind(this); + + this.updateUsername = this.updateUsername.bind(this); + this.updateFirstName = this.updateFirstName.bind(this); + this.updateLastName = this.updateLastName.bind(this); + this.updateNickname = this.updateNickname.bind(this); + this.updateEmail = this.updateEmail.bind(this); + this.updatePicture = this.updatePicture.bind(this); + this.updateSection = this.updateSection.bind(this); + + this.handleClose = this.handleClose.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(props); + } + submitUsername(e) { + e.preventDefault(); + + var user = this.props.user; + var username = this.state.username.trim().toLowerCase(); + + var usernameError = utils.isValidUsername(username); + if (usernameError === 'Cannot use a reserved word as a username.') { + this.setState({clientError: 'This username is reserved, please choose a new one.'}); + return; + } else if (usernameError) { + this.setState({clientError: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'."}); + return; + } + + if (user.username === username) { + this.setState({clientError: 'You must submit a new username'}); + return; + } + + user.username = username; + + this.submitUser(user); + } + submitNickname(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var nickname = this.state.nickname.trim(); + + if (user.nickname === nickname) { + this.setState({clientError: 'You must submit a new nickname'}); + return; + } + + user.nickname = nickname; + + this.submitUser(user); + } + submitName(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var firstName = this.state.firstName.trim(); + var lastName = this.state.lastName.trim(); + + if (user.first_name === firstName && user.last_name === lastName) { + this.setState({clientError: 'You must submit a new first or last name'}); + return; + } + + user.first_name = firstName; + user.last_name = lastName; + + this.submitUser(user); + } + submitEmail(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var email = this.state.email.trim().toLowerCase(); + + if (user.email === email) { + return; + } + + if (email === '' || !utils.isEmail(email)) { + this.setState({emailError: 'Please enter a valid email address'}); + return; + } + + user.email = email; + + this.submitUser(user); + } + submitUser(user) { + client.updateUser(user, + function updateSuccess() { + this.updateSection(''); + AsyncClient.getMe(); + }.bind(this), + function updateFailure(err) { + var state = this.setupInitialState(this.props); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + this.setState(state); + }.bind(this) + ); + } + submitPicture(e) { + e.preventDefault(); + + if (!this.state.picture) { + return; + } + + if (!this.submitActive) { + return; + } + + var picture = this.state.picture; + + if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') { + this.setState({clientError: 'Only JPG or PNG images may be used for profile pictures'}); + return; + } + + var formData = new FormData(); + formData.append('image', picture, picture.name); + this.setState({loadingPicture: true}); + + client.uploadProfileImage(formData, + function imageUploadSuccess() { + this.submitActive = false; + AsyncClient.getMe(); + window.location.reload(); + }.bind(this), + function imageUploadFailure(err) { + var state = this.setupInitialState(this.props); + state.serverError = err; + this.setState(state); + }.bind(this) + ); + } + updateUsername(e) { + this.setState({username: e.target.value}); + } + updateFirstName(e) { + this.setState({firstName: e.target.value}); + } + updateLastName(e) { + this.setState({lastName: e.target.value}); + } + updateNickname(e) { + this.setState({nickname: e.target.value}); + } + updateEmail(e) { + this.setState({email: e.target.value}); + } + updatePicture(e) { + if (e.target.files && e.target.files[0]) { + this.setState({picture: e.target.files[0]}); + + this.submitActive = true; + this.setState({clientError: null}); + } else { + this.setState({picture: null}); + } + } + updateSection(section) { + this.setState(assign({}, this.setupInitialState(this.props), {clientError: '', serverError: '', emailError: ''})); + this.submitActive = false; + this.props.updateSection(section); + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function clearForms() { + this.value = ''; + }); + + this.setState(assign({}, this.setupInitialState(this.props), {clientError: null, serverError: null, emailError: null})); + this.props.updateSection(''); + } + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + setupInitialState(props) { + var user = props.user; + var emailEnabled = !global.window.config.ByPassEmail; + return {username: user.username, firstName: user.first_name, lastName: user.last_name, nickname: user.nickname, + email: user.email, picture: null, loadingPicture: false, emailEnabled: emailEnabled}; + } + render() { + var user = this.props.user; + + var clientError = null; + if (this.state.clientError) { + clientError = this.state.clientError; + } + var serverError = null; + if (this.state.serverError) { + serverError = this.state.serverError; + } + var emailError = null; + if (this.state.emailError) { + emailError = this.state.emailError; + } + + var nameSection; + var inputs = []; + + if (this.props.activeSection === 'name') { + inputs.push( +
+ +
+ +
+
+ ); + + inputs.push( +
+ +
+ +
+
+ ); + + function notifClick(e) { + e.preventDefault(); + this.updateSection(''); + this.props.updateTab('notifications'); + } + + const notifLink = ( + + {'Notifications'} + + ); + + const extraInfo = ( + + {'By default, you will receive mention notifications when someone types your first name. '} + {'Go to '} {notifLink} {'settings to change this default.'} + + ); + + nameSection = ( + + ); + } else { + var fullName = ''; + + if (user.first_name && user.last_name) { + fullName = user.first_name + ' ' + user.last_name; + } else if (user.first_name) { + fullName = user.first_name; + } else if (user.last_name) { + fullName = user.last_name; + } + + nameSection = ( + + ); + } + + var nicknameSection; + if (this.props.activeSection === 'nickname') { + let nicknameLabel = 'Nickname'; + if (utils.isMobile()) { + nicknameLabel = ''; + } + + inputs.push( +
+ +
+ +
+
+ ); + + const extraInfo = ( + + {'Use Nickname for a name you might be called that is different from your first name and user name.'} + {'This is most often used when two or more people have similar sounding names and usernames.'} + + ); + + nicknameSection = ( + + ); + } else { + nicknameSection = ( + + ); + } + + var usernameSection; + if (this.props.activeSection === 'username') { + let usernameLabel = 'Username'; + if (utils.isMobile()) { + usernameLabel = ''; + } + + inputs.push( +
+ +
+ +
+
+ ); + + const extraInfo = ({'Pick something easy for teammates to recognize and recall.'}); + + usernameSection = ( + + ); + } else { + usernameSection = ( + + ); + } + var emailSection; + if (this.props.activeSection === 'email') { + let helpText =
Email is used for notifications, and requires verification if changed.
; + + if (!this.state.emailEnabled) { + helpText =
{'Email has been disabled by your system administrator. No notification emails will be sent until it is enabled.'}
; + } + + inputs.push( +
+
+ +
+ +
+
+ {helpText} +
+ ); + + emailSection = ( + + ); + } else { + emailSection = ( + + ); + } + + var pictureSection; + if (this.props.activeSection === 'picture') { + pictureSection = ( + + ); + } else { + var minMessage = 'Click \'Edit\' to upload an image.'; + if (user.last_picture_update) { + minMessage = 'Image last updated ' + utils.displayDate(user.last_picture_update); + } + pictureSection = ( + + ); + } + return ( +
+
+ +

+ + {'General Settings'} +

+
+
+

{'General Settings'}

+
+ {nameSection} +
+ {usernameSection} +
+ {nicknameSection} +
+ {emailSection} +
+ {pictureSection} +
+
+
+ ); + } +} + +UserSettingsGeneralTab.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx new file mode 100644 index 000000000..cb45c5178 --- /dev/null +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -0,0 +1,95 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var ManageIncomingHooks = require('./manage_incoming_hooks.jsx'); + +export default class UserSettingsIntegrationsTab extends React.Component { + constructor(props) { + super(props); + + this.updateSection = this.updateSection.bind(this); + this.handleClose = this.handleClose.bind(this); + + this.state = {}; + } + updateSection(section) { + this.props.updateSection(section); + } + handleClose() { + this.updateSection(''); + } + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + } + render() { + let incomingHooksSection; + var inputs = []; + + if (this.props.activeSection === 'incoming-hooks') { + inputs.push( + + ); + + incomingHooksSection = ( + + ); + } else { + incomingHooksSection = ( + + ); + } + + return ( +
+
+ +

+ + {'Integration Settings'} +

+
+
+

{'Integration Settings'}

+
+ {incomingHooksSection} +
+
+
+ ); + } +} + +UserSettingsIntegrationsTab.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string +}; diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx new file mode 100644 index 000000000..1b22e6045 --- /dev/null +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -0,0 +1,96 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingsSidebar = require('../settings_sidebar.jsx'); +var UserSettings = require('./user_settings.jsx'); + +export default class UserSettingsModal extends React.Component { + constructor(props) { + super(props); + + this.updateTab = this.updateTab.bind(this); + this.updateSection = this.updateSection.bind(this); + + this.state = {active_tab: 'general', active_section: ''}; + } + componentDidMount() { + $('body').on('click', '.modal-back', function changeDisplay() { + $(this).closest('.modal-dialog').removeClass('display--content'); + }); + $('body').on('click', '.modal-header .close', () => { + setTimeout(() => { + $('.modal-dialog.display--content').removeClass('display--content'); + }, 500); + }); + } + updateTab(tab) { + this.setState({active_tab: tab}); + } + updateSection(section) { + this.setState({active_section: section}); + } + render() { + var tabs = []; + tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'}); + tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'}); + tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'}); + tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'}); + if (global.window.config.EnableOAuthServiceProvider === 'true') { + tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); + } + if (global.window.config.AllowIncomingWebhooks === 'true') { + tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); + } + + return ( + + ); + } +} diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx new file mode 100644 index 000000000..fde4970ce --- /dev/null +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -0,0 +1,697 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var UserStore = require('../../stores/user_store.jsx'); +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var client = require('../../utils/client.jsx'); +var AsyncClient = require('../../utils/async_client.jsx'); +var utils = require('../../utils/utils.jsx'); +var assign = require('object-assign'); + +function getNotificationsStateFromStores() { + var user = UserStore.getCurrentUser(); + var soundNeeded = !utils.isBrowserFirefox(); + + var sound = 'true'; + if (user.notify_props && user.notify_props.desktop_sound) { + sound = user.notify_props.desktop_sound; + } + var desktop = 'all'; + if (user.notify_props && user.notify_props.desktop) { + desktop = user.notify_props.desktop; + } + var email = 'true'; + if (user.notify_props && user.notify_props.email) { + email = user.notify_props.email; + } + + var usernameKey = false; + var mentionKey = false; + var customKeys = ''; + var firstNameKey = false; + var allKey = false; + var channelKey = false; + + if (user.notify_props) { + if (user.notify_props.mention_keys) { + var keys = user.notify_props.mention_keys.split(','); + + if (keys.indexOf(user.username) !== -1) { + usernameKey = true; + keys.splice(keys.indexOf(user.username), 1); + } else { + usernameKey = false; + } + + if (keys.indexOf('@' + user.username) !== -1) { + mentionKey = true; + keys.splice(keys.indexOf('@' + user.username), 1); + } else { + mentionKey = false; + } + + customKeys = keys.join(','); + } + + if (user.notify_props.first_name) { + firstNameKey = user.notify_props.first_name === 'true'; + } + + if (user.notify_props.all) { + allKey = user.notify_props.all === 'true'; + } + + if (user.notify_props.channel) { + channelKey = user.notify_props.channel === 'true'; + } + } + + return {notifyLevel: desktop, enableEmail: email, soundNeeded: soundNeeded, enableSound: sound, + usernameKey: usernameKey, mentionKey: mentionKey, customKeys: customKeys, customKeysChecked: customKeys.length > 0, + firstNameKey: firstNameKey, allKey: allKey, channelKey: channelKey}; +} + +export default class NotificationsTab extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleClose = this.handleClose.bind(this); + this.updateSection = this.updateSection.bind(this); + this.onListenerChange = this.onListenerChange.bind(this); + this.handleNotifyRadio = this.handleNotifyRadio.bind(this); + this.handleEmailRadio = this.handleEmailRadio.bind(this); + this.handleSoundRadio = this.handleSoundRadio.bind(this); + this.updateUsernameKey = this.updateUsernameKey.bind(this); + this.updateMentionKey = this.updateMentionKey.bind(this); + this.updateFirstNameKey = this.updateFirstNameKey.bind(this); + this.updateAllKey = this.updateAllKey.bind(this); + this.updateChannelKey = this.updateChannelKey.bind(this); + this.updateCustomMentionKeys = this.updateCustomMentionKeys.bind(this); + this.onCustomChange = this.onCustomChange.bind(this); + + this.state = getNotificationsStateFromStores(); + } + handleSubmit() { + var data = {}; + data.user_id = this.props.user.id; + data.email = this.state.enableEmail; + data.desktop_sound = this.state.enableSound; + data.desktop = this.state.notifyLevel; + + var mentionKeys = []; + if (this.state.usernameKey) { + mentionKeys.push(this.props.user.username); + } + if (this.state.mentionKey) { + mentionKeys.push('@' + this.props.user.username); + } + + var stringKeys = mentionKeys.join(','); + if (this.state.customKeys.length > 0 && this.state.customKeysChecked) { + stringKeys += ',' + this.state.customKeys; + } + + data.mention_keys = stringKeys; + data.first_name = this.state.firstNameKey.toString(); + data.all = this.state.allKey.toString(); + data.channel = this.state.channelKey.toString(); + + client.updateUserNotifyProps(data, + function success() { + this.props.updateSection(''); + AsyncClient.getMe(); + }.bind(this), + function failure(err) { + this.setState({serverError: err.message}); + }.bind(this) + ); + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function clearField() { + this.value = ''; + }); + + this.setState(assign({}, getNotificationsStateFromStores(), {serverError: null})); + + this.props.updateTab('general'); + } + updateSection(section) { + this.setState(getNotificationsStateFromStores()); + this.props.updateSection(section); + } + componentDidMount() { + UserStore.addChangeListener(this.onListenerChange); + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + UserStore.removeChangeListener(this.onListenerChange); + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); + } + onListenerChange() { + var newState = getNotificationsStateFromStores(); + if (!utils.areStatesEqual(newState, this.state)) { + this.setState(newState); + } + } + handleNotifyRadio(notifyLevel) { + this.setState({notifyLevel: notifyLevel}); + React.findDOMNode(this.refs.wrapper).focus(); + } + handleEmailRadio(enableEmail) { + this.setState({enableEmail: enableEmail}); + React.findDOMNode(this.refs.wrapper).focus(); + } + handleSoundRadio(enableSound) { + this.setState({enableSound: enableSound}); + React.findDOMNode(this.refs.wrapper).focus(); + } + updateUsernameKey(val) { + this.setState({usernameKey: val}); + } + updateMentionKey(val) { + this.setState({mentionKey: val}); + } + updateFirstNameKey(val) { + this.setState({firstNameKey: val}); + } + updateAllKey(val) { + this.setState({allKey: val}); + } + updateChannelKey(val) { + this.setState({channelKey: val}); + } + updateCustomMentionKeys() { + var checked = React.findDOMNode(this.refs.customcheck).checked; + + if (checked) { + var text = React.findDOMNode(this.refs.custommentions).value; + + // remove all spaces and split string into individual keys + this.setState({customKeys: text.replace(/ /g, ''), customKeysChecked: true}); + } else { + this.setState({customKeys: '', customKeysChecked: false}); + } + } + onCustomChange() { + React.findDOMNode(this.refs.customcheck).checked = true; + this.updateCustomMentionKeys(); + } + render() { + var serverError = null; + if (this.state.serverError) { + serverError = this.state.serverError; + } + + var user = this.props.user; + + var desktopSection; + var handleUpdateDesktopSection; + if (this.props.activeSection === 'desktop') { + var notifyActive = [false, false, false]; + if (this.state.notifyLevel === 'mention') { + notifyActive[1] = true; + } else if (this.state.notifyLevel === 'none') { + notifyActive[2] = true; + } else { + notifyActive[0] = true; + } + + let inputs = []; + + inputs.push( +
+
+ +
+
+
+ +
+
+
+ +
+
+ ); + + handleUpdateDesktopSection = function updateDesktopSection(e) { + this.props.updateSection(''); + e.preventDefault(); + }.bind(this); + + desktopSection = ( + + ); + } else { + let describe = ''; + if (this.state.notifyLevel === 'mention') { + describe = 'Only for mentions and private messages'; + } else if (this.state.notifyLevel === 'none') { + describe = 'Never'; + } else { + describe = 'For all activity'; + } + + handleUpdateDesktopSection = function updateDesktopSection() { + this.props.updateSection('desktop'); + }.bind(this); + + desktopSection = ( + + ); + } + + var soundSection; + var handleUpdateSoundSection; + if (this.props.activeSection === 'sound' && this.state.soundNeeded) { + var soundActive = [false, false]; + if (this.state.enableSound === 'false') { + soundActive[1] = true; + } else { + soundActive[0] = true; + } + + let inputs = []; + + inputs.push( +
+
+ +
+
+
+ +
+
+
+ ); + + handleUpdateSoundSection = function updateSoundSection(e) { + this.props.updateSection(''); + e.preventDefault(); + }.bind(this); + + soundSection = ( + + ); + } else { + let describe = ''; + if (!this.state.soundNeeded) { + describe = 'Please configure notification sounds in your browser settings'; + } else if (this.state.enableSound === 'false') { + describe = 'Off'; + } else { + describe = 'On'; + } + + handleUpdateSoundSection = function updateSoundSection() { + this.props.updateSection('sound'); + }.bind(this); + + soundSection = ( + + ); + } + + var emailSection; + var handleUpdateEmailSection; + if (this.props.activeSection === 'email') { + var emailActive = [false, false]; + if (this.state.enableEmail === 'false') { + emailActive[1] = true; + } else { + emailActive[0] = true; + } + + let inputs = []; + + inputs.push( +
+
+ +
+
+
+ +
+
+

{'Email notifications are sent for mentions and private messages after you have been away from ' + global.window.config.SiteName + ' for 5 minutes.'}
+
+ ); + + handleUpdateEmailSection = function updateEmailSection(e) { + this.props.updateSection(''); + e.preventDefault(); + }.bind(this); + + emailSection = ( + + ); + } else { + let describe = ''; + if (this.state.enableEmail === 'false') { + describe = 'Off'; + } else { + describe = 'On'; + } + + handleUpdateEmailSection = function updateEmailSection() { + this.props.updateSection('email'); + }.bind(this); + + emailSection = ( + + ); + } + + var keysSection; + var handleUpdateKeysSection; + if (this.props.activeSection === 'keys') { + let inputs = []; + + let handleUpdateFirstNameKey; + let handleUpdateUsernameKey; + let handleUpdateMentionKey; + let handleUpdateAllKey; + let handleUpdateChannelKey; + + if (user.first_name) { + handleUpdateFirstNameKey = function handleFirstNameKeyChange(e) { + this.updateFirstNameKey(e.target.checked); + }.bind(this); + inputs.push( +
+
+ +
+
+ ); + } + + handleUpdateUsernameKey = function handleUsernameKeyChange(e) { + this.updateUsernameKey(e.target.checked); + }.bind(this); + inputs.push( +
+
+ +
+
+ ); + + handleUpdateMentionKey = function handleMentionKeyChange(e) { + this.updateMentionKey(e.target.checked); + }.bind(this); + inputs.push( +
+
+ +
+
+ ); + + handleUpdateAllKey = function handleAllKeyChange(e) { + this.updateAllKey(e.target.checked); + }.bind(this); + inputs.push( +
+
+ +
+
+ ); + + handleUpdateChannelKey = function handleChannelKeyChange(e) { + this.updateChannelKey(e.target.checked); + }.bind(this); + inputs.push( +
+
+ +
+
+ ); + + inputs.push( +
+
+ +
+ +
+ ); + + handleUpdateKeysSection = function updateKeysSection(e) { + this.props.updateSection(''); + e.preventDefault(); + }.bind(this); + keysSection = ( + + ); + } else { + let keys = []; + if (this.state.firstNameKey) { + keys.push(user.first_name); + } + if (this.state.usernameKey) { + keys.push(user.username); + } + if (this.state.mentionKey) { + keys.push('@' + user.username); + } + if (this.state.allKey) { + keys.push('@all'); + } + if (this.state.channelKey) { + keys.push('@channel'); + } + if (this.state.customKeys.length > 0) { + keys = keys.concat(this.state.customKeys.split(',')); + } + + let describe = ''; + for (var i = 0; i < keys.length; i++) { + describe += '"' + keys[i] + '", '; + } + + if (describe.length > 0) { + describe = describe.substring(0, describe.length - 2); + } else { + describe = 'No words configured'; + } + + handleUpdateKeysSection = function updateKeysSection() { + this.props.updateSection('keys'); + }.bind(this); + + keysSection = ( + + ); + } + + return ( +
+
+ +

+ + Notifications +

+
+
+

Notifications

+
+ {desktopSection} +
+ {soundSection} +
+ {emailSection} +
+ {keysSection} +
+
+
+ + ); + } +} + +NotificationsTab.defaultProps = { + user: null, + activeSection: '', + activeTab: '' +}; +NotificationsTab.propTypes = { + user: React.PropTypes.object, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func, + activeSection: React.PropTypes.string, + activeTab: React.PropTypes.string +}; diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx new file mode 100644 index 000000000..b59c08af0 --- /dev/null +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -0,0 +1,300 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SettingItemMin = require('../setting_item_min.jsx'); +var SettingItemMax = require('../setting_item_max.jsx'); +var Client = require('../../utils/client.jsx'); +var AsyncClient = require('../../utils/async_client.jsx'); +var Constants = require('../../utils/constants.jsx'); + +export default class SecurityTab extends React.Component { + constructor(props) { + super(props); + + this.submitPassword = this.submitPassword.bind(this); + this.updateCurrentPassword = this.updateCurrentPassword.bind(this); + this.updateNewPassword = this.updateNewPassword.bind(this); + this.updateConfirmPassword = this.updateConfirmPassword.bind(this); + this.handleClose = this.handleClose.bind(this); + this.setupInitialState = this.setupInitialState.bind(this); + + this.state = this.setupInitialState(); + } + submitPassword(e) { + e.preventDefault(); + + var user = this.props.user; + var currentPassword = this.state.currentPassword; + var newPassword = this.state.newPassword; + var confirmPassword = this.state.confirmPassword; + + if (currentPassword === '') { + this.setState({passwordError: 'Please enter your current password', serverError: ''}); + return; + } + + if (newPassword.length < 5) { + this.setState({passwordError: 'New passwords must be at least 5 characters', serverError: ''}); + return; + } + + if (newPassword !== confirmPassword) { + this.setState({passwordError: 'The new passwords you entered do not match', serverError: ''}); + return; + } + + var data = {}; + data.user_id = user.id; + data.current_password = currentPassword; + data.new_password = newPassword; + + Client.updatePassword(data, + function success() { + this.props.updateSection(''); + AsyncClient.getMe(); + this.setState(this.setupInitialState()); + }.bind(this), + function fail(err) { + var state = this.setupInitialState(); + if (err.message) { + state.serverError = err.message; + } else { + state.serverError = err; + } + state.passwordError = ''; + this.setState(state); + }.bind(this) + ); + } + updateCurrentPassword(e) { + this.setState({currentPassword: e.target.value}); + } + updateNewPassword(e) { + this.setState({newPassword: e.target.value}); + } + updateConfirmPassword(e) { + this.setState({confirmPassword: e.target.value}); + } + handleHistoryOpen() { + $('#user_settings').modal('hide'); + } + handleDevicesOpen() { + $('#user_settings').modal('hide'); + } + handleClose() { + $(React.findDOMNode(this)).find('.form-control').each(function resetValue() { + this.value = ''; + }); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + + this.props.updateTab('general'); + } + setupInitialState() { + return {currentPassword: '', newPassword: '', confirmPassword: ''}; + } + componentDidMount() { + $('#user_settings').on('hidden.bs.modal', this.handleClose); + } + componentWillUnmount() { + $('#user_settings').off('hidden.bs.modal', this.handleClose); + this.props.updateSection(''); + } + render() { + var serverError; + if (this.state.serverError) { + serverError = this.state.serverError; + } + var passwordError; + if (this.state.passwordError) { + passwordError = this.state.passwordError; + } + + var updateSectionStatus; + var passwordSection; + if (this.props.activeSection === 'password') { + var inputs = []; + var submit = null; + + if (this.props.user.auth_service === '') { + inputs.push( +
+ +
+ +
+
+ ); + inputs.push( +
+ +
+ +
+
+ ); + inputs.push( +
+ +
+ +
+
+ ); + + submit = this.submitPassword; + } else { + inputs.push( +
+ +
+ ); + } + + updateSectionStatus = function resetSection(e) { + this.props.updateSection(''); + this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); + e.preventDefault(); + }.bind(this); + + passwordSection = ( + + ); + } else { + var describe; + if (this.props.user.auth_service === '') { + var d = new Date(this.props.user.last_password_update); + var hour = '12'; + if (d.getHours() % 12) { + hour = String(d.getHours() % 12); + } + var min = String(d.getMinutes()); + if (d.getMinutes() < 10) { + min = '0' + d.getMinutes(); + } + var timeOfDay = ' am'; + if (d.getHours() >= 12) { + timeOfDay = ' pm'; + } + + describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; + } else { + describe = 'Log in done through GitLab'; + } + + updateSectionStatus = function updateSection() { + this.props.updateSection('password'); + }.bind(this); + + passwordSection = ( + + ); + } + + return ( +
+
+ +

+ Security Settings +

+
+
+

Security Settings

+
+ {passwordSection} +
+
    +
  • {'Version ' + global.window.config.Version}
  • +
  • +
    {'Build Number: ' + global.window.config.BuildNumber}
    +
    {'Build Date: ' + global.window.config.BuildDate}
    +
    {'Build Hash: ' + global.window.config.BuildHash}
    +
  • +
+ +
+ ); + } +} + +SecurityTab.defaultProps = { + user: {}, + activeSection: '' +}; +SecurityTab.propTypes = { + user: React.PropTypes.object, + activeSection: React.PropTypes.string, + updateSection: React.PropTypes.func, + updateTab: React.PropTypes.func +}; -- cgit v1.2.3-1-g7c22