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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{Category, Label} = require 'nylas-exports' {Category, Label} = require 'nylas-exports'
fdescribe 'Category', -> describe 'Category', ->
describe '_initCategoryTypes', -> describe '_initCategoryTypes', ->
@ -22,6 +22,15 @@ fdescribe 'Category', ->
expect(cat.isHiddenCategory()).toBe false expect(cat.isHiddenCategory()).toBe false
expect(cat.isLockedCategory()).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', -> it 'assigns type correctly when it is a hidden category', ->
cat = new Label cat = new Label
cat.name = 'archive' cat.name = 'archive'

View file

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

View file

@ -95,7 +95,22 @@ class Category extends Model
if @name in HiddenCategoryNames if @name in HiddenCategoryNames
@types.push @constructor.Types.Hidden @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 for key, val of @constructor.Types
continue if val is @constructor.Types.Standard
do (key, val) => do (key, val) =>
Object.defineProperty @, "is#{key}Category", Object.defineProperty @, "is#{key}Category",
enumerable: true enumerable: true

View file

@ -94,7 +94,7 @@ class Thread extends Model
super(json) super(json)
# Public: Returns true if the thread has a {Category} with the given # 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` # `names`
# #
# * `id` A {String} {Category} name # * `id` A {String} {Category} name

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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