fix(category-store): Fix issue with observables in CategoryStore

- Removes use of observables from category store and keeps a big cache
  of categories per account
- Upates Category Observables with new helper observables
- Updates CategoryPicker and AccountSidebarStore to use observables
- Misc fixes
This commit is contained in:
Juan Tejada 2016-01-08 17:49:27 -08:00
parent 2beeccdecd
commit d265bf1248
16 changed files with 139 additions and 66 deletions

View file

@ -2,7 +2,6 @@ NylasStore = require 'nylas-store'
_ = require 'underscore'
{DatabaseStore,
AccountStore,
CategoryStore,
ThreadCountsStore,
WorkspaceStore,
Actions,
@ -16,10 +15,14 @@ _ = require 'underscore'
CategoryHelpers,
Thread} = require 'nylas-exports'
{Categories} = require 'nylas-observables'
class AccountSidebarStore extends NylasStore
constructor: ->
@_sections = []
@_account = AccountStore.accounts()[0] # TODO Temporarily, should be null
@_standardCategories = []
@_userCategories = []
@_registerListeners()
@_updateSections()
@ -41,7 +44,6 @@ class AccountSidebarStore extends NylasStore
_registerListeners: ->
@listenTo WorkspaceStore, @_updateSections
@listenTo CategoryStore, @_updateSections
@listenTo ThreadCountsStore, @_updateSections
@listenTo FocusedPerspectiveStore, => @trigger()
@listenTo Actions.selectAccount, @_onSelectAccount
@ -50,16 +52,31 @@ class AccountSidebarStore extends NylasStore
@_updateSections
)
_registerObservables: ->
@_disposables ?= []
@_disposables.forEach (disp) -> disp.dispose()
@_disposables = [
Categories.standard(@_currentAccount).subscribe(@_onStandardCategoriesChanged),
Categories.user(@_currentAccount).subscribe(@_onUserCategoriesChanged)
]
_onSelectAccount: (accountId)=>
@_account = AccountStore.accountForId(accountId)
@_registerObservables()
@trigger()
_onStandardCategoriesChanged: (categories) ->
@_standardCategories = categories
@_updateSections()
_onUserCategoriesChanged: (categories) ->
@_userCategories = categories
@_updateSections()
_updateSections: =>
# TODO As it is now, if the current account is null, we will display the
# categories for all accounts.
# Update this to reflect UI decision for sidebar
userCategories = CategoryStore.userCategories(@_account)
userCategoryItems = _.map(userCategories, @_sidebarItemForCategory)
# Compute hierarchy for userCategoryItems using known "path" separators
# NOTE: This code uses the fact that userCategoryItems is a sorted set, eg:
@ -71,7 +88,7 @@ class AccountSidebarStore extends NylasStore
#
userCategoryItemsHierarchical = []
userCategoryItemsSeen = {}
for category in userCategories
for category in @_userCategories
# https://regex101.com/r/jK8cC2/1
itemKey = category.displayName.replace(/[./\\]/g, '/')
@ -93,8 +110,7 @@ class AccountSidebarStore extends NylasStore
# Our drafts are displayed via the `DraftListSidebarItem` which
# is loading into the `Drafts` Sheet.
standardCategories = CategoryStore.standardCategories(@_account)
standardCategories = _.reject standardCategories, (category) =>
standardCategories = _.reject @_standardCategories, (category) =>
category.name is "drafts"
standardCategoryItems = _.map standardCategories, (cat) => @_sidebarItemForCategory(cat)

View file

@ -1,7 +1,8 @@
React = require 'react'
AccountSidebarStore = require './account-sidebar-store'
{Actions, AccountStore} = require("nylas-exports")
crypto = require 'crypto'
{RetinaImg} = require 'nylas-component-kit'
crypto = require 'crypto'
classNames = require 'classnames'
class AccountSwitcher extends React.Component

View file

