fix(prefs): Move to a sheet rather than a window, use configSchema

Summary:
This diff moves the preferences interface to a sheet in the main window, with the following benefits:
- We can put any sort of React control in it (no ReactRemote)
- It's not strange for the interface to scroll
- Since it can scroll, it's safe to auto-generate preferences for plugins based on their package config schema.

The general tab is now mostly based on the config schema, with the exception of the "Workspace" and "Layout" bits.

The other tabs are still manual, and should be polished more.

Test Plan: No new tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2278
This commit is contained in:
Ben Gotow 2015-11-23 12:20:51 -08:00
parent 971089aeb0
commit 78dd69290d
22 changed files with 500 additions and 619 deletions

View file

@ -119,7 +119,9 @@ class AccountSwitcher extends React.Component
@setState(showing: false)
_onManageAccounts: =>
Actions.openPreferences({tab: 'Accounts'})
Actions.switchPreferencesSection('Accounts')
Actions.openPreferences()
@setState(showing: false)
_getStateFromStores: =>

View file

@ -1,10 +1,13 @@
{PreferencesSectionStore} = require 'nylas-exports'
{PreferencesSectionStore,
Actions,
WorkspaceStore,
ComponentRegistry} = require 'nylas-exports'
module.exports =
activate: (@state={}) ->
activate: ->
ipc = require 'ipc'
React = require 'react'
{Actions} = require('nylas-exports')
Cfg = PreferencesSectionStore.SectionConfig
@ -29,38 +32,20 @@ module.exports =
component: require './tabs/preferences-keymaps'
order: 3
})
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-notifications.png'
sectionId: 'Notifications'
displayName: 'Notifications'
component: require './tabs/preferences-notifications'
order: 4
})
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-appearance.png'
sectionId: 'Appearance'
displayName: 'Appearance'
component: require './tabs/preferences-appearance'
order: 5
})
WorkspaceStore.defineSheet 'Preferences', {},
split: ['Preferences']
list: ['Preferences']
PreferencesRoot = require('./preferences-root')
ComponentRegistry.register PreferencesRoot,
location: WorkspaceStore.Location.Preferences
Actions.openPreferences.listen(@_openPreferences)
ipc.on 'open-preferences', => @_openPreferences()
_openPreferences: ({tab} = {}) ->
{ReactRemote} = require('nylas-exports')
Preferences = require('./preferences')
ReactRemote.openWindowForComponent(Preferences, {
tag: 'preferences'
title: "Preferences"
width: 520
resizable: false
autosize: true
stylesheetRegex: /(preferences|nylas\-fonts)/
props: {
initialTab: tab
}
})
_openPreferences: ->
Actions.pushSheet(WorkspaceStore.Sheet.Preferences)
deactivate: ->

View file

@ -1,38 +0,0 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesHeader extends React.Component
@displayName: 'PreferencesHeader'
@propTypes:
tabs: React.PropTypes.array.isRequired
changeActiveTab: React.PropTypes.func.isRequired
activeTab: React.PropTypes.object
render: =>
if process.platform is "win32"
imgMode = RetinaImg.Mode.ContentIsMask
else
imgMode = RetinaImg.Mode.ContentPreserve
<div className="preference-header">
{ @props.tabs.map (sectionConfig) =>
classname = "preference-header-item"
classname += " active" if sectionConfig is @props.activeTab
<div className={classname} onClick={ => @props.changeActiveTab(sectionConfig) } key={sectionConfig.sectionId}>
<div className="phi-container">
<div className="icon">
<RetinaImg mode={imgMode} {...sectionConfig.nameOrUrl()} />
</div>
<div className="name">
{sectionConfig.displayName}
</div>
</div>
</div>
}
</div>
module.exports = PreferencesHeader

View file

@ -0,0 +1,43 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox, ConfigPropContainer, ScrollRegion} = require 'nylas-component-kit'
{PreferencesSectionStore} = require 'nylas-exports'
PreferencesSidebar = require './preferences-sidebar'
class PreferencesRoot extends React.Component
@displayName: 'PreferencesRoot'
@containerRequired: false
constructor: (@props) ->
@state = @getStateFromStores()
componentDidMount: =>
@unlisteners = []
@unlisteners.push PreferencesSectionStore.listen =>
@setState(@getStateFromStores())
componentWillUnmount: =>
unlisten() for unlisten in @unlisteners
getStateFromStores: =>
sections: PreferencesSectionStore.sections()
activeSectionId: PreferencesSectionStore.activeSectionId()
render: =>
section = _.find @state.sections, ({sectionId}) => sectionId is @state.activeSectionId
if section
bodyElement = <section.component />
else
bodyElement = <div>No Section Active</div>
<Flexbox direction="row" className="preferences-wrap">
<PreferencesSidebar sections={@state.sections}
activeSectionId={@state.activeSectionId} />
<ScrollRegion className="preferences-content">
<ConfigPropContainer>{bodyElement}</ConfigPropContainer>
</ScrollRegion>
</Flexbox>
module.exports = PreferencesRoot

View file

@ -0,0 +1,30 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
{Actions} = require 'nylas-exports'
class PreferencesSidebar extends React.Component
@displayName: 'PreferencesSidebar'
@propTypes:
sections: React.PropTypes.array.isRequired
activeSectionId: React.PropTypes.string
render: =>
<div className="preferences-sidebar">
{ @props.sections.map ({sectionId, displayName}) =>
classname = "item"
classname += " active" if sectionId is @props.activeSectionId
<div key={sectionId}
className={classname}
onClick={ => Actions.switchPreferencesSection(sectionId) }>
<div className="name">
{displayName}
</div>
</div>
}
</div>
module.exports = PreferencesSidebar

View file

@ -1,53 +0,0 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox, ConfigPropContainer} = require 'nylas-component-kit'
{PreferencesSectionStore} = require 'nylas-exports'
PreferencesHeader = require './preferences-header'
class Preferences extends React.Component
@displayName: 'Preferences'
constructor: (@props) ->
tabs = PreferencesSectionStore.sections()
if @props.initialTab
activeTab = _.find tabs, (t) => t.name is @props.initialTab
activeTab ||= tabs[0]
@state = _.extend(@getStateFromStores(), {activeTab})
componentDidMount: =>
@unlisteners = []
@unlisteners.push PreferencesSectionStore.listen =>
@setState(@getStateFromStores())
componentWillUnmount: =>
unlisten() for unlisten in @unlisteners
componentDidUpdate: =>
if @state.tabs.length > 0 and not @state.activeTab
@setState(activeTab: @state.tabs[0])
getStateFromStores: =>
tabs: PreferencesSectionStore.sections()
render: =>
if @state.activeTab
bodyElement = <@state.activeTab.component config={@state.config} />
else
bodyElement = <div>No Tab Active</div>
<div className="preferences-wrap">
<PreferencesHeader tabs={@state.tabs}
activeTab={@state.activeTab}
changeActiveTab={@_onChangeActiveTab}/>
<ConfigPropContainer>
{bodyElement}
</ConfigPropContainer>
<div style={clear:'both'}></div>
</div>
_onChangeActiveTab: (tab) =>
@setState(activeTab: tab)
module.exports = Preferences

View file

@ -0,0 +1,63 @@
React = require 'react'
_ = require 'underscore'
_str = require 'underscore.string'
###
This component renders input controls for a subtree of the N1 config-schema
and reads/writes current values using the `config` prop, which is expected to
be an instance of the config provided by `ConfigPropContainer`.
The config schema follows the JSON Schema standard: http://json-schema.org/
###
class ConfigSchemaItem extends React.Component
@displayName: 'ConfigSchemaItem'
@propTypes:
config: React.PropTypes.object
configSchema: React.PropTypes.object
keyPath: React.PropTypes.string
render: ->
return false unless @_appliesToPlatform()
if @props.configSchema.type is 'object'
<section>
<h2>{_str.humanize(@props.keyName)}</h2>
{_.pairs(@props.configSchema.properties).map ([key, value]) =>
<ConfigSchemaItem
keyName={key}
keyPath={"#{@props.keyPath}.#{key}"}
configSchema={value}
config={@props.config}
/>
}
</section>
else if @props.configSchema['enum']?
<div className="item">
<label htmlFor={@props.keyPath}>{@props.configSchema.title}:</label>
<select onChange={@_onChangeValue}>
{_.zip(@props.configSchema.enum, @props.configSchema.enumLabels).map ([value, label]) =>
<option value={value}>{label}</option>
}
</select>
</div>
else if @props.configSchema.type is 'boolean'
<div className="item">
<input id={@props.keyPath} type="checkbox" onChange={@_onChangeChecked} checked={ @props.config.get(@props.keyPath) }/>
<label htmlFor={@props.keyPath}>{@props.configSchema.title}</label>
</div>
else
<span></span>
_appliesToPlatform: =>
return true if not @props.configSchema.platforms?
return true if process.platform in @props.configSchema.platforms
return false
_onChangeChecked: =>
@props.config.toggle(@props.keyPath)
_onChangeValue: (event) =>
@props.config.set(@props.keyPath, event.target.value)
module.exports = ConfigSchemaItem

View file

