feat(account-sidebar): move account switcher from the side to the top of the account sidebar. fixes T3546.

Summary: ready for final code review! @evan @bengotow

Test Plan: added some tests for the account switcher

Reviewers: evan, bengotow

Reviewed By: evan, bengotow

Maniphest Tasks: T3546

Differential Revision: https://phab.nylas.com/D2016
This commit is contained in:
dillon 2015-09-14 15:05:33 -07:00
parent 48de24ad8f
commit 9c7259227d
10 changed files with 260 additions and 114 deletions

View file

@ -1,10 +1,13 @@
React = require 'react'
{Actions, MailViewFilter} = require("nylas-exports")
{Actions, MailViewFilter, AccountStore} = require("nylas-exports")
{ScrollRegion} = require("nylas-component-kit")
SidebarDividerItem = require("./account-sidebar-divider-item")
SidebarSheetItem = require("./account-sidebar-sheet-item")
AccountSidebarStore = require ("./account-sidebar-store")
AccountSidebarMailViewItem = require("./account-sidebar-mail-view-item")
crypto = require 'crypto'
{RetinaImg} = require 'nylas-component-kit'
classNames = require 'classnames'
class AccountSidebar extends React.Component
@displayName: 'AccountSidebar'
@ -16,10 +19,12 @@ class AccountSidebar extends React.Component
constructor: (@props) ->
@state = @_getStateFromStores()
@state.showing = false
componentDidMount: =>
@unsubscribers = []
@unsubscribers.push AccountSidebarStore.listen @_onStoreChange
@unsubscribers.push AccountStore.listen @_onStoreChange
# It's important that every React class explicitly stops listening to
# atom events before it unmounts. Thank you event-kit
@ -29,11 +34,105 @@ class AccountSidebar extends React.Component
render: =>
<ScrollRegion style={flex:1} id="account-sidebar">
{@_accountSwitcher()}
<div className="account-sidebar-sections">
{@_sections()}
</div>
</ScrollRegion>
_accountSwitcher: =>
return undefined if @state.accounts.length < 1
<div id="account-switcher" tabIndex={-1} onBlur={@_onBlur} ref="button">
{@_renderAccount @state.account, true}
{@_renderDropdown()}
</div>
_renderAccount: (account, isPrimaryItem) =>
classes = classNames
"account": true
"item": true
"dropdown-item-padding": not isPrimaryItem
"active": account is @state.account
"bg-color-hover": not isPrimaryItem
"primary-item": isPrimaryItem
"account-option": not isPrimaryItem
email = account.emailAddress.trim().toLowerCase()
if isPrimaryItem
dropdownClasses = classNames
"account-switcher-dropdown": true,
"account-switcher-dropdown-hidden": @state.showing
dropdownArrow = <div style={float: 'right', marginTop: -2}>
<RetinaImg className={dropdownClasses} name="account-switcher-dropdown.png"
mode={RetinaImg.Mode.ContentPreserve} />
</div>
onClick = @_toggleDropdown
else
onClick = =>
@_onSwitchAccount account
<div className={classes}
onClick={onClick}
key={email}>
<div style={float: 'left'}>
<div className="gravatar" style={backgroundImage: @_gravatarUrl(email)}></div>
<RetinaImg name={"ic-settings-account-#{account.provider}@2x.png"}
style={width: 28, height: 28, marginTop: -10}
fallback="ic-settings-account-imap.png"
mode={RetinaImg.Mode.ContentPreserve} />
</div>
{dropdownArrow}
<div className="name" style={lineHeight: "110%"}>
{email}
</div>
<div style={clear: "both"}>
</div>
</div>
_renderNewAccountOption: =>
<div className="account item dropdown-item-padding bg-color-hover new-account-option"
onClick={@_onAddAccount}
tabIndex={999}>
<div style={float: 'left'}>
<RetinaImg name="icon-accounts-addnew.png"
fallback="ic-settings-account-imap.png"
mode={RetinaImg.Mode.ContentPreserve}
style={width: 28, height: 28, marginTop: -10} />
</div>
<div className="name" style={lineHeight: "110%", textTransform: 'none'}>
Add account&hellip;
</div>
<div style={clear: "both"}>
</div>
</div>
_renderDropdown: =>
display = if @state.showing then "block" else "none"
# display = "block"
accounts = @state.accounts.map (a) =>
@_renderAccount(a)
<div style={display: display}
ref="account-switcher-dropdown"
className="dropdown dropdown-positioning dropdown-colors">
{accounts}
{@_renderNewAccountOption()}
</div>
_toggleDropdown: =>
@setState showing: !@state.showing
_gravatarUrl: (email) =>
hash = crypto.createHash('md5').update(email, 'utf8').digest('hex')
"url(http://www.gravatar.com/avatar/#{hash}?d=blank&s=56)"
_sections: =>
return @state.sections.map (section) =>
<section key={section.label}>
@ -64,12 +163,25 @@ class AccountSidebar extends React.Component
_onStoreChange: =>
@setState @_getStateFromStores()
_onBlur: (e) =>
target = e.nativeEvent.relatedTarget
if target? and React.findDOMNode(@refs.button).contains(target)
return
@setState(showing: false)
_onSwitchAccount: (account) =>
Actions.selectAccountId(account.id)
@setState(showing: false)
_onAddAccount: =>
require('remote').getGlobal('application').windowManager.newOnboardingWindow()
@setState showing: false
_getStateFromStores: =>
sections: AccountSidebarStore.sections()
selected: AccountSidebarStore.selected()
accounts: AccountStore.items()
account: AccountStore.current()
module.exports = AccountSidebar

