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) @setState(showing: false)
_onManageAccounts: => _onManageAccounts: =>
Actions.openPreferences({tab: 'Accounts'}) Actions.switchPreferencesSection('Accounts')
Actions.openPreferences()
@setState(showing: false) @setState(showing: false)
_getStateFromStores: => _getStateFromStores: =>

View file

@ -1,10 +1,13 @@
{PreferencesSectionStore} = require 'nylas-exports' {PreferencesSectionStore,
Actions,
WorkspaceStore,
ComponentRegistry} = require 'nylas-exports'
module.exports = module.exports =
activate: (@state={}) ->
activate: ->
ipc = require 'ipc' ipc = require 'ipc'
React = require 'react' React = require 'react'
{Actions} = require('nylas-exports')
Cfg = PreferencesSectionStore.SectionConfig Cfg = PreferencesSectionStore.SectionConfig
@ -29,38 +32,20 @@ module.exports =
component: require './tabs/preferences-keymaps' component: require './tabs/preferences-keymaps'
order: 3 order: 3
}) })
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-notifications.png' WorkspaceStore.defineSheet 'Preferences', {},
sectionId: 'Notifications' split: ['Preferences']
displayName: 'Notifications' list: ['Preferences']
component: require './tabs/preferences-notifications'
order: 4 PreferencesRoot = require('./preferences-root')
}) ComponentRegistry.register PreferencesRoot,
PreferencesSectionStore.registerPreferenceSection(new Cfg { location: WorkspaceStore.Location.Preferences
icon: 'ic-settings-appearance.png'
sectionId: 'Appearance'
displayName: 'Appearance'
component: require './tabs/preferences-appearance'
order: 5
})
Actions.openPreferences.listen(@_openPreferences) Actions.openPreferences.listen(@_openPreferences)
ipc.on 'open-preferences', => @_openPreferences() ipc.on 'open-preferences', => @_openPreferences()
_openPreferences: ({tab} = {}) -> _openPreferences: ->
{ReactRemote} = require('nylas-exports') Actions.pushSheet(WorkspaceStore.Sheet.Preferences)
Preferences = require('./preferences')
ReactRemote.openWindowForComponent(Preferences, {
tag: 'preferences'
title: "Preferences"
width: 520
resizable: false
autosize: true
stylesheetRegex: /(preferences|nylas\-fonts)/
props: {
initialTab: tab
}
})
deactivate: -> 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?() @unsubscribe?()
render: => render: =>
<div className="container-accounts"> <section className="container-accounts">
<h2>Accounts</h2>
{@_renderAccounts()} {@_renderAccounts()}
<div style={textAlign:"right", marginTop: '20'}> <div style={textAlign:"right", marginTop: '20'}>
<button className="btn btn-large" onClick={@_onAddAccount}>Add Account...</button> <button className="btn btn-large" onClick={@_onAddAccount}>Add Account...</button>
</div> </div>
{@_renderLinkedAccounts()} {@_renderLinkedAccounts()}
</div> </section>
_renderAccounts: => _renderAccounts: =>
return false unless @state.accounts return false unless @state.accounts
<div> <div>
<div className="section-header">
Accounts:
</div>
{ @state.accounts.map (account) => { @state.accounts.map (account) =>
<div className="well large" style={marginBottom:10} key={account.id}> <div className="well large" style={marginBottom:10} key={account.id}>
<Flexbox direction="row" style={alignItems: 'middle'}> <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' React = require 'react'
_ = require 'underscore' _ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit' {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 class PreferencesGeneral extends React.Component
@displayName: 'PreferencesGeneral' @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: => render: =>
<div className="container-notifications"> <div className="container" style={maxWidth:600}>
<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="section-header platform-darwin-only"> <WorkspaceSection config={@props.config} configSchema={@props.configSchema} />
<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>
{@_renderImportanceOptionElement()} <ConfigSchemaItem
configSchema={@props.configSchema.properties.notifications}
keyName="Notifications"
keyPath="core.notifications"
config={@props.config} />
<div className="section-header"> <ConfigSchemaItem
<input type="checkbox" id="autoload-images" configSchema={@props.configSchema.properties.reading}
checked={@props.config.get('core.reading.autoloadImages')} keyName="Reading"
onChange={@toggleAutoloadImages}/> keyPath="core.reading"
<label htmlFor="autoload-images">Automatically load images in viewed messages</label> config={@props.config} />
</div>
<div className="section-header" style={marginTop:30}> <ConfigSchemaItem
Delay for marking messages as read: configSchema={@props.configSchema.properties.sending}
<select value={@props.config.get('core.reading.markAsReadDelay')} keyName="Sending"
onChange={ (event) => @props.config.set('core.reading.markAsReadDelay', event.target.value) }> keyPath="core.sending"
<option value={0}>Instantly</option> config={@props.config} />
<option value={500}>½ Second</option>
<option value={2000}>2 Seconds</option>
</select>
</div>
<div className="section-header"> <ConfigSchemaItem
Download attachments for new mail: configSchema={@props.configSchema.properties.attachments}
<select value={@props.config.get('core.attachments.downloadPolicy')} keyName="Attachments"
onChange={ (event) => @props.config.set('core.attachments.downloadPolicy', event.target.value) }> keyPath="core.attachments"
<option value="on-receive">When Received</option> config={@props.config} />
<option value="on-read">When Reading</option>
<option value="manually">Manually</option>
</select>
</div>
<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> </div>
module.exports = PreferencesGeneral module.exports = PreferencesGeneral

View file

@ -49,24 +49,28 @@ class PreferencesKeymaps extends React.Component
render: => render: =>
<div className="container-keymaps"> <div className="container-keymaps">
<Flexbox className="shortcut shortcut-select"> <section>
<div className="shortcut-name">Keyboard shortcut set:</div> <h2>Shortcuts</h2>
<div className="shortcut-value"> <Flexbox className="shortcut shortcut-select">
<select <div className="shortcut-name">Keyboard shortcut set:</div>
style={margin:0} <div className="shortcut-value">
value={@props.config.get('core.keymapTemplate')} <select
onChange={ (event) => @props.config.set('core.keymapTemplate', event.target.value) }> style={margin:0}
{ @state.templates.map (template) => value={@props.config.get('core.keymapTemplate')}
<option key={template} value={template}>{template}</option> onChange={ (event) => @props.config.set('core.keymapTemplate', event.target.value) }>
} { @state.templates.map (template) =>
</select> <option key={template} value={template}>{template}</option>
</div> }
</Flexbox> </select>
{@_renderBindings()} </div>
</Flexbox>
<div className="shortcuts-extras"> {@_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> <button className="btn" onClick={@_onShowUserKeymaps}>Edit custom shortcuts</button>
</div> </section>
</div> </div>
_renderBindingFor: ([command, label]) => _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-variables";
@import "ui-mixins"; @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 // 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 { .preferences-wrap {
input[type=checkbox] {margin: 0 7px 0 0; position: relative; top: -1px; } input[type=checkbox] {margin: 0 7px 0 0; position: relative; top: -1px; }
input[type=radio] {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; } select { margin: 4px 0 0 8px; }
padding-bottom:50px; height: 100%;
background-color: @background-primary; background-color: @background-primary;
color: @text-color; color: @text-color;
.platform-darwin-only { section:first-child h2:first-child {
display: none; 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 { .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 { .container-signatures {
width:95%; .contenteditable-container {
margin-left: 2.5%; border: 1px solid @input-border-color;
margin-right: 2.5%; padding: 10px;
margin-top: 20px;
min-height: 200px;
}
.contenteditable-container { .section-body {
border: 1px solid @input-border-color; padding: 10px 0 0 0;
padding: 10px;
margin-top: 20px; .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; min-height: 200px;
} }
.menu-footer {
.section-body { border: solid thin #CCC;
padding: 10px 0 0 0; overflow: auto;
.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;
}
}
} }
.signature-footer {
border: solid thin #CCC;
overflow: auto;
} .edit-html-button {
float: right;
.container-notifications { margin: 6px;
width: 75%; }
margin-left: auto; }
margin-right: auto;
}
.container-appearance {
width:70%;
margin: auto;
.section-appearance {
padding-left: 0;
} }
}
.appearance-mode-switch {
max-width:400px;
text-align: right;
.appearance-mode { .appearance-mode {
background-color: @background-off-primary;; background-color: @background-off-primary;;
@ -181,6 +139,7 @@ body.platform-darwin {
padding:25px; padding:25px;
padding-bottom:9px; padding-bottom:9px;
margin-right:10px; margin-right:10px;
margin-bottom:7px;
margin-top:0; margin-top:0;
img { img {
background-color: @background-tertiary; background-color: @background-tertiary;
@ -190,6 +149,9 @@ body.platform-darwin {
text-transform: capitalize; text-transform: capitalize;
cursor: default; cursor: default;
} }
&:last-child {
margin-right:0;
}
} }
.appearance-mode.active { .appearance-mode.active {
border:1px solid @component-active-color; border:1px solid @component-active-color;
@ -199,129 +161,43 @@ body.platform-darwin {
} }
.container-keymaps { .container-keymaps {
width:75%; .shortcut {
margin-left:12.5%; padding: 3px 0;
margin-right:12.5%; &.shortcut-select {
padding: 5px 0 20px 0;
.shortcut { select {
padding: 3px 0; width: 75%;
&.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;
}
} }
.shortcuts-extras { .shortcut-name {
text-align:center; text-align: right;
margin-top: 20px; flex: 1;
} margin-right: 20px;
}
.shortcut-value {
text-align: left;
flex: 1;
}
}
} }
.container-accounts { .container-accounts {
margin-left:20px;
margin-right:20px;
.account-name { .account-name {
font-size: @font-size-large; font-size: @font-size-large;
cursor:default; cursor:default;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.account-subtext { .account-subtext {
font-size: @font-size-small; font-size: @font-size-small;
cursor:default; 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 { 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 { .preferences-wrap {
.well { .well {
border-radius: 0; border-radius: 0;

View file

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

View file

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

View file

@ -9,15 +9,15 @@ module.exports =
type: 'string' type: 'string'
default: 'list' default: 'list'
enum: ['split', 'list'] enum: ['split', 'list']
showUnreadBadge: systemTray:
type: 'boolean' type: 'boolean'
default: true default: true
showImportant: title: "Show icon in Mac OS X menu bar"
type: 'boolean' platforms: ['darwin']
default: true showImportant:
showSystemTray: type: 'boolean'
type: 'boolean' default: true
default: true title: "Show Gmail-style important markers (Gmail Only)"
disabledPackages: disabledPackages:
type: 'array' type: 'array'
default: [] default: []
@ -38,31 +38,47 @@ module.exports =
type: 'string' type: 'string'
default: 'on-read' default: 'on-read'
enum: ['on-receive', 'on-read', 'manually'] enum: ['on-receive', 'on-read', 'manually']
enumLabels: ['When Received', 'When Read', 'Manually']
title: "Download attachments for new mail"
reading: reading:
type: 'object' type: 'object'
properties: properties:
markAsReadDelay: markAsReadDelay:
type: 'integer' type: 'integer'
default: 500 default: 500
enum: [0, 500, 2000]
enumLabels: ['Instantly', '½ Second', '2 Seconds']
title: "Delay for marking messages as read"
autoloadImages: autoloadImages:
type: 'boolean' type: 'boolean'
default: true default: true
title: "Automatically load images in viewed messages"
sending: sending:
type: 'object' type: 'object'
properties: properties:
sounds: sounds:
type: 'boolean' type: 'boolean'
default: true default: true
title: "Play sound when a message is sent"
defaultReplyType: defaultReplyType:
type: 'string' type: 'string'
default: 'reply-all' default: 'reply-all'
enum: ['reply', 'reply-all'] enum: ['reply', 'reply-all']
enumLabels: ['Reply', 'Reply All']
title: "Default reply behavior"
notifications: notifications:
type: 'object' type: 'object'
properties: properties:
enabled: enabled:
type: 'boolean' type: 'boolean'
default: true default: true
title: "Show notifications for new unread messages"
sounds: sounds:
type: 'boolean' type: 'boolean'
default: true 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 @retryInitialSync: ActionScopeWorkWindow
### ###
Public: Open the preferences window. Pass an object with a tab name Public: Open the preferences view.
(ie: `{tab: 'Accounts'}`) to open a specific panel.
*Scope: Window* *Scope: Window*
### ###
@openPreferences: ActionScopeWindow @openPreferences: ActionScopeWindow
### ###
Public: Register a preferences tab, usually applied in Preferences window Public: Switch to the preferences tab with the specific name
*Scope: Window* *Scope: Window*
### ###
@registerPreferencesTab: ActionScopeWindow @switchPreferencesSection: ActionScopeWindow
### ###
Public: Clear the developer console for the current window. Public: Clear the developer console for the current window.

View file

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

View file

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

View file

@ -34,6 +34,7 @@ class WorkspaceStore extends NylasStore
@listenTo Actions.toggleWorkspaceLocationHidden, @_onToggleLocationHidden @listenTo Actions.toggleWorkspaceLocationHidden, @_onToggleLocationHidden
@listenTo Actions.popSheet, @popSheet @listenTo Actions.popSheet, @popSheet
@listenTo Actions.pushSheet, @pushSheet
@listenTo Actions.searchQueryCommitted, @popToRootSheet @listenTo Actions.searchQueryCommitted, @popToRootSheet
@_preferredLayoutMode = NylasEnv.config.get('core.workspace.mode') @_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%))); 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 { .btn-disabled {
color: fadeout(@btn-default-text-color, 40%);
background: fadeout(@btn-default-bg-color, 15%); 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); 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 { &:active {