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
Этот коммит содержится в:
Harrison Healey
2016-07-14 10:08:36 -04:00
коммит произвёл Christopher Speller
родитель 8e810bc2eb
Коммит caabfbcdd5
23 изменённых файлов: 671 добавлений и 273 удалений

Просмотреть файл

@@ -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: (
<FormattedMessage
id='user.settings.import_theme.submitError'
defaultMessage='Invalid format, please try copying and pasting in again.'
/>
)
});
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: (
<FormattedMessage
id='user.settings.import_theme.submitError'
defaultMessage='Invalid format, please try copying and pasting in again.'
/>
)
});
}
}
render() {
return (
<span>
@@ -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'>
@@ -210,9 +213,3 @@ class ImportThemeModal extends React.Component {
);
}
}
ImportThemeModal.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(ImportThemeModal);

Просмотреть файл

@@ -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 UserActions from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
import {intlShape, injectIntl, defineMessages, 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;
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
theme = Object.assign({}, user.theme_props);
} else {
theme = $.extend(true, {}, Constants.THEMES.default);
}
let type = 'premade';
if (theme.type === 'custom') {
type = 'custom';
}
const teamId = TeamStore.getCurrentId();
const theme = PreferenceStore.getTheme(teamId);
if (!theme.codeTheme) {
theme.codeTheme = Constants.DEFAULT_CODE_THEME;
}
return {theme, type};
let showAllTeamsCheckbox = false;
let applyToAllTeams = true;
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 {
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 {
</div>
);
let allTeamsCheckbox = null;
if (this.state.showAllTeamsCheckbox) {
allTeamsCheckbox = (
<div className='checkbox user-settings__submit-checkbox'>
<label>
<input
type='checkbox'
checked={this.state.applyToAllTeams}
onChange={(e) => this.setState({applyToAllTeams: e.target.checked})}
/>
<FormattedMessage
id='user.settings.display.theme.applyToAllTeams'
defaultMessage='Apply New Theme to All Teams'
/>
</label>
</div>
);
}
themeUI = (
<SettingItemMax
inputs={inputs}
submitExtra={allTeamsCheckbox}
submit={this.submitTheme}
server_error={serverError}
width='full'
@@ -281,8 +306,18 @@ export default class ThemeSetting extends React.Component {
} else {
themeUI = (
<SettingItemMin
title={formatMessage(holders.themeTitle)}
describe={formatMessage(holders.themeDescribe)}
title={
<FormattedMessage
id='user.settings.display.theme.title'
defaultMessage='Theme'
/>
}
describe={
<FormattedMessage
id='user.settings.display.theme.describe'
defaultMessage='Open to manage your theme'
/>
}
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);