View file

@ -1,65 +0,0 @@
React = require 'react'
{Actions, AccountStore} = require("nylas-exports")
{RetinaImg} = require('nylas-component-kit')
crypto = require 'crypto'
classNames = require 'classnames'
class AccountSwitcher extends React.Component
@displayName: 'AccountSwitcher'
@containerRequired: false
@containerStyles:
minWidth: 64
maxWidth: 64
constructor: (@props) ->
@state = @_getStateFromStores()
componentDidMount: =>
@unsubscribers = []
@unsubscribers.push AccountStore.listen @_onStoreChange
# It's important that every React class explicitly stops listening to
# atom events before it unmounts. Thank you event-kit
# This can be fixed via a Reflux mixin
componentWillUnmount: =>
unsubscribe() for unsubscribe in @unsubscribers
render: =>
if @state.accounts.length is 0
return <span></span>
<div id="account-switcher">
{@_accounts()}
</div>
_accounts: =>
return @state.accounts.map (account) =>
hash = account.emailAddress.trim().toLowerCase()
hash = crypto.createHash('md5').update(hash, 'utf8').digest('hex')
classnames = classNames
'account': true
'active': account is @state.account
gravatarUrl = "http://www.gravatar.com/avatar/#{hash}?d=blank&s=44"
<div title={account.emailAddress} className={classnames} key={account.id} onClick={ => @_onSwitchAccount(account) }>
<div style={backgroundImage: "url(#{gravatarUrl})"} className="gravatar"></div>
<RetinaImg name={"ic-settings-account-#{account.provider}.png"}
style={width: 44, height: 44}
fallback="ic-settings-account-imap.png"
mode={RetinaImg.Mode.ContentPreserve} />
</div>
_onStoreChange: =>
@setState @_getStateFromStores()
_onSwitchAccount: (account) =>
Actions.selectAccountId(account.id)
_getStateFromStores: =>
accounts: AccountStore.items()
account: AccountStore.current()
module.exports = AccountSwitcher

View file

@ -1,15 +1,11 @@
React = require "react"
AccountSidebar = require "./account-sidebar"
AccountSwitcher = require "./account-switcher"
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
module.exports =
item: null # The DOM item the main React component renders into
activate: (@state) ->
ComponentRegistry.register AccountSwitcher,
location: WorkspaceStore.Location.RootSwitcher
ComponentRegistry.register AccountSidebar,
location: WorkspaceStore.Location.RootSidebar

View file

@ -0,0 +1,60 @@
React = require 'react/addons'
TestUtils = React.addons.TestUtils
AccountSidebar = require './../lib/account-sidebar'
{AccountStore} = require 'nylas-exports'
describe "AccountSidebar", ->
describe "account switcher", ->
sidebar = null
beforeEach ->
spyOn(AccountStore, "items").andCallFake ->
[
AccountStore.current(),
{
emailAddress: "dillon@nylas.com",
provider: "exchange"
}
]
sidebar = TestUtils.renderIntoDocument(
<AccountSidebar />
)
it "doesn't render the dropdown if nothing clicked", ->
dropdown = TestUtils.findRenderedDOMComponentWithClass sidebar, "dropdown"
dropdownNode = React.findDOMNode dropdown, "account-switcher-dropdown"
expect(dropdownNode.style.display).toBe "none"
it "renders the dropdown if clicking the arrow btn", ->
toggler = TestUtils.findRenderedDOMComponentWithClass sidebar, 'primary-item'
TestUtils.Simulate.click toggler
dropdown = TestUtils.findRenderedDOMComponentWithClass sidebar, "dropdown"
dropdownNode = React.findDOMNode dropdown, "account-switcher-dropdown"
expect(dropdownNode.style.display).toBe "block"
it "removes the dropdown when clicking elsewhere", ->
toggler = TestUtils.findRenderedDOMComponentWithClass sidebar, 'primary-item'
TestUtils.Simulate.blur toggler
dropdown = TestUtils.findRenderedDOMComponentWithClass sidebar, "dropdown"
dropdownNode = React.findDOMNode dropdown, "account-switcher-dropdown"
expect(dropdownNode.style.display).toBe "none"
it "shows all the accounts in the dropdown", ->
toggler = TestUtils.findRenderedDOMComponentWithClass sidebar, 'primary-item'
TestUtils.Simulate.click toggler
dropdown = TestUtils.findRenderedDOMComponentWithClass sidebar, "dropdown"
accounts = TestUtils.scryRenderedDOMComponentsWithClass dropdown, "account-option"
expect(accounts.length).toBe 2
it "shows the 'Add Account' button too", ->
toggler = TestUtils.findRenderedDOMComponentWithClass sidebar, 'primary-item'
TestUtils.Simulate.click toggler
dropdown = TestUtils.findRenderedDOMComponentWithClass sidebar, "dropdown"
accounts = TestUtils.scryRenderedDOMComponentsWithClass dropdown, "new-account-option"
expect(accounts.length).toBe 1