@ -22,37 +22,55 @@ React = require 'react'
KeyCommandsRegion,
LabelColorizer} = require 'nylas-component-kit'
{Categories} = require 'nylas-observables'
# This changes the category on one or more threads.
class CategoryPicker extends React.Component
@displayName: "CategoryPicker"
@containerRequired: false
constructor: (@props) ->
@_account = AccountStore.accountForItems(@_threads(@props))
@_categories = []
@_standardCategories = []
@_userCategories = []
@state = _.extend @_recalculateState(@props), searchValue: ""
@contextTypes:
sheetDepth: React.PropTypes.number
componentDidMount: =>
@_registerObservables()
# If the threads we're picking categories for change, (like when they
# get their categories updated), we expect our parents to pass us new
# props. We don't listen to the DatabaseStore ourselves.
componentWillReceiveProps: (nextProps) ->
@_account = AccountStore.accountForItems(@_threads(nextProps))
@_registerObservables()
@setState @_recalculateState(nextProps)
componentWillUnmount: =>
return unless @unsubscribers
unsubscribe() for unsubscribe in @unsubscribers
@_unregisterObservables()
_registerObservables: =>
@_unregisterObservables()
@disposables = []
@disposables.push(
Categories.forAccount(@_account).subscribe(@_onCategoriesChanged)
)
_unregisterObservables: =>
return unless @disposables
disp.dispose() for disp in @disposables
_keymapHandlers: ->
"application:change-category": @_onOpenCategoryPopover
render: =>
return <span></span> if @state.disabled or not @_account?
btnClasses = "btn btn-toolbar"
btnClasses += " btn-disabled" if @state.disabled
button = <button className={btnClasses} title={tooltip}>
<RetinaImg name={img} mode={RetinaImg.Mode.ContentIsMask}/>
</button>
return button if @state.disabled or not @_account?
if @_account?.usesLabels()
img = "toolbar-tag.png"
@ -69,6 +87,12 @@ class CategoryPicker extends React.Component
if @state.isPopoverOpen then tooltip = ""
button = (
<button className={btnClasses} title={tooltip}>
<RetinaImg name={img} mode={RetinaImg.Mode.ContentIsMask}/>
</button>
)
headerComponents = [
<input type="text"
tabIndex="1"
@ -235,10 +259,15 @@ class CategoryPicker extends React.Component
_onPopoverClosed: =>
@setState isPopoverOpen: false
_recalculateState: (props=@props, {searchValue}={}) =>
threads = @_threads(props)
@_account = AccountStore.accountForItems(threads)
_onCategoriesChanged: (categories) =>
@_categories = categories
@_standardCategories = categories.filter (cat) -> cat.isStandardCategory()
@_userCategories = categories.filter (cat) -> cat.isUserCategory()
@setState @_recalculateState()
_recalculateState: (props = @props, {searchValue}={}) =>
return {disabled: true} unless @_account
threads = @_threads(props)
searchValue = searchValue ? @state?.searchValue ? ""
numThreads = threads.length
@ -246,11 +275,11 @@ class CategoryPicker extends React.Component
return {categoryData: [], searchValue}
if @_account.usesLabels()
categories = CategoryStore.categories(@_account)
categories = @_categories
else
categories = CategoryStore.standardCategories(@_account)
categories = @_standardCategories
.concat([{divider: true, id: "category-divider"}])
.concat(CategoryStore.userCategories(@_account))
.concat(@_userCategories)
usageCount = @_categoryUsageCount(props, categories)

View file

@ -1,6 +1,6 @@
React = require 'react/addons'
classNames = require 'classnames'
{Actions, WorkspaceStore} = require 'nylas-exports'
{Actions, WorkspaceStore, FocusedMailViewStore} = require 'nylas-exports'
{Menu, RetinaImg, KeyCommandsRegion} = require 'nylas-component-kit'
SearchSuggestionStore = require './search-suggestion-store'
_ = require 'underscore'

View file

@ -7,6 +7,7 @@ _ = require 'underscore'
AccountStore,
MutableQuerySubscription,
QueryResultSetView,
FocusedMailViewStore,
DatabaseStore} = require 'nylas-exports'
class DraftListStore extends NylasStore

View file

@ -1,6 +1,6 @@
{Category, Label} = require 'nylas-exports'
fdescribe 'Category', ->
describe 'Category', ->
describe '_initCategoryTypes', ->
@ -22,6 +22,15 @@ fdescribe 'Category', ->
expect(cat.isHiddenCategory()).toBe false
expect(cat.isLockedCategory()).toBe false
it 'assigns type for `important` category when should not show important', ->
cat = new Label
cat.name = 'important'
cat._initCategoryTypes()
expect(cat.isUserCategory()).toBe false
expect(cat.isStandardCategory(false)).toBe false
expect(cat.isHiddenCategory()).toBe true
expect(cat.isLockedCategory()).toBe false
it 'assigns type correctly when it is a hidden category', ->
cat = new Label
cat.name = 'archive'

View file

@ -44,7 +44,7 @@ class MailImportantIcon extends React.Component
onClick={@_onToggleImportant}></div>
_account: =>
AccountStore.accountForId(@state.thread.accountId)
AccountStore.accountForId(@props.thread.accountId)
_onToggleImportant: (event) =>
category = CategoryStore.getStandardCategory(@_account(), 'important')

View file

@ -95,7 +95,22 @@ class Category extends Model
if @name in HiddenCategoryNames
@types.push @constructor.Types.Hidden
# Define getter for isStandardCategory. Must take into account important
# setting
Object.defineProperty @, "isStandardCategory",
enumerable: true
configurable: true
value: (showImportant)=>
showImportant ?= NylasEnv.config.get('core.workspace.showImportant')
val = @constructor.Types.Standard
if showImportant is true
val in @types
else
val in @types and @name isnt 'important'
# Define getters for other category types
for key, val of @constructor.Types
continue if val is @constructor.Types.Standard
do (key, val) =>
Object.defineProperty @, "is#{key}Category",
enumerable: true

View file

@ -94,7 +94,7 @@ class Thread extends Model
super(json)
# Public: Returns true if the thread has a {Category} with the given
# name. Note, only `CategoryStore::standardCategories` have valid
# name. Note, only catgories of type `Category.Types.Standard` have valid
# `names`
#
# * `id` A {String} {Category} name

View file

@ -5,16 +5,6 @@ AccountStore = require './account-store'
{Categories} = require 'nylas-observables'
Rx = require 'rx-lite'
_observables = (account) ->
{accountId} = account
categories = Categories.forAccount(account).sort()
return {
allCategories: categories
userCategories: categories.categoryFilter((cat) -> cat.isUserCategory())
hiddenCategories: categories.categoryFilter((cat) -> cat.isHiddenCategory())
standardCategories: Categories.standardForAccount(account).sort()
}
class CategoryStore extends NylasStore
constructor: ->
@ -23,29 +13,28 @@ class CategoryStore extends NylasStore
@listenTo AccountStore, @_onAccountsChanged
byId: (id) -> @_categoryCache[id]
byId: (account, categoryId) -> @categories(account)[categoryId]
# Public: Returns an array of all categories for an account, both
# standard and user generated. The items returned by this function will be
# either {Folder} or {Label} objects.
#
categories: (account) ->
@_observables[account.id].allCategories.last()
@_categoryCache[account.id]
# Public: Returns all of the standard categories for the current account.
#
standardCategories: (account) ->
@_observables[account.id].standardCategories.last()
_.values(@categories(account)).filter (cat) -> cat.isStandardCategory()
hiddenCategories: (account) ->
@_observables[account.id].hiddenCategories.last()
_.values(@categories(account)).filter (cat) -> cat.isHiddenCategory()
# Public: Returns all of the categories that are not part of the standard
# category set.
#
userCategories: (account) ->
@_observables[account.id].userCategories.last()
_.values(@categories(account)).filter (cat) -> cat.isUserCategory()
# Public: Returns the Folder or Label object for a standard category name and
# for a given account.
@ -78,19 +67,18 @@ class CategoryStore extends NylasStore
_onAccountsChanged: ->
@_setupObservables(AccountStore.accounts())
_onCategoriesChanged: (categories) =>
_onCategoriesChanged: (accountId, categories) =>
return unless categories
@_categoryCache = {}
@_categoryCache[accountId] = {}
for category in categories
@_categoryCache[category.id] = category
@_categoryCache[accountId][category.id] = category
@trigger()
_setupObservables: (accounts) =>
@_observables = {}
accounts.forEach (account) =>
@_observables[account.accountId] = _observables(account)
@_disposable?.dispose()
@_disposable = Categories.forAllAccounts().subscribe(@_onCategoriesChanged)
@_disposables ?= []
@_disposables.forEach (disp) -> disp.dispose()
@_disposables = accounts.map (account) =>
Categories.forAccount(account)
.subscribe(@_onCategoriesChanged.bind(@, account.id))
module.exports = new CategoryStore()

View file

@ -184,6 +184,7 @@ class ContactStore extends NylasStore
return Promise.resolve(detected)
__refreshCache: (contacts) =>
return unless contacts
contacts.forEach (contact) =>
@_contactCache[contact.accountId] ?= []
@_contactCache[contact.accountId].push(contact)

View file

@ -107,7 +107,7 @@ class FocusedContactsStore extends NylasStore
_calculatePenalties: (contact, score) ->
penalties = 0
email = contact.email.toLowerCase().trim()
myEmail = AccountStore.accountForId(@currentThread?.accountId).emailAddress
myEmail = AccountStore.accountForId(@currentThread?.accountId)?.emailAddress
if email is myEmail
# The whole thing which will penalize to zero

View file

@ -1,5 +1,6 @@
NylasStore = require 'nylas-store'
WorkspaceStore = require './workspace-store'
AccountStore = require './workspace-store'
MailboxPerspective = require '../../mailbox-perspective'
CategoryStore = require './category-store'
Actions = require '../actions'
@ -15,8 +16,11 @@ class FocusedPerspectiveStore extends NylasStore
_onCategoryStoreChanged: ->
if not @_current
@_setPerspective(@_defaultPerspective())
else if @_current.categoryId() and not CategoryStore.byId(@_current.categoryId())
@_setPerspective(@_defaultPerspective())
else
account = @_current.account
catId = @_current.categoryId()
if catId and not CategoryStore.byId(account, catId)
@_setPerspective(@_defaultPerspective())
_onFocusMailView: (filter) =>
return if filter.isEqual(@_current)
@ -38,8 +42,12 @@ class FocusedPerspectiveStore extends NylasStore
@_currentBeforeSearch = null
_defaultPerspective: ->
# TODO what should the default mail view be
MailboxPerspective.unified()
# TODO Update unified MailboxPerspective
account = AccountStore.accounts()[0]
category = CategoryStore.getStandardCategory(account, "inbox")
return null unless category
MailViewFilter.forCategory(account, category)
# MailboxPerspective.unified()
_setPerspective: (filter) ->
return if filter?.isEqual(@_current)

View file

@ -21,7 +21,7 @@ class TaskFactory
else
labelsToRemove = []
if exclusive
currentLabel = CategoryStore.byId(fromView?.categoryId())
currentLabel = CategoryStore.byId(account, fromView?.categoryId())
currentLabel ?= CategoryStore.getStandardCategory(account, "inbox")
labelsToRemove = [currentLabel]
@ -42,7 +42,7 @@ class TaskFactory
else
labelsToAdd = []
if exclusive
currentLabel = CategoryStore.byId(fromView?.categoryId())
currentLabel = CategoryStore.byId(account, fromView?.categoryId())
currentLabel ?= CategoryStore.getStandardCategory(account, "inbox")
labelsToAdd = [currentLabel]

View file

@ -49,26 +49,28 @@ CategoryObservables =
_.extend(observable, CategoryOperators)
observable
standardForAccount: (account) =>
standard: (account) =>
observable = Rx.Observable.fromConfig('core.workspace.showImportant')
.flatMapLatest (showImportant) =>
accountObservable = CategoryObservables.forAccount(account)
return accountObservable.categoryFilter (cat) ->
if showImportant is true
cat.isStandardCategory()
else
cat.isStandardCategory() and cat.name isnt 'important'
return CategoryObservables.forAccount(account).sort()
.categoryFilter (cat) -> cat.isStandardCategory(showImportant)
_.extend(observable, CategoryOperators)
observable
user: (account) =>
CategoryObservables.forAccount(account).sort()
.categoryFilter (cat) -> cat.isUserCategory()
hidden: (account) =>
CategoryObservables.forAccount(account).sort()
.categoryFilter (cat) -> cat.isHiddenCategory()
module.exports =
Categories: CategoryObservables
Accounts: AccountObservables
# Attach a few global helpers
#
Rx.Observable::last = ->
@takeLast(1).toArray()[0]
Rx.Observable.fromStore = (store) =>
return Rx.Observable.create (observer) =>

View file

@ -190,6 +190,9 @@ class CategoryMailboxPerspective extends MailboxPerspective
class UnifiedMailboxPerspective extends MailboxPerspective
categoryId: ->
null
matchers: ->
[]