Add account switcher back to sidebar:

- Account switcher can now switch between all accounts and each account
- Updates FocusedPerspectiveStore and Actions.focusDefaultMailboxPerspectiveForAccounts
  to focus a perspective for accountIds instead of for a single account,
  and updates methods
  - Adds helpers to CategoryStore and MailboxPerspective
  - Updates key commands to allow switch to unified inbox
This commit is contained in:
Juan Tejada 2016-01-19 23:42:50 -08:00
parent 38590139bc
commit b538ec050c
12 changed files with 207 additions and 178 deletions

View file

@ -1,6 +1,7 @@
_ = require 'underscore'
React = require 'react'
{OutlineView, ScrollRegion} = require 'nylas-component-kit'
AccountSwitcher = require './account-switcher'
SidebarStore = require '../sidebar-store'
@ -26,23 +27,27 @@ class AccountSidebar extends React.Component
@setState @_getStateFromStores()
_getStateFromStores: =>
standardSection: SidebarStore.standardSection()
accounts: SidebarStore.accounts()
focusedAccounts: SidebarStore.focusedAccounts()
userSections: SidebarStore.userSections()
standardSection: SidebarStore.standardSection()
_renderUserSections: (sections) =>
sections.map (section) =>
<OutlineView key={section.title} {...section} />
render: =>
standardSection = @state.standardSection
userSections = @state.userSections
{accounts, focusedAccounts, userSections, standardSection} = @state
<ScrollRegion className="account-sidebar" >
<div className="account-sidebar-sections">
<OutlineView {...standardSection} />
{@_renderUserSections(userSections)}
</div>
</ScrollRegion>
<div style={height: '100%'}>
<AccountSwitcher accounts={accounts} focusedAccounts={focusedAccounts} />
<ScrollRegion className="account-sidebar" >
<div className="account-sidebar-sections">
<OutlineView {...standardSection} />
{@_renderUserSections(userSections)}
</div>
</ScrollRegion>
</div>
module.exports = AccountSidebar

View file

@ -1,10 +1,14 @@
React = require 'react'
SidebarStore = require '../sidebar-store'
{Actions, AccountStore} = require("nylas-exports")
{Actions} = require("nylas-exports")
{RetinaImg} = require 'nylas-component-kit'
crypto = require 'crypto'
classNames = require 'classnames'
ItemTypes = {
"Unified"
}
class AccountSwitcher extends React.Component
@displayName: 'AccountSwitcher'
@ -13,62 +17,69 @@ class AccountSwitcher extends React.Component
minWidth: 165
maxWidth: 210
@propTypes:
accounts: React.PropTypes.array.isRequired
focusedAccounts: React.PropTypes.array.isRequired
constructor: (@props) ->
@state = @_getStateFromStores()
@state.showing = false
@state =
showing: false
componentDidMount: =>
@unsubscribers = []
@unsubscribers.push AccountStore.listen @_onStoreChange
# Helpers
componentWillUnmount: =>
unsubscribe() for unsubscribe in @unsubscribers
_makeItem: ({id, label, emailAddress, provider} = {}) =>
id ?= ItemTypes.Unified
label ?= "All Accounts"
email = emailAddress ? ""
iconName = provider ? 'imap'
accounts = if id is ItemTypes.Unified
@props.accounts
else
[id]
render: =>
return false unless @state.account
return {id, label, email, iconName, accounts}
classnames = ""
classnames += "open" if @state.showing
_selectedItem: =>
if @props.focusedAccounts.length > 1
@_makeItem()
else
@_makeItem(@props.focusedAccounts[0])
<div id="account-switcher"
tabIndex={-1}
onBlur={@_onBlur}
ref="button"
className={classnames}>
{@_renderPrimaryItem()}
{@_renderDropdown()}
</div>
_toggleDropdown: =>
@setState showing: !@state.showing
_renderPrimaryItem: =>
label = @state.account.label.trim()
<div className="item primary-item" onClick={@_toggleDropdown}>
{@_renderGravatarForAccount(@state.account)}
<div style={float: 'right', marginTop: -2}>
<RetinaImg className="toggle"
name="account-switcher-dropdown.png"
mode={RetinaImg.Mode.ContentDark} />
</div>
<div className="name" style={lineHeight: "110%"}>
{label}
</div>
<div style={clear: "both"}></div>
</div>
_renderAccount: (account) =>
email = account.emailAddress.trim().toLowerCase()
label = account.label.trim()
# Handlers
_onBlur: (e) =>
target = e.nativeEvent.relatedTarget
if target? and React.findDOMNode(@refs.button).contains(target)
return
@setState(showing: false)
_onSwitchAccount: (item) =>
Actions.focusDefaultMailboxPerspectiveForAccounts(item.accounts)
@setState(showing: false)
_onManageAccounts: =>
Actions.switchPreferencesTab('Accounts')
Actions.openPreferences()
@setState(showing: false)
_renderItem: (item) =>
classes = classNames
"active": account is @state.account
"active": item.id is @_selectedItem().id
"item": true
"secondary-item": true
<div className={classes} onClick={ => @_onSwitchAccount(account)} key={email}>
{@_renderGravatarForAccount(account)}
<div className="name" style={lineHeight: "110%"}>{label}</div>
<div key={item.email} className={classes} onClick={@_onSwitchAccount.bind(@, item)}>
{@_renderGravatar(item)}
<div className="name" style={lineHeight: "110%"}>{item.label}</div>
<div style={clear: "both"}></div>
</div>
_renderNewAccountOption: =>
_renderManageAccountsItem: =>
<div className="item secondary-item new-account-option"
onClick={@_onManageAccounts}
tabIndex={999}>
@ -84,51 +95,58 @@ class AccountSwitcher extends React.Component
<div style={clear: "both"}></div>
</div>
_renderDropdown: =>
_renderDropdown: (items) =>
<div className="dropdown">
<div className="inner">
{@state.accounts.map(@_renderAccount)}
{@_renderNewAccountOption()}
{items.map(@_renderItem)}
{@_renderManageAccountsItem()}
</div>
</div>
_renderGravatarForAccount: (account) =>
email = account.emailAddress.trim().toLowerCase()
hash = crypto.createHash('md5').update(email, 'utf8').digest('hex')
url = "url(http://www.gravatar.com/avatar/#{hash}?d=blank&s=56)"
_renderGravatar: ({email, iconName}) =>
if email
hash = crypto.createHash('md5').update(email, 'utf8').digest('hex')
url = "url(http://www.gravatar.com/avatar/#{hash}?d=blank&s=56)"
else
url = ''
<div style={float: 'left', position: "relative"}>
<div className="gravatar" style={backgroundImage:url}></div>
<RetinaImg name={"ic-settings-account-#{account.provider}@2x.png"}
<RetinaImg name={"ic-settings-account-#{iconName}@2x.png"}
style={width: 28, height: 28, marginTop: -10}
fallback="ic-settings-account-imap.png"
mode={RetinaImg.Mode.ContentPreserve} />
</div>
_toggleDropdown: =>
@setState showing: !@state.showing
_renderPrimaryItem: (item) =>
<div className="item primary-item" onClick={@_toggleDropdown}>
{@_renderGravatar(item)}
<div style={float: 'right', marginTop: -2}>
<RetinaImg className="toggle"
name="account-switcher-dropdown.png"
mode={RetinaImg.Mode.ContentDark} />
</div>
<div className="name" style={lineHeight: "110%"}>
{item.label}
</div>
<div style={clear: "both"}></div>
</div>
_onStoreChange: =>
@setState @_getStateFromStores()
render: =>
return <span /> unless @props.focusedAccounts
classnames = ""
classnames += "open" if @state.showing
selected = @_selectedItem()
items = [@_makeItem()].concat @props.accounts.map(@_makeItem)
_onBlur: (e) =>
target = e.nativeEvent.relatedTarget
if target? and React.findDOMNode(@refs.button).contains(target)
return
@setState(showing: false)
<div id="account-switcher"
tabIndex={-1}
onBlur={@_onBlur}
ref="button"
className={classnames}>
{@_renderPrimaryItem(selected)}
{@_renderDropdown(items)}
</div>
_onSwitchAccount: (account) =>
Actions.focusDefaultMailboxPerspectiveForAccount(account.id)
@setState(showing: false)
_onManageAccounts: =>
Actions.switchPreferencesTab('Accounts')
Actions.openPreferences()
@setState(showing: false)
_getStateFromStores: =>
accounts: AccountStore.accounts()
account: SidebarStore.currentAccount()
module.exports = AccountSwitcher

View file

@ -1,11 +0,0 @@
Reflux = require 'reflux'
Actions = [
'selectAccount'
]
for key in Actions
Actions[key] = Reflux.createAction(name)
Actions[key].sync = true
module.exports = Actions

View file