View file

@ -1,45 +1,7 @@
@import "ui-variables";
@import "ui-mixins";
#account-switcher {
background-color: #212831;
flex: 1;
.account {
position: relative;
margin:15px 10px;
margin-bottom: 0;
display:block;
border-radius: 8px;
opacity: 0.7;
img {
-webkit-filter: ~"saturate(0%)";
}
.gravatar {
width: 44px;
height: 44px;
position: absolute;
z-index: 2;
border-radius: 7px;
}
}
.account.active {
opacity: 1.0;
img {
-webkit-filter: ~"saturate(100%)";
}
}
.account.active::after {
box-shadow: inset 0 0 1px 2px @source-list-active-color;
border-radius: 7px;
width:100%;
height:100%;
content: ' ';
left: 0;
position:absolute;
z-index: 3;
}
}
#account-switcher,
#account-sidebar {
order: 1;
height: 100%;
@ -79,7 +41,6 @@
float: left;
}
.name {
text-transform: capitalize;
padding-left: @padding-small-horizontal;
position:relative;
top:1px;
@ -110,3 +71,85 @@
padding-bottom: 0.25em;
}
}
#account-sidebar {
.name {
text-transform: capitalize;
}
}
#account-switcher {
padding-top: @padding-large-vertical;
padding-bottom: @padding-base-vertical;
border-bottom: 1px solid @border-color-divider;
.account {
position: relative;
margin-bottom: 0;
display:block;
.gravatar {
background-size: 28px 28px;
width: 28px;
height: 28px;
position: absolute;
z-index: 2;
border-radius: 7px;
top: -2px;
}
}
.name {
text-transform: lowercase;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.account-switcher-dropdown {
margin-top: -7px;
transform: scale(1, -1);
&.account-switcher-dropdown-hidden {
transform: scale(1, 1);
}
}
.dropdown-positioning {
display: block;
position: absolute;
top: 45px;
width: 100%;
z-index: 999;
.account .gravatar {
top: 6px;
}
}
.dropdown-colors {
background: lighten(@source-list-bg, 5%);
border: 1px solid @border-color-divider;
border-radius: @border-radius-base;
box-shadow: @standard-shadow;
}
.dropdown-item-padding {
padding: 6px 5px 0 14px;
&:first-child {
padding-top: 8px;
border-top-left-radius: @list-border-radius;
border-top-right-radius: @list-border-radius;
}
&:last-child {
padding-bottom: 2px;
border-bottom-left-radius: @list-border-radius;
border-bottom-right-radius: @list-border-radius;
}
}
.bg-color-hover {
&:hover {
background: @list-hover-bg;
}
}
}

View file

@ -8,7 +8,7 @@ module.exports =
activate: (@state={}) ->
WorkspaceStore.defineSheet 'Settings', {root: true, supportedModes: ['list'], name: 'Plugins'},
list: ['RootSwitcher', 'RootSidebar', 'SettingsSidebar', 'Settings']
list: ['RootSidebar', 'SettingsSidebar', 'Settings']
ComponentRegistry.register SettingsTabsView,
location: WorkspaceStore.Location.SettingsSidebar

View file

@ -14,7 +14,7 @@ DraftList = require './draft-list'
module.exports =
activate: (@state={}) ->
WorkspaceStore.defineSheet 'Drafts', {root: true, name: 'Drafts', sidebarComponent: DraftListSidebarItem},
list: ['RootSwitcher', 'RootSidebar', 'DraftList']
list: ['RootSidebar', 'DraftList']
ComponentRegistry.register ThreadList,
location: WorkspaceStore.Location.ThreadList

View file

@ -46,8 +46,8 @@ class WorkspaceStore extends NylasStore
if atom.isMainWindow()
@defineSheet 'Global'
@defineSheet 'Threads', {root: true},
list: ['RootSwitcher', 'RootSidebar', 'ThreadList']
split: ['RootSwitcher', 'RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
list: ['RootSidebar', 'ThreadList']
split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
@defineSheet 'Thread', {},
list: ['MessageList', 'MessageListSidebar']
else

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB