More account sidebar refactor + sections for unified inbox

- Refactors some of the old code which was 💩
  - Makes SidbarSection a factory for different types of items for the
    OutlineView
  - Decided not to create a OutlineViewItem.Model class since the only
    purpose it would serve would be to validate getters for props or for
    documentation, both of which are already done via React.PropTypes.
- Adds sections when looking at unified inbox and integrates with new
  mailbox perspecitve interface
- Updates OutlineViewItem a bit + styles
- Tests missing
This commit is contained in:
Juan Tejada 2016-01-18 23:18:19 -08:00
parent fbbc325a84
commit d592474073
18 changed files with 462 additions and 677 deletions

View file

@ -1,105 +0,0 @@
{WorkspaceStore,
FocusedPerspectiveStore,
ThreadCountsStore,
DraftCountStore,
DestroyCategoryTask,
Actions} = require 'nylas-exports'
_ = require 'underscore'
{OutlineViewItem} = require 'nylas-component-kit'
class MailboxPerspectiveSidebarItem
constructor: (@mailboxPerspective, @shortenedName, @children = []) ->
category = @mailboxPerspective.categories()[0]
@id = category?.id ? @mailboxPerspective.name
@name = @shortenedName ? @mailboxPerspective.name
@iconName = @mailboxPerspective.iconName
@dataTransferType = 'nylas-thread-ids'
@counterStyle = OutlineViewItem.CounterStyles.Alt if @mailboxPerspective.isInbox()
# Sidenote: I think treating the sidebar items as dumb bundles of data is a
# good idea. `count` /shouldn't/ be a function since if it's value changes,
# it wouldn't trigger a refresh or anything. It'd just be confusing if it
# could change. But making these all classes makes it feel like you should
# call these methods externally.
#
# Might be good to make a factory that returns OutlineViewItemModels instead
# of having classes here. eg: AccountSidebar.itemForPerspective(p) returns
# { count: X, isSelected: false, isDeleted: true}...
#
@count = @_count()
@selected = @_isSelected()
@deleted = @_isDeleted()
@collapsed = @_isCollapsed()
@
_count: =>
unreadCountEnabled = NylasEnv.config.get('core.workspace.showUnreadForAllCategories')
if @mailboxPerspective.isInbox() or unreadCountEnabled
return @mailboxPerspective.threadUnreadCount()
return 0
_isSelected: =>
(WorkspaceStore.rootSheet() is WorkspaceStore.Sheet.Threads and
FocusedPerspectiveStore.current().isEqual(@mailboxPerspective))
_isDeleted: =>
_.any @mailboxPerspective.categories(), (c) -> c.isDeleted
_isCollapsed: =>
key = "core.accountSidebarCollapsed.#{@id}"
NylasEnv.config.get(key)
onToggleCollapsed: =>
return unless @children.length > 0
key = "core.accountSidebarCollapsed.#{@id}"
@collapsed = not @_isCollapsed()
NylasEnv.config.set(key, @collapsed)
onDelete: =>
return if @category?.isDeleted is true
Actions.queueTask(new DestroyCategoryTask({category: @category}))
onDrop: (ids) =>
return unless ids
@mailboxPerspective.applyToThreads(ids)
shouldAcceptDrop: (event) =>
perspective = @mailboxPerspective
return false unless perspective
return false if perspective.isEqual(FocusedPerspectiveStore.current())
return false unless perspective.canApplyToThreads()
@dataTransferType in event.dataTransfer.types
onSelect: =>
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
Actions.focusMailboxPerspective(@mailboxPerspective)
class SheetSidebarItem
constructor: (@name, @iconName, @sheet) ->
@id = @sheet?.id ? @name
@selected = WorkspaceStore.rootSheet().id is @id
onSelect: =>
Actions.selectRootSheet(@sheet)
class DraftListSidebarItem extends SheetSidebarItem
constructor: ->
super
count: ->
DraftCountStore.count()
module.exports = {
MailboxPerspectiveSidebarItem
SheetSidebarItem
DraftListSidebarItem
}

View file

@ -1,23 +0,0 @@
{Actions, SyncbackCategoryTask, DestroyCategoryTask} = require 'nylas-exports'
class AccountSidebarSection
constructor: ({@title, @iconName, @items} = {}) ->
class CategorySidebarSection extends AccountSidebarSection
constructor: ({@title, @iconName, @account, @items} = {}) ->
onCreateItem: (displayName) =>
return unless @account
CategoryClass = @account.categoryClass()
category = new CategoryClass
displayName: displayName
accountId: @account.id
Actions.queueTask(new SyncbackCategoryTask({category}))
module.exports = {
AccountSidebarSection
CategorySidebarSection
}

View file

@ -1,149 +0,0 @@
NylasStore = require 'nylas-store'
_ = require 'underscore'
{DatabaseStore,
AccountStore,
ThreadCountsStore,
DraftCountStore,
WorkspaceStore,
MailboxPerspective,
FocusedPerspectiveStore,
DestroyCategoryTask,
CategoryHelpers,
CategoryStore} = require 'nylas-exports'
{AccountSidebarSection,
CategorySidebarSection} = require './account-sidebar-sections'
{DraftListSidebarItem,
MailboxPerspectiveSidebarItem} = require './account-sidebar-items'
Sections = {
"Accounts"
"Mailboxes"
"Categories"
}
class AccountSidebarStore extends NylasStore
constructor: ->
@_sections = {}
@_account = AccountStore.accounts()[0] # TODO Just to prevent a crash at launch
# @_account = FocusedPerspectiveStore.current().account
@_registerListeners()
@_updateAccountsSection()
@_updateSections()
currentAccount: ->
@_account
accountsSection: ->
@_sections[Sections.Accounts]
mailboxesSection: ->
@_sections[Sections.Mailboxes]
categoriesSection: ->
@_sections[Sections.Categories]
_registerListeners: ->
@listenTo ThreadCountsStore, @_updateSections
@listenTo DraftCountStore, @_updateSections
@listenTo CategoryStore, @_updateSections
@listenTo FocusedPerspectiveStore, @_onPerspectiveChanged
@configSubscription = NylasEnv.config.observe(
'core.workspace.showUnreadForAllCategories',
@_updateSections
)
@configSubscription = NylasEnv.config.observe(
'core.accountSidebarCollapsed',
@_updateSections
)
# TODO this needs to change
_onPerspectiveChanged: =>
account = FocusedPerspectiveStore.current().account
if account?.id isnt @_account?.id
@_account = account
@_updateSections()
@trigger()
_onAccountsChanged: =>
@_updateAccountsSection()
@trigger()
_updateSections: =>
@_updateAccountsSection()
@_updateMailboxesSection()
@_updateCategoriesSection()
@trigger()
_updateAccountsSection: =>
@_sections[Sections.Accounts] = new AccountSidebarSection(
title: 'Accounts'
items: []
)
_updateMailboxesSection: =>
return unless @_account
# Drafts are displayed via a `DraftListSidebarItem`
standardCategories = CategoryStore.standardCategories(@_account)
items = _.reject(standardCategories, (cat) => cat.name is "drafts")
.map (cat) =>
new MailboxPerspectiveSidebarItem(MailboxPerspective.forCategory(cat))
starredItem = new MailboxPerspectiveSidebarItem(MailboxPerspective.forStarred([@_account.id]))
draftsItem = new DraftListSidebarItem('Drafts', 'drafts.png', WorkspaceStore.Sheet.Drafts)
# Order correctly: Inbox, Starred, rest... , Drafts
items.splice(1, 0, starredItem)
items.push(draftsItem)
@_sections[Sections.Mailboxes] = new AccountSidebarSection(
title: 'Mailboxes'
items: items
)
_updateCategoriesSection: =>
return unless @_account
# Compute hierarchy for user categories using known "path" separators
# NOTE: This code uses the fact that userCategoryItems is a sorted set, eg:
#
# Inbox
# Inbox.FolderA
# Inbox.FolderA.FolderB
# Inbox.FolderB
#
items = []
seenItems = {}
for category in CategoryStore.userCategories(@_account)
# https://regex101.com/r/jK8cC2/1
itemKey = category.displayName.replace(/[./\\]/g, '/')
perspective = MailboxPerspective.forCategory(@_account, category)
parent = null
parentComponents = itemKey.split('/')
for i in [parentComponents.length..1] by -1
parentKey = parentComponents[0...i].join('/')
parent = seenItems[parentKey]
break if parent
if parent
itemDisplayName = category.displayName.substr(parentKey.length+1)
item = new MailboxPerspectiveSidebarItem(perspective, itemDisplayName)
parent.children.push(item)
else
item = new MailboxPerspectiveSidebarItem(perspective)
items.push(item)
seenItems[itemKey] = item
@_sections[Sections.Categories] = new CategorySidebarSection(
title: CategoryHelpers.categoryLabel(@_account)
iconName: CategoryHelpers.categoryIconName(@_account)
account: @_account
items: items
)
module.exports = new AccountSidebarStore()

View file

@ -1,7 +1,7 @@
_ = require 'underscore'
React = require 'react'
{OutlineView, ScrollRegion} = require 'nylas-component-kit'
AccountSidebarStore = require '../account-sidebar-store'
SidebarStore = require '../sidebar-store'
class AccountSidebar extends React.Component
@ -17,7 +17,7 @@ class AccountSidebar extends React.Component
componentDidMount: =>
@unsubscribers = []
@unsubscribers.push AccountSidebarStore.listen @_onStoreChange
@unsubscribers.push SidebarStore.listen @_onStoreChange
componentWillUnmount: =>
unsubscribe() for unsubscribe in @unsubscribers
@ -26,19 +26,21 @@ class AccountSidebar extends React.Component
@setState @_getStateFromStores()
_getStateFromStores: =>
sections: [
AccountSidebarStore.mailboxesSection()
AccountSidebarStore.categoriesSection()
]
standardSection: SidebarStore.standardSection()
userSections: SidebarStore.userSections()
_renderSections: =>
@state.sections.map (section) =>
<OutlineView key={section.label} {...section} />
_renderUserSections: (sections) =>
sections.map (section) =>
<OutlineView key={section.title} {...section} />
render: =>
standardSection = @state.standardSection
userSections = @state.userSections
<ScrollRegion className="account-sidebar" >
<div className="account-sidebar-sections">
{@_renderSections()}
<OutlineView {...standardSection} />
{@_renderUserSections(userSections)}
</div>
</ScrollRegion>

View file

@ -1,5 +1,5 @@
React = require 'react'
AccountSidebarStore = require './account-sidebar-store'
SidebarStore = require '../sidebar-store'
{Actions, AccountStore} = require("nylas-exports")
{RetinaImg} = require 'nylas-component-kit'
crypto = require 'crypto'
@ -129,6 +129,6 @@ class AccountSwitcher extends React.Component
_getStateFromStores: =>
accounts: AccountStore.accounts()
account: AccountSidebarStore.currentAccount()
account: SidebarStore.currentAccount()
module.exports = AccountSwitcher

View file

@ -0,0 +1,11 @@
Reflux = require 'reflux'
Actions = [
'selectAccount'
]
for key in Actions
Actions[key] = Reflux.createAction(name)
Actions[key].sync = true
module.exports = Actions

View file

@ -0,0 +1,114 @@
_ = require 'underscore'
{WorkspaceStore,
MailboxPerspective,
FocusedPerspectiveStore,
DraftCountStore,
DestroyCategoryTask,
Actions} = require 'nylas-exports'
{OutlineViewItem} = require 'nylas-component-kit'
idForCategories = (categories) ->
categories.map((cat) -> cat.id).join('-')
countForItem = (perspective) ->
unreadCountEnabled = NylasEnv.config.get('core.workspace.showUnreadForAllCategories')
if perspective.isInbox() or unreadCountEnabled
return perspective.threadUnreadCount()
return 0
isItemSelected = (perspective) ->
(WorkspaceStore.rootSheet() is WorkspaceStore.Sheet.Threads and
FocusedPerspectiveStore.current().isEqual(perspective))
isItemDeleted = (perspective) ->
_.any perspective.categories(), (c) -> c.isDeleted
isItemCollapsed = (id) ->
key = "core.accountSidebarCollapsed.#{id}"
NylasEnv.config.get(key)
class SidebarItem
@forPerspective: (id, perspective, {children, deletable, name} = {}) ->
children ?= []
counterStyle = OutlineViewItem.CounterStyles.Alt if perspective.isInbox()
dataTransferType = 'nylas-thread-ids'
if deletable
onDeleteItem = (item) ->
# TODO Delete multiple categories at once
return if item.perspective.categories.length > 1
return if item.deleted is true
category = item.perspective.categories[0]
Actions.queueTask(new DestroyCategoryTask({category: category}))
return {
id: id
name: name ? perspective.name
count: countForItem(perspective)
iconName: perspective.iconName
children: children
perspective: perspective
selected: isItemSelected(perspective)
collapsed: isItemCollapsed(id)
deleted: isItemDeleted(perspective)
counterStyle: counterStyle
dataTransferType: dataTransferType
onDelete: onDeleteItem
onToggleCollapsed: (item) ->
return unless item.children.length > 0
key = "core.accountSidebarCollapsed.#{item.id}"
NylasEnv.config.set(key, not item.collapsed)
onDrop: (item, ids) ->
return unless ids
item.perspective.applyToThreads(ids)
shouldAcceptDrop: (item, event) ->
perspective = item.perspective
return false unless perspective
return false if perspective.isEqual(FocusedPerspectiveStore.current())
return false unless perspective.canApplyToThreads()
item.dataTransferType in event.dataTransfer.types
onSelect: (item) ->
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
Actions.focusMailboxPerspective(item.perspective)
}
@forCategories: (categories = [], opts = {}) ->
id = idForCategories(categories)
perspective = MailboxPerspective.forCategories(categories)
@forPerspective(id, perspective, opts)
@forStarred: (accountIds, opts = {}) ->
perspective = MailboxPerspective.forStarred(accountIds)
id = 'Starred'
id += "-#{opts.name}" if opts.name
@forPerspective(id, perspective, opts)
@forSheet: (name, iconName, sheet, count, children = []) ->
id = sheet?.id ? name
return {
id,
name,
iconName,
count,
sheet,
children,
onSelect: (item) ->
Actions.selectRootSheet(item.sheet)
}
@forDrafts: ({accountId, name, children} = {}) ->
sheet = WorkspaceStore.Sheet.Drafts
iconName = 'drafts.png'
name ?= 'Drafts'
count = if accountId?
DraftCountStore.count(accountId)
else
DraftCountStore.totalCount()
@forSheet(name, iconName, sheet, count)
module.exports = SidebarItem

View file

@ -0,0 +1,134 @@
_ = require 'underscore'
{Actions,
SyncbackCategoryTask,
DestroyCategoryTask,
CategoryHelpers,
CategoryStore,
Category} = require 'nylas-exports'
SidebarItem = require './sidebar-item'
class SidebarSection
@empty: (title)->
return {
title,
items: []
}
@standardSectionForAccount: (account) ->
cats = CategoryStore.standardCategories(account)
items = _
.reject(cats, (cat) -> cat.name is 'drafts')
.map (cat) => SidebarItem.forCategories([cat])
starredItem = SidebarItem.forStarred([account.id])
draftsItem = SidebarItem.forDrafts(accountId: account.id)
# Order correctly: Inbox, Starred, rest... , Drafts
items.splice(1, 0, starredItem)
items.push(draftsItem)
return {
title: 'Mailboxes'
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 @standardSectionForAccount(accounts[0]) if accounts.length is 1
# TODO Decide standard items for the unified case
inboxItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getStandardCategory(acc, 'inbox')),
children: accounts.map (acc) ->
cat = CategoryStore.getStandardCategory(acc, 'inbox')
SidebarItem.forCategories([cat], name: acc.label)
)
sentItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getStandardCategory(acc, 'sent')),
children: accounts.map (acc) ->
cat = CategoryStore.getStandardCategory(acc, 'sent')
SidebarItem.forCategories([cat], name: acc.label)
)
archiveItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getArchiveCategory(acc)),
children: accounts.map (acc) ->
cat = CategoryStore.getArchiveCategory(acc)
SidebarItem.forCategories([cat], name: acc.label)
)
trashItem = SidebarItem.forCategories(
(accounts.map (acc)-> CategoryStore.getTrashCategory(acc)),
children: accounts.map (acc) ->
cat = CategoryStore.getTrashCategory(acc)
SidebarItem.forCategories([cat], name: acc.label)
)
starredItem = SidebarItem.forStarred(_.pluck(accounts, 'id'),
children: accounts.map (acc) -> SidebarItem.forStarred([acc.id], name: acc.label)
)
draftsItem = SidebarItem.forDrafts(
children: accounts.map (acc) ->
SidebarItem.forDrafts(accountId: acc.id, name: acc.label)
)
items = [
inboxItem
starredItem
sentItem
archiveItem
trashItem
draftsItem
]
return {
title: 'Mailboxes'
items: items
}
@forUserCategories: (account) ->
return unless account
# Compute hierarchy for user categories using known "path" separators
# NOTE: This code uses the fact that userCategoryItems is a sorted set, eg:
#
# Inbox
# Inbox.FolderA
# Inbox.FolderA.FolderB
# Inbox.FolderB
#
items = []
seenItems = {}
for category in CategoryStore.userCategories(account)
# https://regex101.com/r/jK8cC2/1
itemKey = category.displayName.replace(/[./\\]/g, '/')
parent = null
parentComponents = itemKey.split('/')
for i in [parentComponents.length..1] by -1
parentKey = parentComponents[0...i].join('/')
parent = seenItems[parentKey]
break if parent
if parent
itemDisplayName = category.displayName.substr(parentKey.length+1)
item = SidebarItem.forCategories([category], name: itemDisplayName)
parent.children.push(item)
else
item = SidebarItem.forCategories([category])
items.push(item)
seenItems[itemKey] = item
return {
title: CategoryHelpers.categoryLabel(account)
iconName: CategoryHelpers.categoryIconName(account)
items: items
onCreateItem: (displayName) ->
category = new Category
displayName: displayName
accountId: account.id
Actions.queueTask(new SyncbackCategoryTask({category}))
}
module.exports = SidebarSection

View file

@ -0,0 +1,68 @@
NylasStore = require 'nylas-store'
_ = require 'underscore'
{DatabaseStore,
AccountStore,
ThreadCountsStore,
DraftCountStore,
WorkspaceStore,
MailboxPerspective,
FocusedPerspectiveStore,
DestroyCategoryTask,
CategoryHelpers,
CategoryStore} = require 'nylas-exports'
SidebarSection = require './sidebar-section'
SidebarActions = require './sidebar-actions'
Sections = {
"Standard",
"User"
}
class SidebarStore extends NylasStore
constructor: ->
@_sections = {}
# @_account = AccountStore.accounts()[0]
@_account = FocusedPerspectiveStore.current().account
@_registerListeners()
@_updateSections()
standardSection: ->
@_sections[Sections.Standard]
userSections: ->
@_sections[Sections.User]
_registerListeners: ->
@listenTo SidebarActions.selectAccount, @_onAccountSelected
@listenTo AccountStore, @_updateSections
@listenTo WorkspaceStore, @_updateSections
@listenTo ThreadCountsStore, @_updateSections
@listenTo DraftCountStore, @_updateSections
@listenTo CategoryStore, @_updateSections
@listenTo FocusedPerspectiveStore, @_updateSections
@configSubscription = NylasEnv.config.observe(
'core.workspace.showUnreadForAllCategories',
@_updateSections
)
@configSubscription = NylasEnv.config.observe(
'core.accountSidebarCollapsed',
@_updateSections
)
return
_onAccountSelected: (account) =>
if @_account isnt account
@_account = account
@_updateSections()
_updateSections: =>
accounts = if @_account? then [@_account] else AccountStore.accounts()
@_sections[Sections.Standard] = SidebarSection.standardSectionForAccounts(accounts)
@_sections[Sections.User] = accounts.map (acc) ->
SidebarSection.forUserCategories(acc)
@trigger()
module.exports = new SidebarStore()

View file

@ -1,283 +0,0 @@
AccountSidebarStore = require '../lib/account-sidebar-store'
{Folder, WorkspaceStore, CategoryStore, AccountStore} = require 'nylas-exports'
NylasEnv.testOrganizationUnit = 'folder'
describe "AccountSidebarStore", ->
describe "sections", ->
it "should return the correct output", ->
account = AccountStore.accounts()[0]
account.organizationUnit = 'folder'
# Converting to JSON removes keys whose values are `undefined`,
# makes the output smaller and easier to visually compare.
jsonAcc = JSON.parse(JSON.stringify(account))
AccountSidebarStore._account = account
spyOn(CategoryStore, 'standardCategories').andReturn [
new Folder(displayName:'Inbox', clientId: '1', name: 'inbox')
new Folder(displayName:'Sent', clientId: '3', name: 'sent')
new Folder(displayName:'Important', clientId: '4', name: 'important')
]
spyOn(CategoryStore, 'userCategories').andReturn [
new Folder(displayName:'A', clientId: 'a')
new Folder(displayName:'B', clientId: 'b')
new Folder(displayName:'A/B', clientId: 'a+b')
new Folder(displayName:'A.D', clientId: 'a+d')
new Folder(displayName:'A\\E', clientId: 'a+e')
new Folder(displayName:'B/C', clientId: 'b+c')
new Folder(displayName:'A/B/C', clientId: 'a+b+c')
new Folder(displayName:'A/B-C', clientId: 'a+b-c')
]
spyOn(WorkspaceStore, 'sidebarItems').andCallFake ->
return [
new WorkspaceStore.SidebarItem
component: {}
sheet: 'stub'
id: 'Drafts'
name: 'Drafts'
]
# Note: If you replace this JSON with new JSON, you may have to replace
# A\E with A\\E manually.
expected = [{
label: 'Mailboxes',
items: [
{
id: '1',
name: 'Inbox',
mailboxPerspective: {
name: 'Inbox',
category: {
client_id: '1',
name: 'inbox',
display_name: 'Inbox',
id: '1'
},
iconName: 'inbox.png'
account: jsonAcc
},
children: [
],
unreadCount: null
},
{
id: 'starred',
name: 'Starred',
mailboxPerspective: {
name: 'Starred',
iconName: 'starred.png'
account: jsonAcc
},
children: [
]
},
{
id: '3',
name: 'Sent',
mailboxPerspective: {
name: 'Sent',
category: {
client_id: '3',
name: 'sent',
display_name: 'Sent',
id: '3'
},
iconName: 'sent.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
},
{
id: '4',
name: 'Important',
mailboxPerspective: {
name: 'Important',
category: {
client_id: '4',
name: 'important',
display_name: 'Important',
id: '4'
},
iconName: 'important.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
},
{
id: 'Drafts',
component: {
},
name: 'Drafts',
sheet: 'stub',
children: [
]
}
]
},
{
label: 'Folders',
items: [
{
id: 'a',
name: 'A',
mailboxPerspective: {
name: 'A',
category: {
client_id: 'a',
display_name: 'A',
id: 'a'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
{
id: 'a+b',
name: 'B',
mailboxPerspective: {
name: 'A/B',
category: {
client_id: 'a+b',
display_name: 'A/B',
id: 'a+b'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
{
id: 'a+b+c',
name: 'C',
mailboxPerspective: {
name: 'A/B/C',
category: {
client_id: 'a+b+c',
display_name: 'A/B/C',
id: 'a+b+c'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
}
],
unreadCount: 0
},
{
id: 'a+d',
name: 'D',
mailboxPerspective: {
name: 'A.D',
category: {
client_id: 'a+d',
display_name: 'A.D',
id: 'a+d'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
},
{
id: 'a+e',
name: 'E',
mailboxPerspective: {
name: 'A\\E',
category: {
client_id: 'a+e',
display_name: 'A\\E',
id: 'a+e'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
},
{
id: 'a+b-c',
name: 'B-C',
mailboxPerspective: {
name: 'A/B-C',
category: {
client_id: 'a+b-c',
display_name: 'A/B-C',
id: 'a+b-c'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
}
],
unreadCount: 0
},
{
id: 'b',
name: 'B',
mailboxPerspective: {
name: 'B',
category: {
client_id: 'b',
display_name: 'B',
id: 'b'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
{
id: 'b+c',
name: 'C',
mailboxPerspective: {
name: 'B/C',
category: {
client_id: 'b+c',
display_name: 'B/C',
id: 'b+c'
},
iconName: 'folder.png'
account: jsonAcc
},
children: [
],
unreadCount: 0
}
],
unreadCount: 0
}
],
iconName: 'folder.png'
}]
AccountSidebarStore._updateSections()
# Converting to JSON removes keys whose values are `undefined`,
# makes the output smaller and easier to visually compare.
output = JSON.parse(JSON.stringify(AccountSidebarStore.sections()))
expect(output).toEqual(expected)

View file

@ -1,7 +1,7 @@
React = require 'react/addons'
TestUtils = React.addons.TestUtils
AccountSwitcher = require './../lib/account-switcher'
AccountSidebarStore = require './../lib/account-sidebar-store'
AccountSwitcher = require './../lib/components/account-switcher'
SidebarStore = require './../lib/sidebar-store'
{AccountStore} = require 'nylas-exports'
describe "AccountSwitcher", ->
@ -17,7 +17,7 @@ describe "AccountSwitcher", ->
label: "work"
}
]
spyOn(AccountSidebarStore, "currentAccount").andReturn account
spyOn(SidebarStore, "currentAccount").andReturn account
switcher = TestUtils.renderIntoDocument(
<AccountSwitcher />

View file

@ -8,41 +8,31 @@ import RetinaImg from './retina-img';
const CounterStyles = {
Default: 'def',
Alt: 'alt',
}
};
// TODO Docs
class OutlineViewItem extends Component {
static displayName = 'OutlineView'
static propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.array,
name: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
count: PropTypes.number,
counterStyle: PropTypes.string,
dataTransferType: PropTypes.string,
collapsed: PropTypes.bool,
deleted: PropTypes.bool,
selected: PropTypes.bool,
shouldAcceptDrop: PropTypes.func,
onToggleCollapsed: PropTypes.func,
onDrop: PropTypes.func,
onSelect: PropTypes.func,
onDelete: PropTypes.func,
}
static defaultProps = {
children: [],
count: 0,
counterStyle: CounterStyles.Default,
dataTransferType: '',
collapsed: false,
deleted: false,
selected: false,
shouldAcceptDrop: ()=> false,
onToggleCollapsed: ()=> {},
onDrop: ()=> {},
onSelect: ()=> {},
item: PropTypes.shape({
id: PropTypes.string.isRequired,
children: PropTypes.array.isRequired,
name: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
count: PropTypes.number,
counterStyle: PropTypes.string,
dataTransferType: PropTypes.string,
collapsed: PropTypes.bool,
deleted: PropTypes.bool,
selected: PropTypes.bool,
shouldAcceptDrop: PropTypes.func,
onToggleCollapsed: PropTypes.func,
onDrop: PropTypes.func,
onSelect: PropTypes.func,
onDelete: PropTypes.func,
}).isRequired,
}
state = {
@ -50,7 +40,7 @@ class OutlineViewItem extends Component {
}
componentDidMount() {
if (this.props.onDelete != null) {
if (this.props.item.onDelete) {
React.findDOMNode(this).addEventListener('contextmenu', this._onShowContextMenu);
}
}
@ -61,7 +51,7 @@ class OutlineViewItem extends Component {
}
componentWillUnmount() {
if (this.props.onDelete != null) {
if (this.props.item.onDelete) {
React.findDOMNode(this).removeEventListener('contextmenu', this._onShowContextMenu);
}
}
@ -69,30 +59,25 @@ class OutlineViewItem extends Component {
static CounterStyles = CounterStyles;
// Handlers
// Helpers
_onShowContextMenu = ()=> {
const item = this.props;
const name = item.name;
const {remote} = require('electron');
const {Menu, MenuItem} = remote.require('electron');
const menu = new Menu();
menu.append(new MenuItem({
name: `Delete ${name}`,
click: ()=> {
item.onDelete(item.id);
},
}));
menu.popup(remote.getCurrentWindow());
_runCallback = (method, ...args)=> {
const item = this.props.item;
if (item[method]) {
item[method](item, ...args);
}
}
// Handlers
_onDragStateChange = ({isDropping})=> {
this.setState({isDropping});
}
_onDrop = (event)=> {
const jsonString = event.dataTransfer.getData(this.props.dataTransferType);
const item = this.props.item;
const jsonString = event.dataTransfer.getData(item.dataTransferType);
let ids;
try {
ids = JSON.parse(jsonString);
@ -101,18 +86,44 @@ class OutlineViewItem extends Component {
}
if (!ids) return;
this.props.onDrop(ids);
this._runCallback('onDrop', ids);
}
_onToggleCollapsed = ()=> {
this._runCallback('onToggleCollapsed');
}
_onClick = (event)=> {
event.preventDefault();
this.props.onSelect(this.props.id);
this._runCallback('onSelect');
}
_onDelete = ()=> {
this._runCallback('onDelete');
}
_shouldAcceptDrop = (event)=> {
this._runCallback('shouldAcceptDrop', event);
}
_onShowContextMenu = ()=> {
const item = this.props.item;
const name = item.name;
const {remote} = require('electron');
const {Menu, MenuItem} = remote.require('electron');
const menu = new Menu();
menu.append(new MenuItem({
label: `Delete ${name}`,
click: this._onDelete,
}));
menu.popup(remote.getCurrentWindow());
}
// Renderers
_renderCount(item = this.props) {
_renderCount(item = this.props.item) {
if (!item.count) return <span></span>;
const className = classnames({
'item-count-box': true,
@ -121,7 +132,7 @@ class OutlineViewItem extends Component {
return <div className={className}>{item.count}</div>;
}
_renderIcon(item = this.props) {
_renderIcon(item = this.props.item) {
return (
<RetinaImg
name={item.iconName}
@ -130,7 +141,7 @@ class OutlineViewItem extends Component {
);
}
_renderItem(item = this.props, state = this.state) {
_renderItem(item = this.props.item, state = this.state) {
const containerClass = classnames({
'item': true,
'selected': item.selected,
@ -143,7 +154,7 @@ class OutlineViewItem extends Component {
className={containerClass}
onClick={this._onClick}
id={item.id}
shouldAcceptDrop={item.shouldAcceptDrop}
shouldAcceptDrop={this._shouldAcceptDrop}
onDragStateChange={this._onDragStateChange}
onDrop={this._onDrop}>
{this._renderCount()}
@ -153,12 +164,12 @@ class OutlineViewItem extends Component {
);
}
_renderChildren(item = this.props) {
_renderChildren(item = this.props.item) {
if (item.children.length > 0 && !item.collapsed) {
return (
<section key={`${item.id}-children`}>
<section className="item-children" key={`${item.id}-children`}>
{item.children.map(
child => <OutlineViewItem key={child.id} {...child} />
child => <OutlineViewItem key={child.id} item={child} />
)}
</section>
);
@ -167,14 +178,15 @@ class OutlineViewItem extends Component {
}
render() {
const item = this.props;
const item = this.props.item;
return (
<div>
<span className="item-container">
<DisclosureTriangle
collapsed={item.collapsed}
visible={item.children.length > 0}
onToggleCollapsed={item.onToggleCollapsed} />
onToggleCollapsed={this._onToggleCollapsed} />
{this._renderItem()}
</span>
{this._renderChildren()}

View file

@ -6,6 +6,7 @@ import RetinaImg from './retina-img';
import OutlineViewItem from './outline-view-item';
// TODO Docs
class OutlineView extends Component {
static displayName = 'OutlineView'
@ -26,6 +27,9 @@ class OutlineView extends Component {
showCreateInput: false,
}
// Handlers
_onCreateButtonMouseDown = ()=> {
this._clickingCreateButton = true;
}
@ -51,6 +55,9 @@ class OutlineView extends Component {
}
}
// Renderers
_renderCreateButton() {
const title = this.props.title;
return (
@ -84,7 +91,7 @@ class OutlineView extends Component {
type="text"
tabIndex="1"
className="add-item-input"
onKeyDown={_.partial(this._onInputKeyDown, _, section)}
onKeyDown={this._onInputKeyDown}
onBlur={this._onInputBlur}
placeholder={placeholder}/>
</div>
@ -94,9 +101,7 @@ class OutlineView extends Component {
_renderItems() {
return this.props.items.map(item => (
<OutlineViewItem
key={item.id}
{...item} />
<OutlineViewItem key={item.id} item={item} />
));
}

View file

@ -88,6 +88,7 @@ class Category extends Model
@
displayType: ->
AccountStore = require '../stores/account-store'
if AccountStore.accountForId(@category.accountId).usesLabels()
return 'label'
else

View file

@ -38,7 +38,7 @@ class CategoryStore extends NylasStore
all = all.concat(_.values(categories))
all
# Public: Returns all of the standard categories for the current account.
# Public: Returns all of the standard categories for the given account.
#
standardCategories: (accountOrId) ->
@_standardCategories[asAccountId(accountOrId)] ? []

View file

@ -1,10 +1,12 @@
Reflux = require 'reflux'
_ = require 'underscore'
FocusedPerspectiveStore = require './focused-perspective-store'
DatabaseStore = require './database-store'
DraftStore = require './draft-store'
Rx = require 'rx-lite'
NylasStore = require 'nylas-store'
Actions = require '../actions'
Message = require '../models/message'
Account = require '../models/account'
DatabaseStore = require './database-store'
AccountStore = require './account-store'
FocusedPerspectiveStore = require './focused-perspective-store'
###
Public: The DraftCountStore exposes a simple API for getting the number of
@ -17,38 +19,34 @@ The DraftCountStore is only available in the main window.
if not NylasEnv.isMainWindow() and not NylasEnv.inSpecMode() then return
DraftCountStore = Reflux.createStore
init: ->
@listenTo FocusedPerspectiveStore, @_onFocusedPerspectiveChanged
@listenTo DraftStore, @_onDraftChanged
@_view = FocusedPerspectiveStore.current()
@_count = null
_.defer => @_fetchCount()
class DraftCountStore extends NylasStore
# Public: Returns the number of drafts in the user's mailbox
count: ->
@_count
constructor: ->
@_counts = {}
@_total = 0
@_disposable = Rx.Observable.fromQuery(
DatabaseStore.findAll(Message).where([Message.attributes.draft.equal(true)])
).subscribe(@_onDraftsChanged)
_onFocusedPerspectiveChanged: ->
view = FocusedPerspectiveStore.current()
if view? and not(view.isEqual(@_view))
@_view = view
@_onDraftChanged()
totalCount: ->
@_total
_onDraftChanged: ->
@_fetchCountDebounced ?= _.debounce(@_fetchCount, 250)
@_fetchCountDebounced()
# Public: Returns the number of drafts for the given account
count: (accountOrId)->
return 0 unless accountOrId
accountId = if accountOrId instanceof Account
accountOrId.id
else
accountOrId
@_counts[accountId]
_fetchCount: ->
account = @_view?.account
matchers = [
Message.attributes.draft.equal(true)
]
matchers.push(Message.attributes.accountId.equal(account.accountId)) if account?
_onDraftsChanged: (drafts) =>
@_total = 0
@_counts = {}
for account in AccountStore.accounts()
@_counts[account.id] = _.where(drafts, accountId: account.id).length
@_total += @_counts[account.id]
@trigger()
DatabaseStore.count(Message, matchers).then (count) =>
return if @_count is count
@_count = count
@trigger()
module.exports = DraftCountStore
module.exports = new DraftCountStore()

View file

@ -160,7 +160,7 @@ class CategoryMailboxPerspective extends MailboxPerspective
if @_categories[0].name
@iconName = "#{@_categories[0].name}.png"
else
@iconName = CategoryHelpers.categoryIconName(@accountIds[0])
@iconName = CategoryHelpers.categoryIconName(AccountStore.accountForId(@accountIds[0]))
@

View file

@ -8,11 +8,11 @@
section {
margin-bottom: @padding-base-vertical;
}
section {
padding-left: @padding-base-horizontal * 1.3;
margin-bottom: 0;
}
section.item-children {
padding-left: @padding-base-horizontal * 1.3;
margin-bottom: 0;
}
.heading {