feat(account-sidebar): Ability to add custom items to the sidebar

This commit is contained in:
Ben Gotow 2015-09-22 14:43:08 -07:00
parent 3d84c41625
commit a316518326
7 changed files with 217 additions and 155 deletions

View file

@ -44,11 +44,8 @@ class AccountSidebarStore extends NylasStore
account = AccountStore.current()
return unless account
viewFilterForCategory = (cat) ->
return MailViewFilter.forCategory(cat)
userCategories = CategoryStore.getUserCategories()
userCategoryViews = _.map(userCategories, viewFilterForCategory)
userCategoryItems = _.map(userCategories, @_sidebarItemForCategory)
# Our drafts are displayed via the `DraftListSidebarItem` which
# is loading into the `Drafts` Sheet.
@ -56,30 +53,33 @@ class AccountSidebarStore extends NylasStore
standardCategories = _.reject standardCategories, (category) =>
category.name is "drafts"
standardViews = _.map(standardCategories, viewFilterForCategory)
starredView = MailViewFilter.forStarred()
standardViews.splice(1, 0, starredView)
standardCategoryItems = _.map(standardCategories, @_sidebarItemForCategory)
starredItem = @_sidebarItemForMailView('starred', MailViewFilter.forStarred())
# Find root views and add them to the bottom of the list (Drafts, etc.)
_.each WorkspaceStore.Sheet, (sheet) ->
if sheet.root and sheet.name
standardViews.push(sheet)
standardItems = standardCategoryItems
standardItems.splice(1, 0, starredItem)
standardItems.push(WorkspaceStore.sidebarItems()...)
@_sections = []
@_sections.push
label: 'Mailboxes'
items: standardViews
items: standardItems
type: 'mailboxes'
@_sections.push
label: CategoryStore.categoryLabel()
items: userCategoryViews
items: userCategoryItems
type: 'category'
@trigger()
_isStandardCategory: (category) =>
category.name and category.name in CategoryStore.standardCategories
_sidebarItemForMailView: (id, filter) =>
new WorkspaceStore.SidebarItem({id: id, name: filter.name, mailViewFilter: filter})
_sidebarItemForCategory: (category) =>
filter = MailViewFilter.forCategory(category)
@_sidebarItemForMailView(category.id, filter)
module.exports = new AccountSidebarStore()

View file

@ -1,13 +1,12 @@
React = require 'react'
{Actions, MailViewFilter, AccountStore} = require("nylas-exports")
{Actions, MailViewFilter, WorkspaceStore} = require("nylas-exports")
{ScrollRegion} = require("nylas-component-kit")
SidebarDividerItem = require("./account-sidebar-divider-item")
SidebarSheetItem = require("./account-sidebar-sheet-item")
AccountSwitcher = require ("./account-switcher")
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'
@ -19,169 +18,64 @@ 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
# This can be fixed via a Reflux mixin
componentWillUnmount: =>
unsubscribe() for unsubscribe in @unsubscribers
render: =>
<ScrollRegion style={flex:1} id="account-sidebar">
{@_accountSwitcher()}
<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) =>
@state.sections.map (section) =>
<section key={section.label}>
<div className="heading">{section.label}</div>
{@_itemComponents(section)}
</section>
_itemComponents: (section) =>
section.items?.map (item) =>
return unless item
if item instanceof MailViewFilter
<AccountSidebarMailViewItem
key={item.id ? item.type}
mailView={item}
select={ item.isEqual(@state.selected) }/>
else
if item.sidebarComponent
itemClass = item.sidebarComponent
else
itemClass = SidebarSheetItem
section.items.map (item) =>
unless item instanceof WorkspaceStore.SidebarItem
throw new Error("AccountSidebar:_itemComponents: sections contained an \
item which was not a SidebarItem")
<itemClass
key={item.id ? item.type}
if item.component
Component = item.component
<Component
key={item.id}
item={item}
sectionType={section.type}
select={item.id is @state.selected?.id }/>
select={item.id is @state.selected?.id } />
else if item.mailViewFilter
<AccountSidebarMailViewItem
key={item.id}
mailView={item.mailViewFilter}
select={item.mailViewFilter.isEqual(@state.selected)} />
else if item.sheet
<SidebarSheetItem
key={item.id}
item={item.sheet}
select={item.sheet.id is @state.selected?.id} />
else
throw new Error("AccountSidebar:_itemComponents: each item must have a \
custom component, or a sheet or mailViewFilter")
_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

@ -0,0 +1,136 @@
React = require 'react'
{Actions, AccountStore} = require("nylas-exports")
{ScrollRegion} = require("nylas-component-kit")
crypto = require 'crypto'
{RetinaImg} = require 'nylas-component-kit'
classNames = require 'classnames'
class AccountSwitcher extends React.Component
@displayName: 'AccountSwitcher'
constructor: (@props) ->
@state = @_getStateFromStores()
@state.showing = false
componentDidMount: =>
@unsubscribers = []
@unsubscribers.push AccountStore.listen @_onStoreChange
componentWillUnmount: =>
unsubscribe() for unsubscribe in @unsubscribers
render: =>
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)"
_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: =>
accounts: AccountStore.items()
account: AccountStore.current()
module.exports = AccountSwitcher

View file

@ -7,7 +7,7 @@ SettingsTabsView = require "./settings-tabs-view"
module.exports =
activate: (@state={}) ->
WorkspaceStore.defineSheet 'Settings', {root: true, supportedModes: ['list'], name: 'Plugins'},
WorkspaceStore.defineSheet 'Settings', {root: true, supportedModes: ['list']},
list: ['RootSidebar', 'SettingsSidebar', 'Settings']
ComponentRegistry.register SettingsTabsView,

View file

@ -34,11 +34,11 @@ class DraftListSidebarItem extends React.Component
<div className={classSet} onClick={@_onClick}>
{unread}
<div className="icon"><RetinaImg name={'drafts.png'} mode={RetinaImg.Mode.ContentIsMask} /></div>
<div className="name"> {@props.item.name}</div>
<div className="name"> Drafts</div>
</div>
_onClick: (event) =>
event.preventDefault()
Actions.selectRootSheet(@props.item)
Actions.selectRootSheet(WorkspaceStore.Sheet.Drafts)
module.exports = DraftListSidebarItem

View file

@ -1,6 +1,6 @@
_ = require 'underscore'
React = require "react"
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
{MailViewFilter, ComponentRegistry, WorkspaceStore} = require "nylas-exports"
{DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkStarButton, ThreadBulkToggleUnreadButton} = require "./thread-buttons"
{DraftDeleteButton} = require "./draft-buttons"
@ -13,9 +13,17 @@ DraftList = require './draft-list'
module.exports =
activate: (@state={}) ->
WorkspaceStore.defineSheet 'Drafts', {root: true, name: 'Drafts', sidebarComponent: DraftListSidebarItem},
WorkspaceStore.defineSheet 'Drafts', {root: true},
list: ['RootSidebar', 'DraftList']
@sidebarItem = new WorkspaceStore.SidebarItem
component: DraftListSidebarItem
sheet: WorkspaceStore.Sheet.Drafts
id: 'Drafts'
name: 'Drafts'
WorkspaceStore.addSidebarItem(@sidebarItem)
ComponentRegistry.register ThreadList,
location: WorkspaceStore.Location.ThreadList

View file

@ -5,6 +5,13 @@ NylasStore = require 'nylas-store'
Sheet = {}
Location = {}
SidebarItems = {}
class WorkspaceSidebarItem
constructor: ({@id, @component, @name, @sheet, @mailViewFilter}) ->
if not @sheet and not @mailViewFilter and not @component
throw new Error("WorkspaceSidebarItem: You must provide either a sheet \
component, or a mailViewFilter for the sidebar item named #{@name}")
###
Public: The WorkspaceStore manages Sheets and layout modes in the application.
@ -40,6 +47,9 @@ class WorkspaceStore extends NylasStore
@Location = Location = {}
@Sheet = Sheet = {}
@SidebarItem = WorkspaceSidebarItem
@SidebarItems = SidebarItems = {}
@_hiddenLocations = {}
@_sheetStack = []
@ -137,6 +147,20 @@ class WorkspaceStore extends NylasStore
return false unless loc
@_hiddenLocations[loc.id]?
sidebarItems: =>
_.values(@SidebarItems)
addSidebarItem: (item) =>
unless item instanceof WorkspaceSidebarItem
throw new Error("WorkspaceStore::addSidebarItem requires a `WorkspaceSidebarItem`")
@SidebarItems[item.id] = item
@triggerDebounced()
removeSidebarItem: (item) =>
delete @SidebarItems[item.id]
@triggerDebounced()
###
Managing Sheets
###