mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 23:36:21 +08:00
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:
parent
48de24ad8f
commit
9c7259227d
|
@ -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…
|
||||
</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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
static/images/sidebar/account-switcher-dropdown@2x.png
Normal file
BIN
static/images/sidebar/account-switcher-dropdown@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
static/images/source-list/icon-accounts-addnew@2x.png
Normal file
BIN
static/images/source-list/icon-accounts-addnew@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Loading…
Reference in a new issue