feat(theme-picker): Uninstall themes on click

Summary: Themes can now be uninstalled by clicking a button in the theme picker, instead of going to `~/.nylas/packages` to delete the directory.

Test Plan: Tested locally.

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2700
This commit is contained in:
Jackie Luo 2016-03-08 17:08:09 -08:00
parent 327eb43932
commit f526bb2736
9 changed files with 139 additions and 32 deletions

View file

@ -1,18 +1,23 @@
/** @babel */
import React from 'react';
import Actions from '../../../src/flux/actions'
import Actions from '../../../src/flux/actions';
import ThemePicker from './theme-picker'
import ThemePicker from './theme-picker';
import ThemePickerStore from './theme-picker-store';
export function activate() {
this.disposable = NylasEnv.commands.add("body",
"window:launch-theme-picker",
() => Actions.openModal(children=<ThemePicker />,
height=400,
width=250));
ThemePickerStore.activate();
this.disposable = NylasEnv.commands.add("body", "window:launch-theme-picker", () => {
Actions.openModal(
children=<ThemePicker />,
height=400,
width=250,
);
});
}
export function deactivate() {
ThemePickerStore.deactivate();
this.disposable.dispose();
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import fs from 'fs-plus';
import path from 'path';
import {EventedIFrame} from 'nylas-component-kit';
import {EventedIFrame, RetinaImg} from 'nylas-component-kit';
import LessCompileCache from '../../../src/less-compile-cache'
@ -10,6 +10,8 @@ class ThemeOption extends React.Component {
static propTypes = {
theme: React.PropTypes.object.isRequired,
active: React.PropTypes.bool.isRequired,
onSelect: React.PropTypes.func.isRequired,
onUninstall: React.PropTypes.func.isRequired,
}
constructor(props) {
@ -65,7 +67,7 @@ class ThemeOption extends React.Component {
}
_writeContent() {
const domNode = React.findDOMNode(this);
const domNode = React.findDOMNode(this.refs.iframe);
const doc = domNode.contentDocument;
if (!doc) return;
@ -93,8 +95,26 @@ class ThemeOption extends React.Component {
}
render() {
const uninstallButton = this.props.theme.path.indexOf("/n1/internal_packages") === -1 ? (
<RetinaImg
className="theme-uninstall-x"
name="uninstall-x.png"
mode={RetinaImg.Mode.ContentDark}
style={{width: "14", height: "14"}}
onMouseDown={this.props.onUninstall} />) : null;
return (
<EventedIFrame ref="iframe" className={`theme-preview-${this.props.theme.name}`} frameBorder="0" width="105px" height="65px" flex="1" style={{pointerEvents: "none"}} />
<div>
{uninstallButton}
<div className="clickable-theme-option" onMouseDown={this.props.onSelect}>
<EventedIFrame
ref="iframe"
className={`theme-preview-${this.props.theme.name}`}
frameBorder="0"
width="105px"
height="65px"
flex="1" />
</div>
</div>
);
}
}

View file

@ -0,0 +1,12 @@
/** @babel */
import Reflux from 'reflux';
ThemePickerActions = Reflux.createActions([
"uninstallTheme",
]);
for (key in ThemePickerActions) {
ThemePickerActions[key].sync = true;
}
export default ThemePickerActions;

View file

@ -0,0 +1,32 @@
import NylasStore from 'nylas-store';
import {APMWrapper} from 'nylas-exports';
import ThemePickerActions from './theme-picker-actions';
class ThemePickerStore extends NylasStore {
constructor() {
super();
this._apm = new APMWrapper();
}
activate = ()=> {
this.unlisten = ThemePickerActions.uninstallTheme.listen(this.uninstallTheme);
}
uninstallTheme = (theme)=> {
if (NylasEnv.packages.isPackageLoaded(theme.name)) {
NylasEnv.packages.disablePackage(theme.name);
NylasEnv.packages.unloadPackage(theme.name);
}
this._apm.uninstall(theme);
}
deactivate = ()=> {
this.unlisten();
}
}
export default new ThemePickerStore();

View file

@ -2,6 +2,7 @@ import React from 'react';
import Actions from '../../../src/flux/actions'
import {Flexbox, RetinaImg} from 'nylas-component-kit';
import ThemePickerActions from './theme-picker-actions';
import ThemeOption from './theme-option';
@ -10,12 +11,12 @@ class ThemePicker extends React.Component {
constructor(props) {
super(props);
this._themeManager = NylasEnv.themes;
this.themes = NylasEnv.themes;
this.state = this._getState();
}
componentDidMount() {
this.disposable = this._themeManager.onDidChangeActiveThemes(() => {
this.disposable = this.themes.onDidChangeActiveThemes(() => {
this.setState(this._getState());
});
}
@ -26,15 +27,14 @@ class ThemePicker extends React.Component {
_getState() {
return {
themes: this._themeManager.getLoadedThemes(),
activeTheme: this._themeManager.getActiveTheme().name,
themes: this.themes.getLoadedThemes(),
activeTheme: this.themes.getActiveTheme().name,
}
}
_setActiveTheme(theme) {
const prevActiveTheme = this.state.activeTheme;
this.setState({activeTheme: theme});
this._themeManager.setActiveTheme(theme);
this.themes.setActiveTheme(theme);
this._rewriteIFrame(prevActiveTheme, theme);
}
@ -47,24 +47,25 @@ class ThemePicker extends React.Component {
activeElement.className = "theme-option active-true";
}
_onUninstallTheme(theme) {
ThemePickerActions.uninstallTheme(theme);
this.setState({themes: this.themes.getLoadedThemes()});
}
_renderThemeOptions() {
const themeOptions = this.state.themes.map((theme) =>
<div
className="clickable-theme-option"
onMouseDown={() => this._setActiveTheme(theme.name)}
style={{cursor: "pointer", width: "115px", margin: "2px"}}>
<ThemeOption
key={theme.name}
theme={theme}
active={this.state.activeTheme === theme.name} />
</div>
)
return themeOptions;
return this.state.themes.map((theme) =>
<ThemeOption
key={theme.name}
theme={theme}
active={this.state.activeTheme === theme.name}
onSelect={() => this._setActiveTheme(theme.name)}
onUninstall={() => this._onUninstallTheme(theme)} />
);
}
render() {
return (
<div style={{textAlign: "center", cursor: "default"}}>
<div className="theme-picker">
<Flexbox direction="column">
<RetinaImg
style={{width: "14", height: "14", margin: "8px", WebkitFilter: "none"}}

View file

@ -7,19 +7,31 @@ import ThemePicker from '../lib/theme-picker';
const {resourcePath} = NylasEnv.getLoadSettings();
const light = new ThemePackage(resourcePath + '/internal_packages/ui-light');
const dark = new ThemePackage(resourcePath + '/internal_packages/ui-dark');
const thirdPartyTheme = new ThemePackage(resourcePath + '/internal_packages/ui-light');
thirdPartyTheme.name = 'third-party-theme'
thirdPartyTheme.path = ''
describe('ThemePicker', ()=> {
beforeEach(()=> {
spyOn(ThemePicker.prototype, '_setActiveTheme').andCallThrough();
spyOn(ThemePicker.prototype, '_rewriteIFrame');
spyOn(NylasEnv.themes, 'getLoadedThemes').andReturn([light, dark]);
spyOn(NylasEnv.themes, 'getLoadedThemes').andReturn([light, dark, thirdPartyTheme]);
spyOn(NylasEnv.themes, 'getActiveTheme').andReturn(light);
this.component = ReactTestUtils.renderIntoDocument(<ThemePicker />);
});
it('changes the active theme when a theme is clicked', ()=> {
spyOn(ThemePicker.prototype, '_setActiveTheme').andCallThrough();
spyOn(ThemePicker.prototype, '_rewriteIFrame');
const themeOption = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'clickable-theme-option')[1]);
ReactTestUtils.Simulate.mouseDown(themeOption);
expect(ThemePicker.prototype._setActiveTheme).toHaveBeenCalled();
});
it('uninstalls themes on click', ()=> {
spyOn(ThemePicker.prototype, '_onUninstallTheme').andCallThrough();
spyOn(ThemePicker.prototype, 'setState').andCallThrough();
const uninstallButton = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'theme-uninstall-x')[0]);
ReactTestUtils.Simulate.mouseDown(uninstallButton);
expect(ThemePicker.prototype._onUninstallTheme).toHaveBeenCalled();
expect(ThemePicker.prototype.setState).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,25 @@
@import "ui-variables";
.theme-picker {
text-align: center;
cursor: default;
iframe {
pointer-events: none;
position: relative;
z-index: 0;
}
.theme-uninstall-x {
float: right;
position: relative;
z-index: 1;
margin-bottom: -40px;
margin-right: 10px;
-webkit-filter: none;
}
.clickable-theme-option {
cursor: pointer;
width: 115px;
margin: 2px;
top: -10px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB