Skip to content

Commit

Permalink
PLT-2992 Added the ability to use different themes for each team (mat…
Browse files Browse the repository at this point in the history
…termost#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
  • Loading branch information
hmhealey authored and crspeller committed Jul 14, 2016
1 parent 91e91e6 commit 73dcc22
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 225 deletions.
56 changes: 55 additions & 1 deletion actions/user_actions.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

import Client from 'utils/web_client.jsx';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'utils/web_client.jsx';

import PreferenceStore from 'stores/preference_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';

import {ActionTypes, Preferences} from 'utils/constants.jsx';

export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) {
Client.ldapToEmail(
Expand All @@ -28,3 +33,52 @@ export function getMoreDmList() {
AsyncClient.getProfilesForDirectMessageList();
AsyncClient.getTeamMembers(TeamStore.getCurrentId());
}

export function saveTheme(teamId, theme, onSuccess, onError) {
AsyncClient.savePreference(
Preferences.CATEGORY_THEME,
teamId,
JSON.stringify(theme),
() => {
onThemeSaved(teamId, theme, onSuccess);
},
(err) => {
onError(err);
}
);
}

function onThemeSaved(teamId, theme, onSuccess) {
const themePreferences = PreferenceStore.getCategory(Preferences.CATEGORY_THEME);

if (teamId !== '' && themePreferences.size > 1) {
// no extra handling to be done to delete team-specific themes
onSuccess();
return;
}

const toDelete = [];

for (const [name] of themePreferences) {
if (name === '') {
continue;
}

toDelete.push({
user_id: UserStore.getCurrentId(),
category: Preferences.CATEGORY_THEME,
name
});
}

// we're saving a new global theme so delete any team-specific ones
AsyncClient.deletePreferences(toDelete);

// delete them locally before we hear from the server so that the UI flow is smoother
AppDispatcher.handleServerAction({
type: ActionTypes.DELETED_PREFERENCES,
preferences: toDelete
});

onSuccess();
}
9 changes: 0 additions & 9 deletions components/logged_in.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
37 changes: 31 additions & 6 deletions components/needs_team.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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');
}
Expand Down
4 changes: 3 additions & 1 deletion components/setting_item_max.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default class SettingItemMax extends React.Component {
</li>
<li className='setting-list-item'>
<hr/>
{this.props.submitExtra}
{serverError}
{clientError}
{submit}
Expand Down Expand Up @@ -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
};
91 changes: 44 additions & 47 deletions components/user_settings/import_theme_modal.jsx
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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: (
<FormattedMessage
id='user.settings.import_theme.submitError'
defaultMessage='Invalid format, please try copying and pasting in again.'
/>
)
});
return;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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: (
<FormattedMessage
id='user.settings.import_theme.submitError'
defaultMessage='Invalid format, please try copying and pasting in again.'
/>
)
});
}
}

render() {
return (
<span>
Expand Down Expand Up @@ -170,9 +173,9 @@ class ImportThemeModal extends React.Component {
<div className='form-group less'>
<div className='col-sm-9'>
<input
ref='input'
type='text'
className='form-control'
value={this.state.value}
onChange={this.handleChange}
/>
<div className='input__help'>
Expand Down Expand Up @@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component {
);
}
}

ImportThemeModal.propTypes = {
intl: intlShape.isRequired
};

export default injectIntl(ImportThemeModal);
Loading

0 comments on commit 73dcc22

Please sign in to comment.