update(sidebar): Update sidebar design + context menus

Summary:
- Removes account switcher almost entirely
- Update context menu to edit and delete sidebar items
- Gross hardcoded position and size for the switcher icon -- will likely update with later redesign

Test Plan: - Visual

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2527
This commit is contained in:
Juan Tejada 2016-02-04 14:48:15 -08:00
parent 8b3f7f0578
commit 182f6abb25
7 changed files with 70 additions and 297 deletions

View file

@ -40,8 +40,8 @@ class AccountSidebar extends React.Component
{accounts, focusedAccounts, userSections, standardSection} = @state
<Flexbox direction="column" style={order: 0, flexShrink: 1, flex: 1}>
<AccountSwitcher accounts={accounts} focusedAccounts={focusedAccounts} />
<ScrollRegion className="account-sidebar" style={order: 2}>
<AccountSwitcher accounts={accounts} focusedAccounts={focusedAccounts} />
<div className="account-sidebar-sections">
<OutlineView {...standardSection} />
{@_renderUserSections(userSections)}

View file

@ -13,19 +13,10 @@ ItemTypes = {
class AccountSwitcher extends React.Component
@displayName: 'AccountSwitcher'
@containerRequired: false
@containerStyles:
minWidth: 165
maxWidth: 210
@propTypes:
accounts: React.PropTypes.array.isRequired
focusedAccounts: React.PropTypes.array.isRequired
constructor: (@props) ->
@state =
showing: false
# Helpers
_makeAccountItem: (account) =>
@ -53,107 +44,60 @@ class AccountSwitcher extends React.Component
_toggleDropdown: =>
@setState showing: !@state.showing
_makeMenuItem: (item, idx = 0) =>
menuItem = {
label: item.label,
click: @_onSwitchAccount.bind(@, item)
accelerator: "CmdOrCtrl+#{idx}"
}
if @_selectedItem().id is item.id
menuItem.type = 'checkbox'
menuItem.checked = true
return menuItem
_makeMenuTemplate: =>
template = []
items = @props.accounts.map(@_makeAccountItem)
if @props.accounts.length > 1
unifiedItem = @_makeUnifiedItem()
template = [
@_makeMenuItem(unifiedItem)
{type: 'separator'}
]
items.forEach (item, idx) => template.push(@_makeMenuItem(item, idx + 1))
template = template.concat [
{type: 'separator'}
{label: 'Manage Accounts...', click: @_onManageAccounts}
]
return template
# Handlers
_onBlur: (e) =>
target = e.nativeEvent.relatedTarget
if target? and React.findDOMNode(@refs.button).contains(target)
return
@setState(showing: false)
_onSwitchAccount: (item) =>
SidebarActions.focusAccounts(item.accounts)
@setState(showing: false)
_onManageAccounts: =>
Actions.switchPreferencesTab('Accounts')
Actions.openPreferences()
@setState(showing: false)
_renderItem: (item) =>
classes = classNames
"active": item.id is @_selectedItem().id
"item": true
"secondary-item": true
<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>
_renderManageAccountsItem: =>
<div className="item secondary-item new-account-option"
onClick={@_onManageAccounts}
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'}>
Manage accounts&hellip;
</div>
<div style={clear: "both"}></div>
</div>
_renderDropdown: (items) =>
<div className="dropdown">
<div className="inner">
{items.map(@_renderItem)}
{@_renderManageAccountsItem()}
</div>
</div>
_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-#{iconName}@2x.png"}
style={width: 28, height: 28, marginTop: -10}
fallback="ic-settings-account-imap.png"
mode={RetinaImg.Mode.ContentPreserve} />
</div>
_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>
_onShowMenu: =>
remote = require('electron').remote
Menu = remote.Menu
menu = Menu.buildFromTemplate(@_makeMenuTemplate())
menu.popup()
render: =>
return <span /> unless @props.focusedAccounts
classnames = "account-switcher"
classnames += " open" if @state.showing
selected = @_selectedItem()
if @props.accounts.length is 1
items = @props.accounts.map(@_makeAccountItem)
else
items = [@_makeUnifiedItem()].concat @props.accounts.map(@_makeAccountItem)
<div
className={classnames}
ref="button"
tabIndex={-1}
onBlur={@_onBlur}>
{@_renderPrimaryItem(selected)}
{@_renderDropdown(items)}
<div className="account-switcher" onMouseDown={@_onShowMenu}>
<RetinaImg
style={width: 13, height: 14}
name="account-switcher-dropdown.png"
mode={RetinaImg.Mode.ContentDark} />
</div>

View file

@ -1,4 +1,5 @@
_ = require 'underscore'
_str = require 'underscore.string'
{WorkspaceStore,
MailboxPerspective,
FocusedPerspectiveStore,
@ -58,6 +59,7 @@ class SidebarItem
return _.extend({
id: id
name: perspective.name
contextMenuLabel: perspective.name
count: countForItem(perspective)
iconName: perspective.iconName
children: []
@ -95,9 +97,12 @@ class SidebarItem
@forCategories: (categories = [], opts = {}) ->
id = idForCategories(categories)
contextMenuLabel = _str.capitalize(categories[0]?.displayType())
perspective = MailboxPerspective.forCategories(categories)
opts.deletable ?= true
opts.editable ?= true
opts.contextMenuLabel = contextMenuLabel
@forPerspective(id, perspective, opts)
@forStarred: (accountIds, opts = {}) ->

View file

@ -31,7 +31,7 @@ class SidebarSection
throw new Error("standardSectionForAccount: You must pass an account.")
cats = CategoryStore.standardCategories(account)
return @empty('Mailboxes') if cats.length is 0
return @empty(account.label) if cats.length is 0
items = _
.reject(cats, (cat) -> cat.name is 'drafts')
@ -45,13 +45,13 @@ class SidebarSection
items.push(draftsItem)
return {
title: 'Mailboxes'
title: account.label
items: items
}
@standardSectionForAccounts: (accounts) ->
return @empty('Mailboxes') if not accounts or accounts.length is 0
return @empty('Mailboxes') if CategoryStore.categories().length is 0
return @empty('All Accounts') if not accounts or accounts.length is 0
return @empty('All Accounts') if CategoryStore.categories().length is 0
return @standardSectionForAccount(accounts[0]) if accounts.length is 1
standardNames = [
@ -89,7 +89,7 @@ class SidebarSection
items.push(draftsItem)
return {
title: 'Mailboxes'
title: 'All Accounts'
items: items
}

View file

@ -1,52 +0,0 @@
React = require 'react/addons'
TestUtils = React.addons.TestUtils
AccountSwitcher = require './../lib/components/account-switcher'
SidebarStore = require './../lib/sidebar-store'
{AccountStore} = require 'nylas-exports'
describe "AccountSwitcher", ->
switcher = null
beforeEach ->
account = AccountStore.accounts()[0]
accounts = [
account,
{
emailAddress: "dillon@nylas.com",
provider: "exchange"
label: "work"
}
]
switcher = TestUtils.renderIntoDocument(
<AccountSwitcher accounts={accounts} focusedAccounts={[account]} />
)
it "doesn't render the dropdown if nothing clicked", ->
openDropdown = TestUtils.scryRenderedDOMComponentsWithClass switcher, 'open'
expect(openDropdown.length).toBe 0
it "shows the dropdown on click", ->
toggler = TestUtils.findRenderedDOMComponentWithClass switcher, 'primary-item'
TestUtils.Simulate.click toggler
openDropdown = TestUtils.scryRenderedDOMComponentsWithClass switcher, 'open'
expect(openDropdown.length).toBe 1
it "hides the dropdown on blur", ->
toggler = TestUtils.findRenderedDOMComponentWithClass switcher, 'primary-item'
TestUtils.Simulate.click toggler
toggler = TestUtils.findRenderedDOMComponentWithClass switcher, 'primary-item'
TestUtils.Simulate.blur toggler
openDropdown = TestUtils.scryRenderedDOMComponentsWithClass switcher, 'open'
expect(openDropdown.length).toBe 0
it "shows other accounts and the 'Add Account' button", ->
toggler = TestUtils.findRenderedDOMComponentWithClass switcher, 'primary-item'
TestUtils.Simulate.click toggler
dropdown = TestUtils.findRenderedDOMComponentWithClass switcher, "dropdown"
items = TestUtils.scryRenderedDOMComponentsWithClass dropdown, "secondary-item"
newAccountButton = TestUtils.scryRenderedDOMComponentsWithClass dropdown, "new-account-option"
# The unified Inbox item, then both accounts, then the manage item
expect(items.length).toBe 4
expect(newAccountButton.length).toBe 1

View file

@ -9,147 +9,22 @@
.item.deleted {
opacity: 0.5;
}
.nylas-outline-view:first-child {
.heading span {
margin-right: 25px;
}
}
}
.account-switcher {
order: 1;
height: auto;
background-color: @source-list-bg;
border-bottom: 1px solid @border-color-divider;
&.open {
.dropdown {
opacity: 1;
pointer-events: initial;
transform:scale(1, 1);
transform-origin: top;
.inner {
opacity: 1;
transition: opacity 50ms linear;
transition-delay: 50ms;
}
}
.toggle {
transform: rotateX(0deg);
}
}
.primary-item {
padding-top: @padding-large-vertical;
padding-bottom: @padding-base-vertical;
padding-left: 10px;
.name {
padding-left: 7px;
}
}
.secondary-item {
&:hover {
background: @list-hover-bg;
}
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;
}
}
.toggle {
position: absolute;
top: 7px;
right: 17px;
z-index: 3;
img {
transform: rotateX(180deg);
}
.dropdown {
opacity: 0;
pointer-events: none;
transform:scale(1, 0.2);
transform-origin: top;
transition: transform 125ms cubic-bezier(0.18, 0.89, 0.32, 1.12), opacity 100ms linear;
.inner {
opacity: 0;
transition: opacity 25ms linear;
transition-delay: 0;
}
margin-top: -7px;
background: lighten(@source-list-bg, 5%);
border: 1px solid @border-color-divider;
border-radius: @border-radius-base;
border-top-left-radius: 0;
border-top-right-radius: 0;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.21);
position: absolute;
top: 54px;
width: 100%;
z-index: 999;
.account .gravatar {
top: 6px;
}
}
.item {
color: @text-color-subtle;
display: block;
img.content-mask {
background-color: @text-color-subtle;
vertical-align: text-bottom;
}
font-size: @font-size-small;
font-weight: 400;
padding-right: @spacing-standard;
line-height: @line-height-large * 1.1;
clear: both;
position: relative;
margin-bottom: 0;
.gravatar {
background-size: 28px 28px;
width: 28px;
height: 28px;
position: absolute;
z-index: 2;
border-radius: 4px;
top: -2px;
background-repeat: no-repeat;
}
.name {
order: 2;
padding-left: @padding-small-horizontal * 0.85;
position:relative;
top:1px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-top: @padding-small-vertical;
padding-bottom:@padding-small-vertical;
line-height: @line-height-small;
}
&:hover {
cursor: default;
}
}
}
body.platform-win32 {
.account-switcher {
.dropdown {
border-radius: 0;
}
.item {
border-radius: 0;
}
.secondary-item {
&:first-child, &:last-child {
border-radius: 0;
}
}
}
}

View file

@ -36,6 +36,7 @@ const CounterStyles = {
* @param {object} props.item - props for OutlineViewItem
* @param {string} props.item.id - Unique id for the item.
* @param {string} props.item.name - Name to display
* @param {string} props.item.contextMenuLabel - Label to be displayed in context menu
* @param {string} props.item.className - Extra classes to add to the item
* @param {string} props.item.iconName - Icon name for icon. See {@link RetinaImg} for further reference.
* @param {array} props.item.children - Array of children of the same type to be
@ -245,21 +246,21 @@ class OutlineViewItem extends Component {
_onShowContextMenu = (event)=> {
event.stopPropagation()
const item = this.props.item;
const name = item.name;
const contextMenuLabel = item.contextMenuLabel || item.name
const {remote} = require('electron');
const {Menu, MenuItem} = remote;
const menu = new Menu();
if (this.props.item.onEdited) {
menu.append(new MenuItem({
label: `Edit ${name}`,
label: `Rename ${contextMenuLabel}`,
click: this._onEdit,
}));
}
if (this.props.item.onDelete) {
menu.append(new MenuItem({
label: `Delete ${name}`,
label: `Delete ${contextMenuLabel}`,
click: this._onDelete,
}));
}