mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-12-27 19:07:15 +08:00
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:
parent
8b3f7f0578
commit
182f6abb25
7 changed files with 70 additions and 297 deletions
|
@ -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)}
|
||||
|
|
|
@ -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…
|
||||
</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>
|
||||
|
||||
|
||||
|
|
|
@ -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 = {}) ->
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue