mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-08 05:34:23 +08:00
feat(prefs): Allow tabs with an accounts submenu
Summary: This diff: - Improves the styling of the tabs in the preferences sidebar. - Adds an optional param to section cofnig that puts an "account" submenu beneath the tab item. - Renames preferences "sections" => "tabs", and renames the PreferencesSectionStore to PreferencesUIStore. I think we should include "UI" in more of our stores, and I think "tabs" is a good idea because it's unambigious—there's no way you could confuse it for a "section" of the NylasEnv.config tree or think it deals with actually saving prefs. Test Plan: Inspect visually Reviewers: evan, juan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2296
This commit is contained in:
parent
847ac1f10e
commit
d7d5ed2832
15 changed files with 249 additions and 157 deletions
|
@ -119,7 +119,7 @@ class AccountSwitcher extends React.Component
|
|||
@setState(showing: false)
|
||||
|
||||
_onManageAccounts: =>
|
||||
Actions.switchPreferencesSection('Accounts')
|
||||
Actions.switchPreferencesTab('Accounts')
|
||||
Actions.openPreferences()
|
||||
|
||||
@setState(showing: false)
|
||||
|
|
|
@ -51,31 +51,6 @@
|
|||
|
||||
.item-container {
|
||||
display:flex;
|
||||
|
||||
.disclosure-triangle {
|
||||
flex-shrink: 0;
|
||||
padding:7px;
|
||||
padding-top:12px;
|
||||
width:20px;
|
||||
visibility: hidden;
|
||||
|
||||
div {
|
||||
transform:rotate(90deg);
|
||||
transition: transform 90ms linear;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 7px solid @text-color-very-subtle;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
&.collapsed {
|
||||
div {
|
||||
transform:rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
{PreferencesSectionStore, DraftStore} = require 'nylas-exports'
|
||||
{PreferencesUIStore, DraftStore} = require 'nylas-exports'
|
||||
SignatureDraftExtension = require './signature-draft-extension'
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
DraftStore.registerExtension(SignatureDraftExtension)
|
||||
|
||||
@sectionConfig = new PreferencesSectionStore.SectionConfig
|
||||
@preferencesTab = new PreferencesUIStore.TabItem
|
||||
# TODO: Fix RetinaImg to handle plugin images
|
||||
icon: ->
|
||||
if process.platform is "win32"
|
||||
"nylas://composer-signature/images/ic-settings-signatures-win32@2x.png"
|
||||
else
|
||||
"nylas://composer-signature/images/ic-settings-signatures@2x.png"
|
||||
sectionId: "Signatures"
|
||||
tabId: "Signatures"
|
||||
displayName: "Signatures"
|
||||
component: require "./preferences-signatures"
|
||||
|
||||
# TODO Re-enable when fixed!
|
||||
# PreferencesSectionStore.registerPreferenceSection(@sectionConfig)
|
||||
# PreferencesUIStore.registerPreferencesTab(@preferencesTab)
|
||||
|
||||
## TODO
|
||||
# PreferencesSectionStore.registerPreferences "composer-signatures", [
|
||||
# PreferencesUIStore.registerPreferences "composer-signatures", [
|
||||
# {
|
||||
# section: PreferencesSectionStore.Section.Signatures
|
||||
# section: PreferencesUIStore.Section.Signatures
|
||||
# type: "richtext"
|
||||
# label: "Signature:"
|
||||
# perAccount: true
|
||||
# defaultValue: "- Sent from N1"
|
||||
# }, {
|
||||
# section: PreferencesSectionStore.Section.Signatures
|
||||
# section: PreferencesUIStore.Section.Signatures
|
||||
# type: "toggle"
|
||||
# label: "Include on replies"
|
||||
# defaultValue: true
|
||||
|
@ -37,6 +37,6 @@ module.exports =
|
|||
|
||||
deactivate: ->
|
||||
DraftStore.unregisterExtension(SignatureDraftExtension)
|
||||
PreferencesSectionStore.unregisterPreferenceSection(@sectionConfig.sectionId)
|
||||
PreferencesUIStore.unregisterPreferencesTab(@preferencesTab.sectionId)
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{PreferencesSectionStore,
|
||||
{PreferencesUIStore,
|
||||
Actions,
|
||||
WorkspaceStore,
|
||||
ComponentRegistry} = require 'nylas-exports'
|
||||
|
@ -9,25 +9,22 @@ module.exports =
|
|||
activate: ->
|
||||
React = require 'react'
|
||||
|
||||
Cfg = PreferencesSectionStore.SectionConfig
|
||||
Cfg = PreferencesUIStore.TabItem
|
||||
|
||||
PreferencesSectionStore.registerPreferenceSection(new Cfg {
|
||||
icon: 'ic-settings-general.png'
|
||||
sectionId: 'General'
|
||||
PreferencesUIStore.registerPreferencesTab(new Cfg {
|
||||
tabId: 'General'
|
||||
displayName: 'General'
|
||||
component: require './tabs/preferences-general'
|
||||
order: 1
|
||||
})
|
||||
PreferencesSectionStore.registerPreferenceSection(new Cfg {
|
||||
icon: 'ic-settings-accounts.png'
|
||||
sectionId: 'Accounts'
|
||||
PreferencesUIStore.registerPreferencesTab(new Cfg {
|
||||
tabId: 'Accounts'
|
||||
displayName: 'Accounts'
|
||||
component: require './tabs/preferences-accounts'
|
||||
order: 2
|
||||
})
|
||||
PreferencesSectionStore.registerPreferenceSection(new Cfg {
|
||||
icon: 'ic-settings-shortcuts.png'
|
||||
sectionId: 'Shortcuts'
|
||||
PreferencesUIStore.registerPreferencesTab(new Cfg {
|
||||
tabId: 'Shortcuts'
|
||||
displayName: 'Shortcuts'
|
||||
component: require './tabs/preferences-keymaps'
|
||||
order: 3
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
{RetinaImg, Flexbox, ConfigPropContainer, ScrollRegion} = require 'nylas-component-kit'
|
||||
{PreferencesSectionStore} = require 'nylas-exports'
|
||||
{RetinaImg,
|
||||
Flexbox,
|
||||
ConfigPropContainer,
|
||||
ScrollRegion} = require 'nylas-component-kit'
|
||||
{PreferencesUIStore} = require 'nylas-exports'
|
||||
|
||||
PreferencesSidebar = require './preferences-sidebar'
|
||||
|
||||
|
@ -14,27 +17,28 @@ class PreferencesRoot extends React.Component
|
|||
|
||||
componentDidMount: =>
|
||||
@unlisteners = []
|
||||
@unlisteners.push PreferencesSectionStore.listen =>
|
||||
@unlisteners.push PreferencesUIStore.listen =>
|
||||
@setState(@getStateFromStores())
|
||||
|
||||
componentWillUnmount: =>
|
||||
unlisten() for unlisten in @unlisteners
|
||||
|
||||
getStateFromStores: =>
|
||||
sections: PreferencesSectionStore.sections()
|
||||
activeSectionId: PreferencesSectionStore.activeSectionId()
|
||||
tabs: PreferencesUIStore.tabs()
|
||||
selection: PreferencesUIStore.selection()
|
||||
|
||||
render: =>
|
||||
section = _.find @state.sections, ({sectionId}) => sectionId is @state.activeSectionId
|
||||
tabId = @state.selection.get('tabId')
|
||||
tab = @state.tabs.find (s) => s.tabId is tabId
|
||||
|
||||
if section
|
||||
bodyElement = <section.component />
|
||||
if tab
|
||||
bodyElement = <tab.component accountId={@state.selection.get('accountId')} />
|
||||
else
|
||||
bodyElement = <div>No Section Active</div>
|
||||
bodyElement = <div></div>
|
||||
|
||||
<Flexbox direction="row" className="preferences-wrap">
|
||||
<PreferencesSidebar sections={@state.sections}
|
||||
activeSectionId={@state.activeSectionId} />
|
||||
<PreferencesSidebar tabs={@state.tabs}
|
||||
selection={@state.selection} />
|
||||
<ScrollRegion className="preferences-content">
|
||||
<ConfigPropContainer>{bodyElement}</ConfigPropContainer>
|
||||
</ScrollRegion>
|
||||
|
|
|
@ -1,30 +1,96 @@
|
|||
React = require 'react'
|
||||
Immutable = require 'immutable'
|
||||
_ = require 'underscore'
|
||||
{RetinaImg, Flexbox} = require 'nylas-component-kit'
|
||||
{Actions} = require 'nylas-exports'
|
||||
classNames = require 'classnames'
|
||||
{RetinaImg, Flexbox, DisclosureTriangle} = require 'nylas-component-kit'
|
||||
{Actions, AccountStore} = require 'nylas-exports'
|
||||
{PreferencesUIStore} = require 'nylas-exports'
|
||||
|
||||
class PreferencesSidebarItem extends React.Component
|
||||
@displayName: 'PreferencesSidebarItem'
|
||||
@propTypes:
|
||||
accounts: React.PropTypes.array
|
||||
selection: React.PropTypes.instanceOf(Immutable.Map).isRequired
|
||||
tabItem: React.PropTypes.instanceOf(PreferencesUIStore.TabItem).isRequired
|
||||
|
||||
constructor: ->
|
||||
@state =
|
||||
collapsed: true
|
||||
|
||||
render: =>
|
||||
{tabId, displayName, componentRequiresAccount} = @props.tabItem
|
||||
|
||||
subitems = @_renderSubitems()
|
||||
subitemsComponent = <ul className="subitems">{subitems}</ul>
|
||||
if @state.collapsed
|
||||
subitemsComponent = false
|
||||
|
||||
classes = classNames
|
||||
"item": true
|
||||
"active": tabId is @props.selection.get('tabId')
|
||||
"has-subitems": subitems isnt false
|
||||
|
||||
<div key={tabId} className={classes} onClick={@_onClick}>
|
||||
<DisclosureTriangle
|
||||
collapsed={@state.collapsed}
|
||||
visible={subitems isnt false}
|
||||
onToggleCollapsed={@_onClick} />
|
||||
<div className="name">{displayName}</div>
|
||||
{subitemsComponent}
|
||||
</div>
|
||||
|
||||
_renderSubitems: =>
|
||||
if @props.tabItem.componentRequiresAccount
|
||||
@props.accounts.map (account) =>
|
||||
classes = classNames
|
||||
"subitem": true
|
||||
"active": account.id is @props.selection.get('accountId')
|
||||
|
||||
<li className={classes} onClick={ (event) => @_onClickAccount(event, account.id)}>
|
||||
{account.emailAddress}
|
||||
</li>
|
||||
else
|
||||
return false
|
||||
|
||||
_onClick: =>
|
||||
if @props.tabItem.componentRequiresAccount
|
||||
@setState(collapsed: !@state.collapsed)
|
||||
else
|
||||
Actions.switchPreferencesTab(@props.tabItem.tabId)
|
||||
|
||||
_onClickAccount: (event, accountId) =>
|
||||
Actions.switchPreferencesTab(@props.tabItem.tabId, {accountId})
|
||||
event.stopPropagation()
|
||||
|
||||
|
||||
class PreferencesSidebar extends React.Component
|
||||
@displayName: 'PreferencesSidebar'
|
||||
|
||||
@propTypes:
|
||||
sections: React.PropTypes.array.isRequired
|
||||
activeSectionId: React.PropTypes.string
|
||||
tabs: React.PropTypes.instanceOf(Immutable.List).isRequired
|
||||
selection: React.PropTypes.instanceOf(Immutable.Map).isRequired
|
||||
|
||||
constructor: ->
|
||||
@state =
|
||||
accounts: AccountStore.items()
|
||||
|
||||
componentDidMount: =>
|
||||
@unsub = AccountStore.listen @_onAccountsChanged
|
||||
|
||||
componentWillUnmount: =>
|
||||
@unsub?()
|
||||
|
||||
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>
|
||||
{ @props.tabs.map (tabItem) =>
|
||||
<PreferencesSidebarItem
|
||||
tabItem={tabItem}
|
||||
accounts={@state.accounts}
|
||||
selection={@props.selection} />
|
||||
}
|
||||
</div>
|
||||
|
||||
_onAccountsChanged: =>
|
||||
@setState(accounts: AccountStore.items())
|
||||
|
||||
module.exports = PreferencesSidebar
|
||||
|
|
|
@ -7,6 +7,7 @@ class PreferencesMailRules extends React.Component
|
|||
|
||||
render: =>
|
||||
<div className="container-mail-rules">
|
||||
{@props.accountId}
|
||||
</div>
|
||||
|
||||
module.exports = PreferencesMailRules
|
||||
|
|
|
@ -37,12 +37,48 @@
|
|||
height: 100%;
|
||||
|
||||
.item {
|
||||
padding: @padding-large-vertical @padding-large-horizontal;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
cursor: default;
|
||||
}
|
||||
.item.active {
|
||||
background: @background-primary;
|
||||
|
||||
&.active {
|
||||
background: @background-primary;
|
||||
|
||||
&:not(.has-subitems) {
|
||||
border-right:3px solid @component-active-color;
|
||||
.name {
|
||||
color: @component-active-color;
|
||||
}
|
||||
}
|
||||
.subitem.active {
|
||||
background: @background-primary;
|
||||
border-right:3px solid @component-active-color;
|
||||
color: @component-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
.disclosure-triangle {
|
||||
float: left;
|
||||
padding-top:16px;
|
||||
}
|
||||
|
||||
.name {
|
||||
padding: @padding-large-vertical @padding-large-horizontal;
|
||||
}
|
||||
|
||||
.subitems {
|
||||
padding-left: 0;
|
||||
box-shadow: inset 0 2px 1px rgba(0, 0, 0, 0.15);
|
||||
background: darken(@background-secondary, 3%);
|
||||
list-style-type: none;
|
||||
font-size: 0.95em;
|
||||
margin: 0;
|
||||
|
||||
.subitem {
|
||||
padding: @padding-large-vertical * 0.8 @padding-large-horizontal + 4;
|
||||
border-top: 1px solid @border-color-divider;
|
||||
border-right:3px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"fstream": "0.1.24",
|
||||
"grim": "1.5.0",
|
||||
"guid": "0.0.10",
|
||||
"immutable": "3.7.5",
|
||||
"inflection": "^1.7",
|
||||
"jasmine-json": "~0.0",
|
||||
"jasmine-tagged": "^1.1.2",
|
||||
|
|
|
@ -137,7 +137,7 @@ class Actions
|
|||
|
||||
*Scope: Window*
|
||||
###
|
||||
@switchPreferencesSection: ActionScopeWindow
|
||||
@switchPreferencesTab: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Clear the developer console for the current window.
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
NylasStore = require 'nylas-store'
|
||||
Actions = require '../actions'
|
||||
|
||||
class SectionConfig
|
||||
constructor: (opts={}) ->
|
||||
opts.order ?= Infinity
|
||||
_.extend(@, opts)
|
||||
|
||||
nameOrUrl: ->
|
||||
if _.isFunction(@icon)
|
||||
icon = @icon()
|
||||
else
|
||||
icon = @icon
|
||||
|
||||
if icon.indexOf("nylas://") is 0
|
||||
return {url: icon}
|
||||
else
|
||||
return {name: icon}
|
||||
|
||||
class PreferencesSectionStore extends NylasStore
|
||||
constructor: ->
|
||||
@_sectionConfigs = []
|
||||
@_activeSectionId = null
|
||||
@_accumulateAndTrigger ?= _.debounce(( => @trigger()), 20)
|
||||
|
||||
@Section = {}
|
||||
@SectionConfig = SectionConfig
|
||||
|
||||
@listenTo Actions.switchPreferencesSection, (sectionName) =>
|
||||
@_activeSectionId = sectionName
|
||||
@trigger()
|
||||
|
||||
sections: =>
|
||||
@_sectionConfigs
|
||||
|
||||
activeSectionId: =>
|
||||
@_activeSectionId
|
||||
|
||||
###
|
||||
Public: Register a new top-level section to preferences
|
||||
|
||||
- `sectionConfig` a `PreferencesSectionStore.SectionConfig` object
|
||||
- `icon` A `nylas://` url or image name. Can be a function that
|
||||
resolves to one of these
|
||||
schema definitions on the PreferencesSectionStore.Section.MySectionId
|
||||
- `sectionId` A unique name to access the Section by
|
||||
- `displayName` The display name. This may go through i18n.
|
||||
- `component` The Preference section's React Component.
|
||||
|
||||
Most Preference sections include an area where a {PreferencesForm} is
|
||||
rendered. This is a type of {GeneratedForm} that uses the schema passed
|
||||
into {PreferencesSectionStore::registerPreferences}
|
||||
|
||||
Note that `icon` gets passed into the `url` field of a {RetinaImg}. This
|
||||
will, in an ideal case, expect to find the following images:
|
||||
|
||||
- my-icon-darwin@1x.png
|
||||
- my-icon-darwin@2x.png
|
||||
- my-icon-win32@1x.png
|
||||
- my-icon-win32@2x.png
|
||||
|
||||
###
|
||||
registerPreferenceSection: (sectionConfig) ->
|
||||
@Section[sectionConfig.sectionId] = sectionConfig.sectionId
|
||||
@_sectionConfigs.push(sectionConfig)
|
||||
@_sectionConfigs = _.sortBy(@_sectionConfigs, "order")
|
||||
|
||||
if @_sectionConfigs.length is 1
|
||||
@_activeSectionId = sectionConfig.sectionId
|
||||
|
||||
@_accumulateAndTrigger()
|
||||
|
||||
unregisterPreferenceSection: (sectionId) ->
|
||||
delete @Section[sectionId]
|
||||
@_sectionConfigs = _.reject @_sectionConfigs, (sectionConfig) ->
|
||||
sectionConfig.sectionId is sectionId
|
||||
@_accumulateAndTrigger()
|
||||
|
||||
module.exports = new PreferencesSectionStore()
|
64
src/flux/stores/preferences-ui-store.coffee
Normal file
64
src/flux/stores/preferences-ui-store.coffee
Normal file
|
@ -0,0 +1,64 @@
|
|||
_ = require 'underscore'
|
||||
NylasStore = require 'nylas-store'
|
||||
AccountStore = require './account-store'
|
||||
Actions = require '../actions'
|
||||
Immutable = require 'immutable'
|
||||
|
||||
class TabItem
|
||||
constructor: (opts={}) ->
|
||||
opts.order ?= Infinity
|
||||
_.extend(@, opts)
|
||||
|
||||
class PreferencesUIStore extends NylasStore
|
||||
constructor: ->
|
||||
@_tabs = Immutable.List()
|
||||
@_selection = Immutable.Map({
|
||||
tabId: null
|
||||
accountId: AccountStore.current()?.id
|
||||
})
|
||||
|
||||
@_triggerDebounced ?= _.debounce(( => @trigger()), 20)
|
||||
|
||||
@listenTo AccountStore, =>
|
||||
@_selection = @_selection.set('accountId', AccountStore.current()?.id)
|
||||
@trigger()
|
||||
|
||||
@listenTo Actions.switchPreferencesTab, (tabId, options = {}) =>
|
||||
@_selection = @_selection.set('tabId', tabId)
|
||||
if options.accountId
|
||||
@_selection = @_selection.set('accountId', options.accountId)
|
||||
@trigger()
|
||||
|
||||
tabs: =>
|
||||
@_tabs
|
||||
|
||||
selection: =>
|
||||
@_selection
|
||||
|
||||
###
|
||||
Public: Register a new top-level section to preferences
|
||||
|
||||
- `tabItem` a `PreferencesUIStore.TabItem` object
|
||||
schema definitions on the PreferencesUIStore.Section.MySectionId
|
||||
- `tabId` A unique name to access the Section by
|
||||
- `displayName` The display name. This may go through i18n.
|
||||
- `component` The Preference section's React Component.
|
||||
|
||||
Most Preference sections include an area where a {PreferencesForm} is
|
||||
rendered. This is a type of {GeneratedForm} that uses the schema passed
|
||||
into {PreferencesUIStore::registerPreferences}
|
||||
|
||||
###
|
||||
registerPreferencesTab: (tabItem) ->
|
||||
@_tabs = @_tabs.push(tabItem).sort (a, b) =>
|
||||
a.order > b.order
|
||||
if @_tabs.size is 1
|
||||
@_selection = @_selection.set('tabId', tabItem.tabId)
|
||||
@_triggerDebounced()
|
||||
|
||||
unregisterPreferencesTab: (tabId) ->
|
||||
@_tabs = @_tabs.filter (s) => s.tabId isnt tabId
|
||||
@_triggerDebounced()
|
||||
|
||||
module.exports = new PreferencesUIStore()
|
||||
module.exports.TabItem = TabItem
|
|
@ -114,7 +114,7 @@ class NylasExports
|
|||
@require "FocusedContactsStore", 'flux/stores/focused-contacts-store'
|
||||
@require "MessageBodyProcessor", 'flux/stores/message-body-processor'
|
||||
@require "MessageStoreExtension", 'flux/stores/message-store-extension'
|
||||
@require "PreferencesSectionStore", 'flux/stores/preferences-section-store'
|
||||
@require "PreferencesUIStore", 'flux/stores/preferences-ui-store'
|
||||
|
||||
# React Components
|
||||
@get "React", -> require 'react' # Our version of React for 3rd party use
|
||||
|
|
27
static/components/disclosure-triangle.less
Normal file
27
static/components/disclosure-triangle.less
Normal file
|
@ -0,0 +1,27 @@
|
|||
@import "ui-variables";
|
||||
@import "ui-mixins";
|
||||
|
||||
.disclosure-triangle {
|
||||
flex-shrink: 0;
|
||||
padding:7px;
|
||||
padding-top:12px;
|
||||
width:20px;
|
||||
visibility: hidden;
|
||||
|
||||
div {
|
||||
transform:rotate(90deg);
|
||||
transition: transform 90ms linear;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 7px solid @text-color-very-subtle;
|
||||
}
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
&.collapsed {
|
||||
div {
|
||||
transform:rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@
|
|||
@import "components/tokenizing-text-field";
|
||||
@import "components/extra";
|
||||
@import "components/list-tabular";
|
||||
@import "components/disclosure-triangle";
|
||||
@import "components/button-dropdown";
|
||||
@import "components/scroll-region";
|
||||
@import "components/spinner";
|
||||
|
|
Loading…
Add table
Reference in a new issue