@ -1,5 +1,6 @@
_ = require 'underscore'
{Actions,
AccountStore,
SyncbackCategoryTask,
DestroyCategoryTask,
CategoryHelpers,
@ -17,13 +18,14 @@ class SidebarSection
}
@standardSectionForAccount: (account) ->
return @empty('Mailboxes') if CategoryStore.categories().length is 0
cats = CategoryStore.standardCategories(account)
items = _
.reject(cats, (cat) -> cat.name is 'drafts')
.map (cat) => SidebarItem.forCategories([cat])
starredItem = SidebarItem.forStarred([account.id])
draftsItem = SidebarItem.forDrafts(accountId: account.id)
draftsItem = SidebarItem.forDrafts({accountId: account.id})
# Order correctly: Inbox, Starred, rest... , Drafts
items.splice(1, 0, starredItem)
@ -39,31 +41,27 @@ class SidebarSection
return @empty('Mailboxes') if CategoryStore.categories().length is 0
return @standardSectionForAccount(accounts[0]) if accounts.length is 1
# TODO Decide standard items for the unified case
inboxItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getStandardCategory(acc, 'inbox')),
children: accounts.map (acc) ->
cat = CategoryStore.getStandardCategory(acc, 'inbox')
standardNames = [
'inbox',
'sent',
['archive', 'all'],
'trash'
]
items = []
for names in standardNames
names = if Array.isArray(names) then names else [names]
categories = CategoryStore.getStandardCategories(accounts, names...)
continue if categories.length is 0
children = accounts.map (acc) ->
cat = _.first(_.compact(
names.map((name) -> CategoryStore.getStandardCategory(acc, name))
))
SidebarItem.forCategories([cat], name: acc.label)
)
sentItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getStandardCategory(acc, 'sent')),
children: accounts.map (acc) ->
cat = CategoryStore.getStandardCategory(acc, 'sent')
SidebarItem.forCategories([cat], name: acc.label)
)
archiveItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getArchiveCategory(acc)),
children: accounts.map (acc) ->
cat = CategoryStore.getArchiveCategory(acc)
SidebarItem.forCategories([cat], name: acc.label)
)
trashItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getTrashCategory(acc)),
children: accounts.map (acc) ->
cat = CategoryStore.getTrashCategory(acc)
SidebarItem.forCategories([cat], name: acc.label)
)
items.push SidebarItem.forCategories(categories, {children})
starredItem = SidebarItem.forStarred(_.pluck(accounts, 'id'),
children: accounts.map (acc) -> SidebarItem.forStarred([acc.id], name: acc.label)
)
@ -72,14 +70,9 @@ class SidebarSection
SidebarItem.forDrafts(accountId: acc.id, name: acc.label)
)
items = [
inboxItem
starredItem
sentItem
archiveItem
trashItem
draftsItem
]
# Order correctly: Inbox, Starred, rest... , Drafts
items.splice(1, 0, starredItem)
items.push(draftsItem)
return {
title: 'Mailboxes'

View file

@ -12,7 +12,6 @@ _ = require 'underscore'
CategoryStore} = require 'nylas-exports'
SidebarSection = require './sidebar-section'
SidebarActions = require './sidebar-actions'
Sections = {
"Standard",
@ -23,11 +22,18 @@ class SidebarStore extends NylasStore
constructor: ->
@_sections = {}
# @_account = AccountStore.accounts()[0]
@_account = FocusedPerspectiveStore.current().account
@_sections[Sections.Standard] = {}
@_sections[Sections.User] = []
@_registerListeners()
@_updateSections()
accounts: ->
AccountStore.accounts()
focusedAccounts: ->
accountIds = FocusedPerspectiveStore.current().accountIds
accountIds.map((accId) -> AccountStore.accountForId(accId))
standardSection: ->
@_sections[Sections.Standard]
@ -35,7 +41,6 @@ class SidebarStore extends NylasStore
@_sections[Sections.User]
_registerListeners: ->
@listenTo SidebarActions.selectAccount, @_onAccountSelected
@listenTo AccountStore, @_updateSections
@listenTo WorkspaceStore, @_updateSections
@listenTo ThreadCountsStore, @_updateSections
@ -52,13 +57,9 @@ class SidebarStore extends NylasStore
)
return
_onAccountSelected: (account) =>
if @_account isnt account
@_account = account
@_updateSections()
_updateSections: =>
accounts = if @_account? then [@_account] else AccountStore.accounts()
accounts = @focusedAccounts()
@_sections[Sections.Standard] = SidebarSection.standardSectionForAccounts(accounts)
@_sections[Sections.User] = accounts.map (acc) ->
SidebarSection.forUserCategories(acc)