@ -16,22 +16,20 @@ class PreferencesAccounts extends React.Component
@unsubscribe?()
render: =>
<div className="container-accounts">
<section className="container-accounts">
<h2>Accounts</h2>
{@_renderAccounts()}
<div style={textAlign:"right", marginTop: '20'}>
<button className="btn btn-large" onClick={@_onAddAccount}>Add Account...</button>
</div>
{@_renderLinkedAccounts()}
</div>
</section>
_renderAccounts: =>
return false unless @state.accounts
<div>
<div className="section-header">
Accounts:
</div>
{ @state.accounts.map (account) =>
<div className="well large" style={marginBottom:10} key={account.id}>
<Flexbox direction="row" style={alignItems: 'middle'}>

View file

@ -1,54 +0,0 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class AppearanceModeOption extends React.Component
@propTypes:
mode: React.PropTypes.string.isRequired
active: React.PropTypes.bool
onClick: React.PropTypes.func
constructor: (@props) ->
render: =>
classname = "appearance-mode"
classname += " active" if @props.active
<div className={classname} onClick={@props.onClick}>
<RetinaImg name={"appearance-mode-#{@props.mode}.png"} mode={RetinaImg.Mode.ContentIsMask}/>
<div>{@props.mode} View</div>
</div>
class PreferencesAppearance extends React.Component
@displayName: 'PreferencesAppearance'
@propTypes:
config: React.PropTypes.object
render: =>
<div className="container-appearance">
<div className="section">
<div className="section-header">
Layout and theme:
</div>
<div className="section-body section-appearance">
<Flexbox direction="row" style={alignItems: "center"}>
{['list', 'split'].map (mode) =>
<AppearanceModeOption
mode={mode} key={mode}
active={@props.config.get('core.workspace.mode') is mode}
onClick={ => @props.config.set('core.workspace.mode', mode)} />
}
</Flexbox>
<div className="section-header">
<input type="checkbox"
id="dark"
checked={@props.config.contains('core.themes','ui-dark')}
onChange={ => @props.config.toggleContains('core.themes', 'ui-dark')}
/>
<label htmlFor="dark">Use dark color scheme</label>
</div>
</div>
</div>
</div>
module.exports = PreferencesAppearance

View file

@ -1,109 +1,43 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
{LaunchServices, AccountStore} = require 'nylas-exports'
{AccountStore} = require 'nylas-exports'
ConfigSchemaItem = require './config-schema-item'
WorkspaceSection = require './workspace-section'
class PreferencesGeneral extends React.Component
@displayName: 'PreferencesGeneral'
constructor: (@props) ->
@state = {}
@_services = new LaunchServices()
if @_services.available()
@_services.isRegisteredForURLScheme 'mailto', (registered) =>
@setState(defaultClient: registered)
toggleDefaultMailClient: =>
if @state.defaultClient is true
@setState(defaultClient: false)
@_services.resetURLScheme('mailto')
else
@setState(defaultClient: true)
@_services.registerForURLScheme('mailto')
toggleShowImportant: (event) =>
@props.config.toggle('core.showImportant')
event.preventDefault()
toggleAutoloadImages: (event) =>
@props.config.toggle('core.reading.autoloadImages')
event.preventDefault()
toggleShowSystemTrayIcon: (event) =>
@props.config.toggle('core.showSystemTray')
event.preventDefault()
_renderImportanceOptionElement: =>
return false unless AccountStore.current()?.usesImportantFlag()
importanceOptionElement = <div className="section-header">
<input type="checkbox" id="show-important"
checked={@props.config.get('core.showImportant')}
onChange={@toggleShowImportant}/>
<label htmlFor="show-important">Show Gmail-style important markers</label>
</div>
render: =>
<div className="container-notifications">
<div className="section">
<div className="section-header platform-darwin-only">
<input type="checkbox" id="default-client" checked={@state.defaultClient} onChange={@toggleDefaultMailClient}/>
<label htmlFor="default-client">Use Nylas as my default mail client</label>
</div>
<div className="container" style={maxWidth:600}>
<div className="section-header platform-darwin-only">
<input type="checkbox" id="show-system-tray"
checked={@props.config.get('core.showSystemTray')}
onChange={@toggleShowSystemTrayIcon}/>
<label htmlFor="show-system-tray">Show N1 icon in the menu bar</label>
</div>
<WorkspaceSection config={@props.config} configSchema={@props.configSchema} />
{@_renderImportanceOptionElement()}
<ConfigSchemaItem
configSchema={@props.configSchema.properties.notifications}
keyName="Notifications"
keyPath="core.notifications"
config={@props.config} />
<div className="section-header">
<input type="checkbox" id="autoload-images"
checked={@props.config.get('core.reading.autoloadImages')}
onChange={@toggleAutoloadImages}/>
<label htmlFor="autoload-images">Automatically load images in viewed messages</label>
</div>
<ConfigSchemaItem
configSchema={@props.configSchema.properties.reading}
keyName="Reading"
keyPath="core.reading"
config={@props.config} />
<div className="section-header" style={marginTop:30}>
Delay for marking messages as read:
<select value={@props.config.get('core.reading.markAsReadDelay')}
onChange={ (event) => @props.config.set('core.reading.markAsReadDelay', event.target.value) }>
<option value={0}>Instantly</option>
<option value={500}>½ Second</option>
<option value={2000}>2 Seconds</option>
</select>
</div>
<ConfigSchemaItem
configSchema={@props.configSchema.properties.sending}
keyName="Sending"
keyPath="core.sending"
config={@props.config} />
<div className="section-header">
Download attachments for new mail:
<select value={@props.config.get('core.attachments.downloadPolicy')}
onChange={ (event) => @props.config.set('core.attachments.downloadPolicy', event.target.value) }>
<option value="on-receive">When Received</option>
<option value="on-read">When Reading</option>
<option value="manually">Manually</option>
</select>
</div>
<ConfigSchemaItem
configSchema={@props.configSchema.properties.attachments}
keyName="Attachments"
keyPath="core.attachments"
config={@props.config} />
<div className="section-header">
Default reply behavior:
<div style={float:'right', width:138}>
<input type="radio"
id="core.sending.defaultReplyType.reply"
checked={@props.config.get('core.sending.defaultReplyType') == 'reply'}
onChange={ => @props.config.set('core.sending.defaultReplyType', 'reply') }/>
<label htmlFor="core.sending.defaultReplyType.reply">Reply</label>
<br/>
<input type="radio"
id="core.sending.defaultReplyType.replyAll"
checked={@props.config.get('core.sending.defaultReplyType') == 'reply-all'}
onChange={ => @props.config.set('core.sending.defaultReplyType', 'reply-all') }/>
<label htmlFor="core.sending.defaultReplyType.replyAll">Reply all</label>
</div>
</div>
</div>
</div>
module.exports = PreferencesGeneral

View file

@ -49,24 +49,28 @@ class PreferencesKeymaps extends React.Component
render: =>
<div className="container-keymaps">
<Flexbox className="shortcut shortcut-select">
<div className="shortcut-name">Keyboard shortcut set:</div>
<div className="shortcut-value">
<select
style={margin:0}
value={@props.config.get('core.keymapTemplate')}
onChange={ (event) => @props.config.set('core.keymapTemplate', event.target.value) }>
{ @state.templates.map (template) =>
<option key={template} value={template}>{template}</option>
}
</select>
</div>
</Flexbox>
{@_renderBindings()}
<div className="shortcuts-extras">
<section>
<h2>Shortcuts</h2>
<Flexbox className="shortcut shortcut-select">
<div className="shortcut-name">Keyboard shortcut set:</div>
<div className="shortcut-value">
<select
style={margin:0}
value={@props.config.get('core.keymapTemplate')}
onChange={ (event) => @props.config.set('core.keymapTemplate', event.target.value) }>
{ @state.templates.map (template) =>
<option key={template} value={template}>{template}</option>
}
</select>
</div>
</Flexbox>
{@_renderBindings()}
</section>
<section>
<h2>Customization</h2>
<p>Define additional shortcuts by adding them to your shortcuts file.</p>
<button className="btn" onClick={@_onShowUserKeymaps}>Edit custom shortcuts</button>
</div>
</section>
</div>
_renderBindingFor: ([command, label]) =>

View file

@ -1,56 +0,0 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesNotifications extends React.Component
@displayName: 'PreferencesNotifications'
@propTypes:
config: React.PropTypes.object.isRequired
render: =>
<div className="container-notifications">
<div className="section">
<div className="section-header">
Notifications:
</div>
<div className="section-body">
<p className="platform-darwin-only">
<input type="checkbox"
id="core.showUnreadBadge"
checked={@props.config.get('core.showUnreadBadge')}
onChange={ => @props.config.toggle('core.showUnreadBadge')}/>
<label htmlFor="core.showUnreadBadge">Badge dock icon with unread message count</label>
</p>
<p>
<input type="checkbox"
id="core.notifications.enabled"
checked={@props.config.get('core.notifications.enabled')}
onChange={ => @props.config.toggle('core.notifications.enabled')}/>
<label htmlFor="core.notifications.enabled">Show notifications for new unread messages</label>
</p>
</div>
</div>
<div className="section-header">
Sounds:
</div>
<div className="section-body">
<p>
<input type="checkbox"
id="core.notifications.sounds"
checked={@props.config.get('core.notifications.sounds')}
onChange={ => @props.config.toggle('core.notifications.sounds')}/>
<label htmlFor="core.notifications.sounds">Play sound when receiving new mail</label>
</p>
<p>
<input type="checkbox"
id="core.sending.sounds"
checked={@props.config.get('core.sending.sounds')}
onChange={ => @props.config.toggle('core.sending.sounds')}/>
<label htmlFor="core.sending.sounds">Play sound when a message is sent</label>
</p>
</div>
</div>
module.exports = PreferencesNotifications

View file

@ -0,0 +1,122 @@
React = require 'react'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
{LaunchServices, AccountStore} = require 'nylas-exports'
ConfigSchemaItem = require './config-schema-item'
class DefaultMailClientItem extends React.Component
constructor: (@props) ->
@state = {}
@_services = new LaunchServices()
if @_services.available()
@_services.isRegisteredForURLScheme 'mailto', (registered) =>
@setState(defaultClient: registered)
render: =>
return false unless process.platform is 'darwin'
<div className="item">
<input type="checkbox" id="default-client" checked={@state.defaultClient} onChange={@toggleDefaultMailClient}/>
<label htmlFor="default-client">Use Nylas as my default mail client</label>
</div>
toggleDefaultMailClient: =>
if @state.defaultClient is true
@setState(defaultClient: false)
@_services.resetURLScheme('mailto')
else
@setState(defaultClient: true)
@_services.registerForURLScheme('mailto')
class AppearanceModeSwitch extends React.Component
@displayName: 'AppearanceModeSwitch'
@propTypes:
config: React.PropTypes.object.isRequired
constructor: (@props) ->
@state = {
value: @props.config.get('core.workspace.mode')
}
componentWillReceiveProps: (nextProps) ->
@setState({
value: nextProps.config.get('core.workspace.mode')
})
render: ->
hasChanges = @state.value isnt @props.config.get('core.workspace.mode')
applyChangesClass = "btn btn-small"
applyChangesClass += " btn-disabled" unless hasChanges
<div className="appearance-mode-switch">
<Flexbox
direction="row"
style={alignItems: "center"}
className="item">
{@_renderModeOptions()}
</Flexbox>
<div className={applyChangesClass} onClick={@_onApplyChanges}>Apply Changes</div>
</div>
_renderModeOptions: ->
['list', 'split'].map (mode) =>
<AppearanceModeOption
mode={mode}
key={mode}
active={@state.value is mode}
onClick={ => @setState(value: mode) } />
_onApplyChanges: =>
@props.config.set('core.workspace.mode', @state.value)
class AppearanceModeOption extends React.Component
@propTypes:
mode: React.PropTypes.string.isRequired
active: React.PropTypes.bool
onClick: React.PropTypes.func
constructor: (@props) ->
render: =>
classname = "appearance-mode"
classname += " active" if @props.active
<div className={classname} onClick={@props.onClick}>
<RetinaImg name={"appearance-mode-#{@props.mode}.png"} mode={RetinaImg.Mode.ContentIsMask}/>
<div>{@props.mode} View</div>
</div>
class WorkspaceSection extends React.Component
@displayName: 'WorkspaceSection'
@propTypes:
config: React.PropTypes.object
configSchema: React.PropTypes.object
render: =>
<section>
<h2>Workspace</h2>
<DefaultMailClientItem />
<ConfigSchemaItem
configSchema={@props.configSchema.properties.workspace.properties.systemTray}
keyPath="core.workspace.systemTray"
config={@props.config} />
<ConfigSchemaItem
configSchema={@props.configSchema.properties.workspace.properties.showImportant}
keyPath="core.workspace.showImportant"
config={@props.config} />
<div className="item">
<input type="checkbox"
id="dark"
checked={@props.config.contains('core.themes','ui-dark')}
onChange={ => @props.config.toggleContains('core.themes', 'ui-dark')}
/>
<label htmlFor="dark">Use dark color scheme</label>
</div>
<h2>Layout</h2>
<AppearanceModeSwitch config={@props.config} />
</section>
module.exports = WorkspaceSection

View file

@ -1,61 +1,56 @@
@import "ui-variables";
@import "ui-mixins";
body.window-type-react-remote {
margin:0;
.sheet-toolbar {
border-bottom: none;
height:30px;
min-height:30px;
max-height:30px;
display: none;
.toolbar-window-controls {
margin-top:3px;
margin-left:3px;
}
.window-title {
line-height:30px;
}
}
.sheet {
background: @background-secondary;
}
}
body.platform-darwin.window-type-react-remote {
.sheet-toolbar {
display:inherit;
}
}
// Preferences Specific
body.is-blurred {
.preference-header {
background-image: -webkit-linear-gradient(top, lighten(@toolbar-background-color, 14%), lighten(@toolbar-background-color, 14%));
}
}
body.platform-darwin {
.preferences-wrap .platform-darwin-only {
display: inherit;
}
}
.preferences-wrap {
input[type=checkbox] {margin: 0 7px 0 0; position: relative; top: -1px; }
input[type=radio] {margin: 0 7px 0 0; position: relative; top: -1px; }
select { margin: 4px 0 0 8px; }
padding-bottom:50px;
height: 100%;
background-color: @background-primary;
color: @text-color;
.platform-darwin-only {
display: none;
section:first-child h2:first-child {
margin-top:0;
}
section section h2 {
font-size:120%;
}
section {
padding-bottom: @padding-base-vertical;
.item {
padding-top: @padding-small-vertical;
padding-bottom: @padding-small-vertical;
}
}
.preferences-sidebar {
background: @background-secondary;
border-right: 1px solid @border-color-divider;
flex: 1;
max-width:350px;
min-width:200px;
height: 100%;
.item {
padding: @padding-large-vertical @padding-large-horizontal;
border-bottom: 1px solid @border-color-divider;
cursor: default;
}
.item.active {
background: @background-primary;
}
}
.preferences-content {
flex: 4;
.scroll-region-content {
padding: @padding-large-vertical*3 @padding-large-horizontal * 3;
}
}
.well {
@ -72,105 +67,68 @@ body.platform-darwin {
}
}
.section {
margin-top:25px;
}
.section-header {
line-height: 2em;
margin-top:7px;
margin-bottom:2px;
cursor: default;
}
.section-body {
padding:5px;
padding-top: 10px;
padding-bottom: 10px;
.section-body-list {
flex: 1;
img { width:100%; padding:15px;}
}
}
.container-signatures {
width:95%;
margin-left: 2.5%;
margin-right: 2.5%;
.contenteditable-container {
border: 1px solid @input-border-color;
padding: 10px;
margin-top: 20px;
min-height: 200px;
}
.contenteditable-container {
border: 1px solid @input-border-color;
padding: 10px;
margin-top: 20px;
.section-body {
padding: 10px 0 0 0;
.menu {
border: solid thin #CCC;
margin-right: 5px;
min-height: 200px;
.menu-items {
margin:0;
padding:0;
list-style: none;
li { padding: 6px; }
}
}
.menu-horizontal {
height: 100%;
.menu-items {
height:100%;
margin: 0;
padding: 0;
list-style: none;
li {
text-align:center;
width:40px;
display:inline-block;
padding:8px 16px 8px 16px;
border-right: solid thin #CCC;
}
}
}
.signature-area {
border: solid thin #CCC;
min-height: 200px;
}
.section-body {
padding: 10px 0 0 0;
.menu {
border: solid thin #CCC;
margin-right: 5px;
min-height: 200px;
.menu-items {
margin:0;
padding:0;
list-style: none;
li { padding: 6px; }
}
}
.menu-horizontal {
height: 100%;
.menu-items {
height:100%;
margin: 0;
padding: 0;
list-style: none;
li {
text-align:center;
width:40px;
display:inline-block;
padding:8px 16px 8px 16px;
border-right: solid thin #CCC;
}
}
}
.signature-area {
border: solid thin #CCC;
min-height: 200px;
}
.menu-footer {
border: solid thin #CCC;
overflow: auto;
}
.signature-footer {
border: solid thin #CCC;
overflow: auto;
.edit-html-button {
float: right;
margin: 6px;
}
}
.menu-footer {
border: solid thin #CCC;
overflow: auto;
}
.signature-footer {
border: solid thin #CCC;
overflow: auto;
}
.container-notifications {
width: 75%;
margin-left: auto;
margin-right: auto;
}
.container-appearance {
width:70%;
margin: auto;
.section-appearance {
padding-left: 0;
.edit-html-button {
float: right;
margin: 6px;
}
}
}
}
.appearance-mode-switch {
max-width:400px;
text-align: right;
.appearance-mode {
background-color: @background-off-primary;;
@ -181,6 +139,7 @@ body.platform-darwin {
padding:25px;
padding-bottom:9px;
margin-right:10px;
margin-bottom:7px;
margin-top:0;
img {
background-color: @background-tertiary;
@ -190,6 +149,9 @@ body.platform-darwin {
text-transform: capitalize;
cursor: default;
}
&:last-child {
margin-right:0;
}
}
.appearance-mode.active {
border:1px solid @component-active-color;
@ -199,129 +161,43 @@ body.platform-darwin {
}
.container-keymaps {
width:75%;
margin-left:12.5%;
margin-right:12.5%;
.shortcut {
padding: 3px 0;
&.shortcut-select {
padding: 5px 0 20px 0;
select {
width: 75%;
}
}
.shortcut-name {
text-align: right;
flex: 1;
margin-right: 20px;
}
.shortcut-value {
text-align: left;
flex: 1;
}
.shortcut {
padding: 3px 0;
&.shortcut-select {
padding: 5px 0 20px 0;
select {
width: 75%;
}
}
.shortcuts-extras {
text-align:center;
margin-top: 20px;
}
.shortcut-name {
text-align: right;
flex: 1;
margin-right: 20px;
}
.shortcut-value {
text-align: left;
flex: 1;
}
}
}
.container-accounts {
margin-left:20px;
margin-right:20px;
.account-name {
font-size: @font-size-large;
cursor:default;
overflow: hidden;
text-overflow: ellipsis;
font-size: @font-size-large;
cursor:default;
overflow: hidden;
text-overflow: ellipsis;
}
.account-subtext {
font-size: @font-size-small;
cursor:default;
font-size: @font-size-small;
cursor:default;
}
}
.preference-header {
margin-bottom:15px;
border-bottom: 1px solid darken(@toolbar-background-color, 9%);
background-image: -webkit-linear-gradient(top, @toolbar-background-color, darken(@toolbar-background-color, 2%));
cursor: default;
-webkit-user-select: none;
-webkit-app-region: drag;
.preference-header-item {
margin-left:3px;
margin-right:3px;
padding-left:3px;
padding-right:3px;
-webkit-app-region: no-drag;
display: inline-block;
.phi-container {
padding-left:5px;
padding-right:5px;
padding-top: 3px;
text-align: center;
height:58px;
.icon {
height:33px;
}
.name {
font-size:12px;
line-height:22px;
white-space:nowrap;
}
}
&.active {
background-color:rgba(0,0,0, 0.12);
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
}
}
}
body.platform-win32.is-blurred {
.preference-header {
background: lighten(@toolbar-background-color, 14%);
}
}
body.platform-win32 {
.preference-header {
background: #f2f2f2;
.preference-header-item {
margin-left: 0;
margin-right: 1px;
width: 80px;
padding-top: 3px;
.windows-btn-bg;
height: 63px;
color: @text-color;
.icon img.content-mask { background: @text-color; }
&:hover {
background: #e5e5e5;
}
&.active {
background: #f2f2f2;
border-radius: 0 0 0 0;
border-bottom: 2px solid @accent-primary;
color: @accent-primary;
.icon img.content-mask { background: @accent-primary; }
}
}
}
.preferences-wrap {
.well {
border-radius: 0;

View file

@ -21,8 +21,8 @@ export function activate() {
}
};
unsubConfig = NylasEnv.config.onDidChange('core.showSystemTray', onSystemTrayToggle).dispose;
if (NylasEnv.config.get('core.showSystemTray')) {
unsubConfig = NylasEnv.config.onDidChange('core.workspace.systemTray', onSystemTrayToggle).dispose;
if (NylasEnv.config.get('core.workspace.systemTray')) {
systemTray = new SystemTray(platform);
}
}

View file

@ -42,6 +42,9 @@ class ConfigPropContainer extends React.Component
}
render: =>
React.cloneElement(@props.children, {config: @state.config})
React.cloneElement(@props.children, {
config: @state.config,
configSchema: NylasEnv.config.getSchema('core')
})
module.exports = ConfigPropContainer

View file

@ -9,15 +9,15 @@ module.exports =
type: 'string'
default: 'list'
enum: ['split', 'list']
showUnreadBadge:
type: 'boolean'
default: true
showImportant:
type: 'boolean'
default: true
showSystemTray:
type: 'boolean'
default: true
systemTray:
type: 'boolean'
default: true
title: "Show icon in Mac OS X menu bar"
platforms: ['darwin']
showImportant:
type: 'boolean'
default: true
title: "Show Gmail-style important markers (Gmail Only)"
disabledPackages:
type: 'array'
default: []
@ -38,31 +38,47 @@ module.exports =
type: 'string'
default: 'on-read'
enum: ['on-receive', 'on-read', 'manually']
enumLabels: ['When Received', 'When Read', 'Manually']
title: "Download attachments for new mail"
reading:
type: 'object'
properties:
markAsReadDelay:
type: 'integer'
default: 500
enum: [0, 500, 2000]
enumLabels: ['Instantly', '½ Second', '2 Seconds']
title: "Delay for marking messages as read"
autoloadImages:
type: 'boolean'
default: true
title: "Automatically load images in viewed messages"
sending:
type: 'object'
properties:
sounds:
type: 'boolean'
default: true
title: "Play sound when a message is sent"
defaultReplyType:
type: 'string'
default: 'reply-all'
enum: ['reply', 'reply-all']
enumLabels: ['Reply', 'Reply All']
title: "Default reply behavior"
notifications:
type: 'object'
properties:
enabled:
type: 'boolean'
default: true
title: "Show notifications for new unread messages"
sounds:
type: 'boolean'
default: true
title: "Play sound when receiving new mail"
unreadBadge:
type: 'boolean'
default: true
title: "Show badge on the app icon"
platforms: ['darwin']

View file

@ -126,19 +126,18 @@ class Actions
@retryInitialSync: ActionScopeWorkWindow
###
Public: Open the preferences window. Pass an object with a tab name
(ie: `{tab: 'Accounts'}`) to open a specific panel.
Public: Open the preferences view.
*Scope: Window*
###
@openPreferences: ActionScopeWindow
###
Public: Register a preferences tab, usually applied in Preferences window
Public: Switch to the preferences tab with the specific name
*Scope: Window*
###
@registerPreferencesTab: ActionScopeWindow
@switchPreferencesSection: ActionScopeWindow
###
Public: Clear the developer console for the current window.

View file

@ -1,5 +1,6 @@
_ = require 'underscore'
NylasStore = require 'nylas-store'
Actions = require '../actions'
class SectionConfig
constructor: (opts={}) ->
@ -20,21 +21,21 @@ class SectionConfig
class PreferencesSectionStore extends NylasStore
constructor: ->
@_sectionConfigs = []
@_activeSectionId = null
@_accumulateAndTrigger ?= _.debounce(( => @trigger()), 20)
@Section = {}
@SectionConfig = SectionConfig
@listenTo Actions.switchPreferencesSection, (sectionName) =>
@_activeSectionId = sectionName
@trigger()
sections: =>
@_sectionConfigs
# TODO: Use our <GeneratedForm /> Class
# TODO: Add in a "richtext" input type in addition to standard input
# types.
registerPreferences: (packageId, config) ->
throw new Error("Not implemented yet")
unregisterPreferences: (packageId) ->
throw new Error("Not implemented yet")
activeSectionId: =>
@_activeSectionId
###
Public: Register a new top-level section to preferences
@ -64,6 +65,10 @@ class PreferencesSectionStore extends NylasStore
@Section[sectionConfig.sectionId] = sectionConfig.sectionId
@_sectionConfigs.push(sectionConfig)
@_sectionConfigs = _.sortBy(@_sectionConfigs, "order")
if @_sectionConfigs.length is 1
@_activeSectionId = sectionConfig.sectionId
@_accumulateAndTrigger()
unregisterPreferenceSection: (sectionId) ->

View file

@ -19,7 +19,7 @@ UnreadCountStore = Reflux.createStore
@listenTo AccountStore, @_onAccountChanged
@listenTo DatabaseStore, @_onDataChanged
NylasEnv.config.observe 'core.showUnreadBadge', (val) =>
NylasEnv.config.observe 'core.notifications.unreadBadge', (val) =>
if val is true
@_updateBadgeForCount()
else
@ -75,7 +75,7 @@ UnreadCountStore = Reflux.createStore
_updateBadgeForCount: (count) ->
return unless NylasEnv.isMainWindow()
return if NylasEnv.config.get('core.showUnreadBadge') is false
return if NylasEnv.config.get('core.notifications.unreadBadge') is false
if count > 999
@_setBadge("999+")
else if count > 0

View file

@ -34,6 +34,7 @@ class WorkspaceStore extends NylasStore
@listenTo Actions.toggleWorkspaceLocationHidden, @_onToggleLocationHidden
@listenTo Actions.popSheet, @popSheet
@listenTo Actions.pushSheet, @pushSheet
@listenTo Actions.searchQueryCommitted, @popToRootSheet
@_preferredLayoutMode = NylasEnv.config.get('core.workspace.mode')

View file

@ -157,6 +157,7 @@ body.platform-win32 {
background: -webkit-gradient(linear, left top, left bottom, from(darken(@btn-default-bg-color, 9%)), to(darken(@btn-default-bg-color, 13.5%)));
}
.btn-disabled {
color: fadeout(@btn-default-text-color, 40%);
background: fadeout(@btn-default-bg-color, 15%);
box-shadow: 0 0.5px 0 rgba(0,0,0,0.15), 0 -0.5px 0 rgba(0,0,0,0.15), 0.5px 0 0 rgba(0,0,0,0.15), -0.5px 0 0 rgba(0,0,0,0.15);
&:active {