From caabfbcdd56bdced7c5c1d38e00f488adffe7c60 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 14 Jul 2016 10:08:36 -0400 Subject: PLT-2992 Added the ability to use different themes for each team (#3411) * Cleaned up user_settings_theme.jsx and import_theme_modal.jsx * Made ImportThemeModal use a callback to return the theme to the user settings modal instead of saving it directly * Moved user theme from model to preferences * Added serverside API to delete preferences TODO update package with client stuff * Changed constants.jsx so that Preferences and ActionTypes can be imported on their own * Updated ThemeProps migration code to properly rename solarized code themes * Fixed warnings thrown by AppDispatcher * Added clientside UI to support team-specific themes * Removed debugging code from test * Fixed setting a user's theme when they haven't set their theme before --- webapp/components/logged_in.jsx | 9 -- webapp/components/needs_team.jsx | 37 ++++++- webapp/components/setting_item_max.jsx | 4 +- .../user_settings/import_theme_modal.jsx | 91 ++++++++------- .../user_settings/user_settings_theme.jsx | 122 +++++++++++++-------- 5 files changed, 155 insertions(+), 108 deletions(-) (limited to 'webapp/components') diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx index f637e9dc5..2ac858dfb 100644 --- a/webapp/components/logged_in.jsx +++ b/webapp/components/logged_in.jsx @@ -92,15 +92,6 @@ export default class LoggedIn extends React.Component { id: user.id }); } - - // Update CSS classes to match user theme - if (user) { - if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { - Utils.applyTheme(user.theme_props); - } else { - Utils.applyTheme(Constants.THEMES.default); - } - } } onUserChanged() { diff --git a/webapp/components/needs_team.jsx b/webapp/components/needs_team.jsx index 07b90636d..a8c7b3508 100644 --- a/webapp/components/needs_team.jsx +++ b/webapp/components/needs_team.jsx @@ -41,19 +41,34 @@ export default class NeedsTeam extends React.Component { constructor(params) { super(params); - this.onChanged = this.onChanged.bind(this); + this.onTeamChanged = this.onTeamChanged.bind(this); + this.onPreferencesChanged = this.onPreferencesChanged.bind(this); + + const team = TeamStore.getCurrent(); this.state = { - team: TeamStore.getCurrent() + team, + theme: PreferenceStore.getTheme(team.id) }; } - onChanged() { + onTeamChanged() { + const team = TeamStore.getCurrent(); + this.setState({ - team: TeamStore.getCurrent() + team, + theme: PreferenceStore.getTheme(team.id) }); } + onPreferencesChanged(category) { + if (!category || category === Preferences.CATEGORY_THEME) { + this.setState({ + theme: PreferenceStore.getTheme(this.state.team.id) + }); + } + } + componentWillMount() { // Go to tutorial if we are first arriving const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999); @@ -63,7 +78,8 @@ export default class NeedsTeam extends React.Component { } componentDidMount() { - TeamStore.addChangeListener(this.onChanged); + TeamStore.addChangeListener(this.onTeamChanged); + PreferenceStore.addChangeListener(this.onPreferencesChanged); // Emit view action GlobalActions.viewLoggedIn(); @@ -80,10 +96,19 @@ export default class NeedsTeam extends React.Component { $(window).on('blur', () => { window.isActive = false; }); + + Utils.applyTheme(this.state.theme); + } + + componentDidUpdate(prevProps, prevState) { + if (!Utils.areObjectsEqual(prevState.theme, this.state.theme)) { + Utils.applyTheme(this.state.theme); + } } componentWillUnmount() { - TeamStore.removeChangeListener(this.onChanged); + TeamStore.removeChangeListener(this.onTeamChanged); + PreferenceStore.removeChangeListener(this.onPreferencesChanged); $(window).off('focus'); $(window).off('blur'); } diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx index ec496a765..ad765a7d6 100644 --- a/webapp/components/setting_item_max.jsx +++ b/webapp/components/setting_item_max.jsx @@ -84,6 +84,7 @@ export default class SettingItemMax extends React.Component {

  • + {this.props.submitExtra} {serverError} {clientError} {submit} @@ -113,5 +114,6 @@ SettingItemMax.propTypes = { updateSection: React.PropTypes.func, submit: React.PropTypes.func, title: React.PropTypes.node, - width: React.PropTypes.string + width: React.PropTypes.string, + submitExtra: React.PropTypes.node }; diff --git a/webapp/components/user_settings/import_theme_modal.jsx b/webapp/components/user_settings/import_theme_modal.jsx index 552659c4c..32c6837e8 100644 --- a/webapp/components/user_settings/import_theme_modal.jsx +++ b/webapp/components/user_settings/import_theme_modal.jsx @@ -1,30 +1,18 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import ReactDOM from 'react-dom'; import ModalStore from 'stores/modal_store.jsx'; -import UserStore from 'stores/user_store.jsx'; -import * as Utils from 'utils/utils.jsx'; -import Client from 'utils/web_client.jsx'; import {Modal} from 'react-bootstrap'; -import AppDispatcher from '../../dispatcher/app_dispatcher.jsx'; import Constants from 'utils/constants.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; - -const holders = defineMessages({ - submitError: { - id: 'user.settings.import_theme.submitError', - defaultMessage: 'Invalid format, please try copying and pasting in again.' - } -}); +import {FormattedMessage} from 'react-intl'; const ActionTypes = Constants.ActionTypes; import React from 'react'; -class ImportThemeModal extends React.Component { +export default class ImportThemeModal extends React.Component { constructor(props) { super(props); @@ -33,26 +21,42 @@ class ImportThemeModal extends React.Component { this.handleChange = this.handleChange.bind(this); this.state = { + value: '', inputError: '', - show: false + show: false, + callback: null }; } + componentDidMount() { ModalStore.addModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow); } + componentWillUnmount() { ModalStore.removeModalListener(ActionTypes.TOGGLE_IMPORT_THEME_MODAL, this.updateShow); } - updateShow(show) { - this.setState({show}); + + updateShow(show, args) { + this.setState({ + show, + callback: args.callback + }); } + handleSubmit(e) { e.preventDefault(); - const text = ReactDOM.findDOMNode(this.refs.input).value; + const text = this.state.value; if (!this.isInputValid(text)) { - this.setState({inputError: this.props.intl.formatMessage(holders.submitError)}); + this.setState({ + inputError: ( + + ) + }); return; } @@ -81,26 +85,13 @@ class ImportThemeModal extends React.Component { theme.mentionHighlightLink = '#2f81b7'; theme.codeTheme = 'github'; - const user = UserStore.getCurrentUser(); - user.theme_props = theme; - - Client.updateUser(user, Constants.UserUpdateEvents.THEME, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_ME, - me: data - }); - - this.setState({show: false}); - Utils.applyTheme(theme); - }, - (err) => { - var state = this.getStateFromStores(); - state.serverError = err; - this.setState(state); - } - ); + this.state.callback(theme); + this.setState({ + show: false, + callback: null + }); } + isInputValid(text) { if (text.length === 0) { return false; @@ -134,13 +125,25 @@ class ImportThemeModal extends React.Component { return true; } + handleChange(e) { - if (this.isInputValid(e.target.value)) { + const value = e.target.value; + this.setState({value}); + + if (this.isInputValid(value)) { this.setState({inputError: null}); } else { - this.setState({inputError: this.props.intl.formatMessage(holders.submitError)}); + this.setState({ + inputError: ( + + ) + }); } } + render() { return ( @@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
    @@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component { ); } } - -ImportThemeModal.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(ImportThemeModal); diff --git a/webapp/components/user_settings/user_settings_theme.jsx b/webapp/components/user_settings/user_settings_theme.jsx index 94516ec8c..4ff08402a 100644 --- a/webapp/components/user_settings/user_settings_theme.jsx +++ b/webapp/components/user_settings/user_settings_theme.jsx @@ -8,28 +8,18 @@ import PremadeThemeChooser from './premade_theme_chooser.jsx'; import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; +import PreferenceStore from 'stores/preference_store.jsx'; +import TeamStore from 'stores/team_store.jsx'; import UserStore from 'stores/user_store.jsx'; import AppDispatcher from '../../dispatcher/app_dispatcher.jsx'; -import Client from 'utils/web_client.jsx'; -import * as Utils from 'utils/utils.jsx'; +import * as UserActions from 'actions/user_actions.jsx'; -import Constants from 'utils/constants.jsx'; +import * as Utils from 'utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; -const ActionTypes = Constants.ActionTypes; - -const holders = defineMessages({ - themeTitle: { - id: 'user.settings.display.theme.title', - defaultMessage: 'Theme' - }, - themeDescribe: { - id: 'user.settings.display.theme.describe', - defaultMessage: 'Open to manage your theme' - } -}); +import {ActionTypes, Constants, Preferences} from 'utils/constants.jsx'; import React from 'react'; @@ -47,6 +37,7 @@ export default class ThemeSetting extends React.Component { this.originalTheme = Object.assign({}, this.state.theme); } + componentDidMount() { UserStore.addChangeListener(this.onChange); @@ -54,17 +45,20 @@ export default class ThemeSetting extends React.Component { $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } } + componentDidUpdate() { if (this.props.selected) { $('.color-btn').removeClass('active-border'); $(ReactDOM.findDOMNode(this.refs[this.state.theme])).addClass('active-border'); } } + componentWillReceiveProps(nextProps) { if (this.props.selected && !nextProps.selected) { this.resetFields(); } } + componentWillUnmount() { UserStore.removeChangeListener(this.onChange); @@ -73,27 +67,35 @@ export default class ThemeSetting extends React.Component { Utils.applyTheme(state.theme); } } + getStateFromStores() { - const user = UserStore.getCurrentUser(); - let theme = null; + const teamId = TeamStore.getCurrentId(); - if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { - theme = Object.assign({}, user.theme_props); - } else { - theme = $.extend(true, {}, Constants.THEMES.default); + const theme = PreferenceStore.getTheme(teamId); + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; } - let type = 'premade'; - if (theme.type === 'custom') { - type = 'custom'; - } + let showAllTeamsCheckbox = false; + let applyToAllTeams = true; - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; + if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true') { + // show the "apply to all teams" checkbox if the user is on more than one team + showAllTeamsCheckbox = Object.keys(TeamStore.getAll()).length > 1; + + // check the "apply to all teams" checkbox by default if the user has any team-specific themes + applyToAllTeams = PreferenceStore.getCategory(Preferences.CATEGORY_THEME).size <= 1; } - return {theme, type}; + return { + teamId: TeamStore.getCurrentId(), + theme, + type: theme.type || 'premade', + showAllTeamsCheckbox, + applyToAllTeams + }; } + onChange() { const newState = this.getStateFromStores(); @@ -103,21 +105,20 @@ export default class ThemeSetting extends React.Component { this.props.setEnforceFocus(true); } + scrollToTop() { $('.ps-container.modal-body').scrollTop(0); } + submitTheme(e) { e.preventDefault(); - var user = UserStore.getCurrentUser(); - user.theme_props = this.state.theme; - Client.updateUser(user, Constants.UserUpdateEvents.THEME, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_ME, - me: data - }); + const teamId = this.state.applyToAllTeams ? '' : this.state.teamId; + UserActions.saveTheme( + teamId, + this.state.theme, + () => { this.props.setRequireConfirm(false); this.originalTheme = Object.assign({}, this.state.theme); this.scrollToTop(); @@ -130,6 +131,7 @@ export default class ThemeSetting extends React.Component { } ); } + updateTheme(theme) { let themeChanged = this.state.theme.length === theme.length; if (!themeChanged) { @@ -148,9 +150,11 @@ export default class ThemeSetting extends React.Component { this.setState({theme}); Utils.applyTheme(theme); } + updateType(type) { this.setState({type}); } + resetFields() { const state = this.getStateFromStores(); state.serverError = null; @@ -161,17 +165,18 @@ export default class ThemeSetting extends React.Component { this.props.setRequireConfirm(false); } + handleImportModal() { AppDispatcher.handleViewAction({ type: ActionTypes.TOGGLE_IMPORT_THEME_MODAL, - value: true + value: true, + callback: this.updateTheme }); this.props.setEnforceFocus(false); } - render() { - const {formatMessage} = this.props.intl; + render() { var serverError; if (this.state.serverError) { serverError = this.state.serverError; @@ -266,9 +271,29 @@ export default class ThemeSetting extends React.Component {
    ); + let allTeamsCheckbox = null; + if (this.state.showAllTeamsCheckbox) { + allTeamsCheckbox = ( +
    + +
    + ); + } + themeUI = ( + } + describe={ + + } updateSection={() => { this.props.updateSection('theme'); }} @@ -295,11 +330,8 @@ export default class ThemeSetting extends React.Component { } ThemeSetting.propTypes = { - intl: intlShape.isRequired, selected: React.PropTypes.bool.isRequired, updateSection: React.PropTypes.func.isRequired, setRequireConfirm: React.PropTypes.func.isRequired, setEnforceFocus: React.PropTypes.func.isRequired }; - -export default injectIntl(ThemeSetting); -- cgit v1.2.3-1-g7c22