View file

@ -50,7 +50,7 @@ class SearchSuggestionStore extends NylasStore
Actions.focusMailboxPerspective(@_perspectiveBeforeSearch)
@_perspectiveBeforeSearch = null
else
Actions.focusDefaultMailboxPerspectiveForAccount(current.accountIds[0])
Actions.focusDefaultMailboxPerspectiveForAccounts([current.accountIds[0]])
@_clearResults()

View file

@ -52,7 +52,7 @@ describe "AccountStore", ->
"auth_token": "auth-123"
"organization_unit": "label"
@instance = new @constructor
spyOn(Actions, 'focusDefaultMailboxPerspectiveForAccount')
spyOn(Actions, 'focusDefaultMailboxPerspectiveForAccounts')
spyOn(@instance, "trigger")
@instance.addAccountFromJSON(@json)
@ -69,7 +69,7 @@ describe "AccountStore", ->
expect(NylasEnv.config.set.calls.length).toBe 2
it "selects the account", ->
expect(Actions.focusDefaultMailboxPerspectiveForAccount).toHaveBeenCalledWith("1234")
expect(Actions.focusDefaultMailboxPerspectiveForAccounts).toHaveBeenCalledWith(["1234"])
it "triggers", ->
expect(@instance.trigger).toHaveBeenCalled()

View file

@ -227,7 +227,7 @@ class Actions
*Scope: Window*
###
@focusDefaultMailboxPerspectiveForAccount: ActionScopeWindow
@focusDefaultMailboxPerspectiveForAccounts: ActionScopeWindow
###
Public: If the message with the provided id is currently beign displayed in the

View file

@ -32,7 +32,7 @@ class AccountStore
newAccountIds = _.keys(_.omit(updatedTokens, _.keys(@_tokens)))
@_load()
if newAccountIds.length > 0
Actions.focusDefaultMailboxPerspectiveForAccount(newAccountIds[0])
Actions.focusDefaultMailboxPerspectiveForAccounts([newAccountIds[0]])
if NylasEnv.isComposerWindow()
NylasEnv.config.observe saveObjectsKey, => @_load()
@ -88,7 +88,7 @@ class AccountStore
@_save()
@trigger()
Actions.focusDefaultMailboxPerspectiveForAccount(account.id)
Actions.focusDefaultMailboxPerspectiveForAccounts([account.id])
# Exposed Data
@ -200,7 +200,7 @@ class AccountStore
t.persistModels(threads)
])
.then =>
Actions.focusDefaultMailboxPerspectiveForAccount(account.id)
Actions.focusDefaultMailboxPerspectiveForAccounts([account.id])
.then -> new Promise (resolve, reject) -> setTimeout(resolve, 1000)
module.exports = new AccountStore()

View file

@ -65,6 +65,16 @@ class CategoryStore extends NylasStore
return _.findWhere(@_standardCategories[asAccountId(accountOrId)], {name})
getStandardCategories: (accountsOrIds, names...) ->
if Array.isArray(accountsOrIds)
res = []
for accOrId in accountsOrIds
cats = names.map((name) => @getStandardCategory(accOrId, name))
res = res.concat(_.compact(cats))
res
else
names.map((name) => @getStandardCategory(accountsOrIds, name))
# Public: Returns the Folder or Label object that should be used for "Archive"
# actions. On Gmail, this is the "all" label. On providers using folders, it
# returns any available "Archive" folder, or null if no such folder exists.

View file

@ -16,7 +16,7 @@ class FocusedPerspectiveStore extends NylasStore
@listenTo AccountStore, @_onAccountStoreChanged
@listenTo Actions.focusMailboxPerspective, @_onFocusPerspective
@listenTo Actions.focusDefaultMailboxPerspectiveForAccount, @_onFocusAccount
@listenTo Actions.focusDefaultMailboxPerspectiveForAccounts, @_onFocusAccounts
@_onCategoryStoreChanged()
@_setupFastAccountCommands()
@ -28,15 +28,16 @@ class FocusedPerspectiveStore extends NylasStore
@_setupFastAccountMenu()
_onCategoryStoreChanged: ->
if not @_current
if @_current.isEqual(MailboxPerspective.forNothing())
@_setPerspective(@_defaultPerspective())
else
account = @_current.account
cats = @_current.categories()
catExists = (cat) -> CategoryStore.byId(cat.accountId, cat.id)
accountIds = @_current.accountIds
categories = @_current.categories()
catExists = (cat) -> CategoryStore.byId(cat.accountId, cat.id)
categoryHasBeenDeleted = categories and not _.every(categories, catExists)
if cats and not _.every(cats, catExists)
@_setPerspective(@_defaultPerspective(account))
if categoryHasBeenDeleted
@_setPerspective(@_defaultPerspective(accountIds))
_onFocusPerspective: (perspective) =>
return if perspective.isEqual(@_current)
@ -44,18 +45,13 @@ class FocusedPerspectiveStore extends NylasStore
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
@_setPerspective(perspective)
_onFocusAccount: (accountId) =>
account = AccountStore.accountForId(accountId) unless account instanceof Account
return unless account
category = CategoryStore.getStandardCategory(account, "inbox")
return unless category
@_setPerspective(MailboxPerspective.forCategory(category))
_onFocusAccounts: (accountsOrIds) =>
return unless accountsOrIds
@_setPerspective(MailboxPerspective.forInbox(accountsOrIds))
_defaultPerspective: (account = AccountStore.accounts()[0]) ->
return MailboxPerspective.forNothing() unless account
category = CategoryStore.getStandardCategory(account, "inbox")
return MailboxPerspective.forNothing() unless category
return MailboxPerspective.forCategory(category)
_defaultPerspective: (accounts = AccountStore.accounts()) ->
return MailboxPerspective.forNothing() unless accounts.length > 0
return MailboxPerspective.forInbox(accounts)
_setPerspective: (perspective) ->
return if perspective?.isEqual(@_current)
@ -64,9 +60,13 @@ class FocusedPerspectiveStore extends NylasStore
_setupFastAccountCommands: ->
commands = {}
[0..8].forEach (index) =>
allKey = "application:select-account-0"
commands[allKey] = => @_onFocusAccounts(AccountStore.accounts())
[1..8].forEach (index) =>
account = AccountStore.accounts()[index - 1]
return unless account
key = "application:select-account-#{index}"
commands[key] = => @_onFocusAccount(AccountStore.accounts()[index])
commands[key] = => @_onFocusAccounts([account])
NylasEnv.commands.add('body', commands)
_setupFastAccountMenu: ->
@ -77,14 +77,18 @@ class FocusedPerspectiveStore extends NylasStore
idx = _.findIndex submenu, ({type}) -> type is 'separator'
return unless idx > 0
accountMenuItems = AccountStore.accounts().map (item, idx) =>
{
label: item.emailAddress,
command: "application:select-account-#{idx}",
account: true
}
menuItems = [{
label: 'All Accounts'
command: "application:select-account-0"
account: true
}]
menuItems = menuItems.concat AccountStore.accounts().map((item, idx) =>
label: item.emailAddress,
command: "application:select-account-#{idx + 1}",
account: true
)
submenu.splice(idx + 1, 0, accountMenuItems...)
submenu.splice(idx + 1, 0, menuItems...)
windowMenu.submenu = submenu
NylasEnv.menu.update()

View file

@ -20,21 +20,29 @@ class MailboxPerspective
new EmptyMailboxPerspective()
@forCategory: (category) ->
return @forNothing() unless category
new CategoryMailboxPerspective([category])
@forCategories: (categories) ->
return @forNothing() if categories.length is 0
new CategoryMailboxPerspective(categories)
@forStarred: (accountIds) ->
new StarredMailboxPerspective(accountIds)
@forStandardCategories: (accountsOrIds, names...) ->
categories = CategoryStore.getStandardCategories(accountsOrIds, names...)
@forCategories(categories)
@forSearch: (accountIds, query) ->
new SearchMailboxPerspective(accountIds, query)
@forStarred: (accountsOrIds) ->
new StarredMailboxPerspective(accountsOrIds)
@forSearch: (accountsOrIds, query) ->
new SearchMailboxPerspective(accountsOrIds, query)
@forInbox: (accountsOrIds) =>
@forStandardCategories(accountsOrIds, 'inbox')
@forAll: (accountsOrIds) =>
@forStandardCategories(accountsOrIds, 'all')
@forAll: (accountIds) ->
categories = accountIds.map (aid) ->
CategoryStore.getStandardCategory(aid, "all")
new CategoryMailboxPerspective(_.compact(categories))
# Instance Methods
@ -130,6 +138,7 @@ class StarredMailboxPerspective extends MailboxPerspective
class EmptyMailboxPerspective extends MailboxPerspective
constructor: ->
@accountIds = []
threads: =>
query = DatabaseStore.findAll(Thread).where(accountId: -1).limit(0)