mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-07 13:14:47 +08:00
feat(categories): enable creating new labels and folders. addresses T3351.
Summary: write tests for adding/removing existing labels and popover closing add more tests address code review comments fix the tests add test for creating label add test for creating label and queueing change label task add test for creating a folder add syncback category task spec make the rest of the tests pass remove unnecessary parens add a few more tests add last test Test Plan: added some tests. all tests green Reviewers: bengotow, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2010
This commit is contained in:
parent
ba00ad9cbe
commit
be7b52cb98
12 changed files with 532 additions and 24 deletions
|
@ -84,6 +84,7 @@ class NylasExports
|
|||
@require "DestroyDraftTask", 'flux/tasks/destroy-draft'
|
||||
@require "ChangeLabelsTask", 'flux/tasks/change-labels-task'
|
||||
@require "ChangeFolderTask", 'flux/tasks/change-folder-task'
|
||||
@require "SyncbackCategoryTask", 'flux/tasks/syncback-category-task'
|
||||
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
||||
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft'
|
||||
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
||||
|
|
|
@ -2,14 +2,19 @@ _ = require 'underscore'
|
|||
React = require 'react'
|
||||
|
||||
{Utils,
|
||||
Label,
|
||||
Folder,
|
||||
Thread,
|
||||
Actions,
|
||||
TaskQueue,
|
||||
CategoryStore,
|
||||
AccountStore,
|
||||
CategoryStore,
|
||||
DatabaseStore,
|
||||
WorkspaceStore,
|
||||
ChangeLabelsTask,
|
||||
ChangeFolderTask,
|
||||
SyncbackCategoryTask,
|
||||
TaskQueueStatusStore,
|
||||
FocusedMailViewStore} = require 'nylas-exports'
|
||||
|
||||
{Menu,
|
||||
|
@ -106,7 +111,10 @@ class CategoryPicker extends React.Component
|
|||
|
||||
_renderItemContent: (item) =>
|
||||
if item.divider
|
||||
return <Menu.Item divider={item.divider} />
|
||||
return <Menu.Item key={item.id} divider={item.divider} />
|
||||
else if item.newCategoryItem
|
||||
return @_renderCreateNewItem(item)
|
||||
|
||||
if @_account?.usesLabels()
|
||||
icon = @_renderCheckbox(item)
|
||||
else if @_account?.usesFolders()
|
||||
|
@ -120,6 +128,21 @@ class CategoryPicker extends React.Component
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_renderCreateNewItem: ({searchValue, name}) =>
|
||||
if @_account?.usesLabels()
|
||||
picName = "tag"
|
||||
else if @_account?.usesFolders()
|
||||
picName = "folder"
|
||||
|
||||
<div className="category-item category-create-new">
|
||||
<RetinaImg className={"category-create-new-#{picName}"}
|
||||
name={"#{picName}.png"}
|
||||
mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<div className="category-display-name">
|
||||
<strong>“{searchValue}”</strong> (create new)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderCheckbox: (item) ->
|
||||
styles = {}
|
||||
styles.backgroundColor = item.backgroundColor
|
||||
|
@ -176,24 +199,64 @@ class CategoryPicker extends React.Component
|
|||
@refs.menu.setSelectedItem(null)
|
||||
|
||||
if @_account.usesLabels()
|
||||
if item.usage > 0
|
||||
if item.newCategoryItem
|
||||
cat = new Label
|
||||
displayName: @state.searchValue,
|
||||
accountId: AccountStore.current().id
|
||||
task = new SyncbackCategoryTask
|
||||
category: cat
|
||||
organizationUnit: "label"
|
||||
|
||||
TaskQueueStatusStore.waitForPerformRemote(task).then =>
|
||||
DatabaseStore.findBy Label, clientId: cat.clientId
|
||||
.then (cat) =>
|
||||
changeLabelsTask = new ChangeLabelsTask
|
||||
labelsToAdd: [cat]
|
||||
threads: @_threads()
|
||||
Actions.queueTask(changeLabelsTask)
|
||||
|
||||
Actions.queueTask(task)
|
||||
else if item.usage > 0
|
||||
task = new ChangeLabelsTask
|
||||
labelsToRemove: [item.category]
|
||||
threads: @_threads()
|
||||
Actions.queueTask(task)
|
||||
else
|
||||
task = new ChangeLabelsTask
|
||||
labelsToAdd: [item.category]
|
||||
threads: @_threads()
|
||||
Actions.queueTask(task)
|
||||
Actions.queueTask(task)
|
||||
|
||||
else if @_account.usesFolders()
|
||||
task = new ChangeFolderTask
|
||||
folder: item.category
|
||||
threads: @_threads()
|
||||
if @props.thread
|
||||
Actions.moveThread(@props.thread, task)
|
||||
else if @props.items
|
||||
Actions.moveThreads(@_threads(), task)
|
||||
if item.newCategoryItem?
|
||||
cat = new Folder
|
||||
displayName: @state?.searchValue,
|
||||
accountId: AccountStore.current().id
|
||||
task = new SyncbackCategoryTask
|
||||
category: cat
|
||||
organizationUnit: "folder"
|
||||
|
||||
TaskQueueStatusStore.waitForPerformRemote(task).then =>
|
||||
DatabaseStore.findBy Folder, clientId: cat.clientId
|
||||
.then (cat) =>
|
||||
return if not cat?.serverId
|
||||
|
||||
changeFolderTask = new ChangeFolderTask
|
||||
folder: cat
|
||||
threads: @_threads()
|
||||
if @props.thread
|
||||
Actions.moveThread(@props.thread, changeFolderTask)
|
||||
else if @props.items
|
||||
Actions.moveThreads(@_threads(), changeFolderTask)
|
||||
Actions.queueTask(task)
|
||||
else
|
||||
task = new ChangeFolderTask
|
||||
folder: item.category
|
||||
threads: @_threads()
|
||||
if @props.thread
|
||||
Actions.moveThread(@props.thread, task)
|
||||
else if @props.items
|
||||
Actions.moveThreads(@_threads(), task)
|
||||
|
||||
else
|
||||
throw new Error("Invalid organizationUnit")
|
||||
|
@ -222,8 +285,8 @@ class CategoryPicker extends React.Component
|
|||
@_account = AccountStore.current()
|
||||
return unless @_account
|
||||
|
||||
categories = [].concat(CategoryStore.getStandardCategories())
|
||||
.concat([{divider: true}])
|
||||
categories = CategoryStore.getStandardCategories()
|
||||
.concat([{divider: true, id: "category-divider"}])
|
||||
.concat(CategoryStore.getUserCategories())
|
||||
|
||||
usageCount = @_categoryUsageCount(props, categories)
|
||||
|
@ -238,6 +301,13 @@ class CategoryPicker extends React.Component
|
|||
.map(_.partial(@_itemForCategory, displayData))
|
||||
.value()
|
||||
|
||||
if searchValue.length > 0
|
||||
newItemData =
|
||||
searchValue: searchValue
|
||||
newCategoryItem: true
|
||||
id: "category-create-new"
|
||||
categoryData.push(newItemData)
|
||||
|
||||
return {categoryData, searchValue}
|
||||
|
||||
_categoryUsageCount: (props, categories) =>
|
||||
|
|
283
internal_packages/category-picker/spec/category-picker-spec.cjsx
Normal file
283
internal_packages/category-picker/spec/category-picker-spec.cjsx
Normal file
|
@ -0,0 +1,283 @@
|
|||
_ = require 'underscore'
|
||||
React = require "react/addons"
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
CategoryPicker = require '../lib/category-picker'
|
||||
{Popover} = require 'nylas-component-kit'
|
||||
|
||||
{Utils,
|
||||
Label,
|
||||
Folder,
|
||||
Thread,
|
||||
Actions,
|
||||
CategoryStore,
|
||||
DatabaseStore,
|
||||
ChangeLabelsTask,
|
||||
ChangeFolderTask,
|
||||
SyncbackCategoryTask,
|
||||
FocusedMailViewStore,
|
||||
TaskQueueStatusStore} = require 'nylas-exports'
|
||||
|
||||
describe 'CategoryPicker', ->
|
||||
beforeEach ->
|
||||
CategoryStore._categoryCache = {}
|
||||
|
||||
afterEach ->
|
||||
atom.testOrganizationUnit = null
|
||||
|
||||
setupFor = (organizationUnit) ->
|
||||
atom.testOrganizationUnit = organizationUnit
|
||||
klass = if organizationUnit is "label" then Label else Folder
|
||||
|
||||
@inboxCategory = new klass(id: 'id-123', name: 'inbox', displayName: "INBOX")
|
||||
@archiveCategory = new klass(id: 'id-456', name: 'archive', displayName: "ArCHIVe")
|
||||
@userCategory = new klass(id: 'id-789', name: null, displayName: "MyCategory")
|
||||
|
||||
spyOn(CategoryStore, "getStandardCategories").andReturn [ @inboxCategory, @archiveCategory ]
|
||||
spyOn(CategoryStore, "getUserCategories").andReturn [ @userCategory ]
|
||||
spyOn(CategoryStore, "getStandardCategory").andReturn @inboxCategory
|
||||
|
||||
# By default we're going to set to "inbox". This has implications for
|
||||
# what categories get filtered out of the list.
|
||||
f = FocusedMailViewStore
|
||||
f._setMailView f._defaultMailView()
|
||||
|
||||
setupForCreateNew = (orgUnit = "folder") ->
|
||||
setupFor.call(@, orgUnit)
|
||||
|
||||
@testThread = new Thread(id: 't1', subject: "fake")
|
||||
@picker = ReactTestUtils.renderIntoDocument(
|
||||
<CategoryPicker thread={@testThread} />
|
||||
)
|
||||
|
||||
@popover = ReactTestUtils.findRenderedComponentWithType @picker, Popover
|
||||
@popover.open()
|
||||
|
||||
describe 'when using labels', ->
|
||||
beforeEach ->
|
||||
setupFor.call(@, "label")
|
||||
|
||||
describe 'when using folders', ->
|
||||
beforeEach ->
|
||||
setupFor.call(@, "folder")
|
||||
|
||||
@testThread = new Thread(id: 't1', subject: "fake")
|
||||
@picker = ReactTestUtils.renderIntoDocument(
|
||||
<CategoryPicker thread={@testThread} />
|
||||
)
|
||||
|
||||
it 'lists the desired categories', ->
|
||||
data = @picker.state.categoryData
|
||||
# NOTE: The inbox category is not included here because it's the
|
||||
# currently focused category, which gets filtered out of the list.
|
||||
expect(data[0].id).toBe "id-456"
|
||||
expect(data[0].name).toBe "archive"
|
||||
expect(data[0].category).toBe @archiveCategory
|
||||
expect(data[1].divider).toBe true
|
||||
expect(data[1].id).toBe "category-divider"
|
||||
expect(data[2].id).toBe "id-789"
|
||||
expect(data[2].name).toBeUndefined()
|
||||
expect(data[2].category).toBe @userCategory
|
||||
|
||||
xdescribe 'when picking for a single Thread', ->
|
||||
it 'renders a picker', ->
|
||||
expect(ReactTestUtils.isCompositeComponentWithType @picker, CategoryPicker).toBe true
|
||||
|
||||
it "does not include a newItem prompt if there's no search", ->
|
||||
outData = @picker._recalculateState().categoryData
|
||||
newItem = _.findWhere(outData, newCategoryItem: true)
|
||||
l1 = _.findWhere(outData, id: 'id-123')
|
||||
expect(newItem).toBeUndefined()
|
||||
expect(l1.name).toBe "inbox"
|
||||
|
||||
it "includes a newItem selector with the current search term", ->
|
||||
|
||||
xdescribe 'when picking labels for a single Thread', ->
|
||||
beforeEach ->
|
||||
atom.testOrganizationUnit = "label"
|
||||
|
||||
describe "'create new' item", ->
|
||||
beforeEach ->
|
||||
setupForCreateNew.call @
|
||||
|
||||
afterEach -> atom.testOrganizationUnit = null
|
||||
|
||||
it "is not visible when the search box is empty", ->
|
||||
count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new').length
|
||||
expect(count).toBe 0
|
||||
|
||||
it "is visible when the search box has text", ->
|
||||
inputNode = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(@picker, "input")[0])
|
||||
ReactTestUtils.Simulate.change inputNode, target: { value: "calendar" }
|
||||
count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new').length
|
||||
expect(count).toBe 1
|
||||
|
||||
it "shows folder icon if we're using exchange", ->
|
||||
inputNode = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(@picker, "input")[0])
|
||||
ReactTestUtils.Simulate.change inputNode, target: { value: "calendar" }
|
||||
count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new-folder').length
|
||||
expect(count).toBe 1
|
||||
|
||||
describe "'create new' item with labels", ->
|
||||
beforeEach ->
|
||||
setupForCreateNew.call @, "label"
|
||||
|
||||
it "shows label icon if we're using gmail", ->
|
||||
inputNode = React.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(@picker, "input")[0])
|
||||
ReactTestUtils.Simulate.change inputNode, target: { value: "calendar" }
|
||||
count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new-tag').length
|
||||
expect(count).toBe 1
|
||||
|
||||
describe "_onSelectCategory()", ->
|
||||
describe "using labels", ->
|
||||
beforeEach ->
|
||||
setupForCreateNew.call @, "label"
|
||||
spyOn Actions, "queueTask"
|
||||
|
||||
it "adds a label if it was previously unused", ->
|
||||
input = { usage: 0, newCategoryItem: undefined, category: "asdf" }
|
||||
|
||||
@picker._onSelectCategory input
|
||||
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
|
||||
labelsToAdd = Actions.queueTask.calls[0].args[0].labelsToAdd
|
||||
expect(labelsToAdd.length).toBe 1
|
||||
expect(labelsToAdd[0]).toEqual input.category
|
||||
|
||||
threadsToUpdate = Actions.queueTask.calls[0].args[0].threads
|
||||
expect(threadsToUpdate).toEqual [ @testThread ]
|
||||
|
||||
it "removes a label if it was previously used", ->
|
||||
input = { usage: 1, newCategoryItem: undefined, category: "asdf" }
|
||||
|
||||
@picker._onSelectCategory input
|
||||
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
|
||||
labelsToRemove = Actions.queueTask.calls[0].args[0].labelsToRemove
|
||||
expect(labelsToRemove.length).toBe 1
|
||||
expect(labelsToRemove[0]).toEqual input.category
|
||||
|
||||
threadsToUpdate = Actions.queueTask.calls[0].args[0].threads
|
||||
expect(threadsToUpdate).toEqual [ @testThread ]
|
||||
|
||||
it "creates a new label task", ->
|
||||
input = { newCategoryItem: true }
|
||||
|
||||
@picker.setState searchValue: "teSTing!"
|
||||
|
||||
@picker._onSelectCategory input
|
||||
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
|
||||
syncbackTask = Actions.queueTask.calls[0].args[0]
|
||||
newCategory = syncbackTask.category
|
||||
expect(syncbackTask.organizationUnit).toBe "label"
|
||||
expect(newCategory.displayName).toBe "teSTing!"
|
||||
expect(newCategory.accountId).toBe TEST_ACCOUNT_ID
|
||||
|
||||
it "queues a change label task after performRemote for creating it", ->
|
||||
input = { newCategoryItem: true }
|
||||
label = new Label(clientId: "local-123")
|
||||
|
||||
spyOn(TaskQueueStatusStore, "waitForPerformRemote").andCallFake (task) ->
|
||||
expect(task instanceof SyncbackCategoryTask).toBe true
|
||||
Promise.resolve()
|
||||
spyOn(DatabaseStore, "findBy").andCallFake (klass, {clientId}) ->
|
||||
expect(klass).toBe Label
|
||||
expect(typeof clientId).toBe "string"
|
||||
Promise.resolve label
|
||||
|
||||
runs ->
|
||||
@picker.setState searchValue: "teSTing!"
|
||||
@picker._onSelectCategory input
|
||||
|
||||
waitsFor -> Actions.queueTask.calls.length > 1
|
||||
|
||||
runs ->
|
||||
changeLabelsTask = Actions.queueTask.calls[1].args[0]
|
||||
expect(changeLabelsTask instanceof ChangeLabelsTask).toBe true
|
||||
expect(changeLabelsTask.labelsToAdd).toEqual [ label ]
|
||||
expect(changeLabelsTask.threads).toEqual [ @testThread ]
|
||||
|
||||
it "doesn't queue any duplicate syncback tasks", ->
|
||||
input = { newCategoryItem: true }
|
||||
label = new Label(clientId: "local-123")
|
||||
|
||||
spyOn(TaskQueueStatusStore, "waitForPerformRemote").andCallFake (task) ->
|
||||
expect(task instanceof SyncbackCategoryTask).toBe true
|
||||
Promise.resolve()
|
||||
spyOn(DatabaseStore, "findBy").andCallFake (klass, {clientId}) ->
|
||||
expect(klass).toBe Label
|
||||
expect(typeof clientId).toBe "string"
|
||||
Promise.resolve label
|
||||
|
||||
runs ->
|
||||
@picker.setState searchValue: "teSTing!"
|
||||
@picker._onSelectCategory input
|
||||
|
||||
waitsFor -> Actions.queueTask.calls.length > 1
|
||||
|
||||
runs ->
|
||||
allInputs = Actions.queueTask.calls.map (c) -> c.args[0]
|
||||
syncbackTasks = allInputs.filter (i) -> i instanceof SyncbackCategoryTask
|
||||
expect(syncbackTasks.length).toBe 1
|
||||
|
||||
describe "using folders", ->
|
||||
beforeEach ->
|
||||
setupForCreateNew.call @, "folder"
|
||||
spyOn Actions, "queueTask"
|
||||
spyOn Actions, "moveThread"
|
||||
spyOn Actions, "moveThreads"
|
||||
|
||||
it "moves a thread if the component has one", ->
|
||||
input = { category: "blah" }
|
||||
@picker._onSelectCategory input
|
||||
expect(Actions.moveThread).toHaveBeenCalled()
|
||||
|
||||
args = Actions.moveThread.calls[0].args
|
||||
expect(args[0]).toEqual @testThread
|
||||
expect(args[1].folder).toEqual input.category
|
||||
expect(args[1].threads).toEqual [ @testThread ]
|
||||
|
||||
it "moves threads if the component has no thread but has items", ->
|
||||
@picker = ReactTestUtils.renderIntoDocument(
|
||||
<CategoryPicker items={[@testThread]} />
|
||||
)
|
||||
@popover = ReactTestUtils.findRenderedComponentWithType @picker, Popover
|
||||
@popover.open()
|
||||
|
||||
input = { category: "blah" }
|
||||
@picker._onSelectCategory input
|
||||
expect(Actions.moveThreads).toHaveBeenCalled()
|
||||
|
||||
it "creates a new folder task", ->
|
||||
input = { newCategoryItem: true }
|
||||
folder = new Folder(clientId: "local-456", serverId: "yes.")
|
||||
|
||||
spyOn(TaskQueueStatusStore, "waitForPerformRemote").andCallFake (task) ->
|
||||
expect(task instanceof SyncbackCategoryTask).toBe true
|
||||
Promise.resolve()
|
||||
spyOn(DatabaseStore, "findBy").andCallFake (klass, {clientId}) ->
|
||||
expect(klass).toBe Folder
|
||||
expect(typeof clientId).toBe "string"
|
||||
Promise.resolve folder
|
||||
|
||||
runs ->
|
||||
@picker.setState searchValue: "teSTing!"
|
||||
@picker._onSelectCategory input
|
||||
|
||||
waitsFor -> Actions.moveThread.calls.length > 0
|
||||
|
||||
runs ->
|
||||
changeFoldersTask = Actions.moveThread.calls[0].args[1]
|
||||
expect(changeFoldersTask instanceof ChangeFolderTask).toBe true
|
||||
expect(changeFoldersTask.folder).toEqual folder
|
||||
expect(changeFoldersTask.threads).toEqual [ @testThread ]
|
||||
|
||||
it "closes the popover", ->
|
||||
setupForCreateNew.call @, "folder"
|
||||
spyOn @popover, "close"
|
||||
spyOn Actions, "moveThread"
|
||||
@picker._onSelectCategory { usage: 0, category: "asdf" }
|
||||
expect(@popover.close).toHaveBeenCalled()
|
59
spec-nylas/tasks/syncback-category-task-spec.coffee
Normal file
59
spec-nylas/tasks/syncback-category-task-spec.coffee
Normal file
|
@ -0,0 +1,59 @@
|
|||
SyncbackCategoryTask = require "../../src/flux/tasks/syncback-category-task"
|
||||
NylasAPI = require "../../src/flux/nylas-api"
|
||||
{Label, Folder, DatabaseStore} = require "nylas-exports"
|
||||
|
||||
describe "SyncbackCategoryTask", ->
|
||||
describe "performRemote", ->
|
||||
pathOf = (fn) ->
|
||||
fn.calls[0].args[0].path
|
||||
|
||||
accountIdOf = (fn) ->
|
||||
fn.calls[0].args[0].accountId
|
||||
|
||||
nameOf = (fn) ->
|
||||
fn.calls[0].args[0].body.display_name
|
||||
|
||||
makeTask = (orgUnit) ->
|
||||
Category = if orgUnit is "label" then Label else Folder
|
||||
category = new Category
|
||||
displayName: "important emails"
|
||||
accountId: "account 123"
|
||||
clientId: "local-444"
|
||||
new SyncbackCategoryTask
|
||||
category: category
|
||||
organizationUnit: orgUnit
|
||||
|
||||
beforeEach ->
|
||||
spyOn(NylasAPI, "makeRequest").andCallFake ->
|
||||
Promise.resolve(id: "server-444")
|
||||
spyOn(DatabaseStore, "persistModel")
|
||||
|
||||
it "sends API req to /labels if user uses labels", ->
|
||||
task = makeTask "label"
|
||||
task.performRemote({})
|
||||
expect(pathOf(NylasAPI.makeRequest)).toBe "/labels"
|
||||
|
||||
it "sends API req to /folders if user uses folders", ->
|
||||
task = makeTask "folder"
|
||||
task.performRemote({})
|
||||
expect(pathOf(NylasAPI.makeRequest)).toBe "/folders"
|
||||
|
||||
it "sends the account id", ->
|
||||
task = makeTask "label"
|
||||
task.performRemote({})
|
||||
expect(accountIdOf(NylasAPI.makeRequest)).toBe "account 123"
|
||||
|
||||
it "sends the display name in the body", ->
|
||||
task = makeTask "label"
|
||||
task.performRemote({})
|
||||
expect(nameOf(NylasAPI.makeRequest)).toBe "important emails"
|
||||
|
||||
it "adds server id to the category, then saves the category", ->
|
||||
waitsForPromise ->
|
||||
task = makeTask "label"
|
||||
task.performRemote({})
|
||||
.then ->
|
||||
expect(DatabaseStore.persistModel).toHaveBeenCalled()
|
||||
model = DatabaseStore.persistModel.calls[0].args[0]
|
||||
expect(model.clientId).toBe "local-444"
|
||||
expect(model.serverId).toBe "server-444"
|
|
@ -16,13 +16,13 @@ class MenuItem extends React.Component
|
|||
###
|
||||
Public: React `props` supported by MenuItem:
|
||||
|
||||
- `divider` (optional) Pass a {String} to render the menu item as a section divider.
|
||||
- `divider` (optional) Pass a {Boolean} to render the menu item as a section divider.
|
||||
- `key` (optional)
|
||||
- `selected` (optional)
|
||||
- `checked` (optional)
|
||||
###
|
||||
@propTypes:
|
||||
divider: React.PropTypes.string
|
||||
divider: React.PropTypes.bool
|
||||
key: React.PropTypes.string
|
||||
selected: React.PropTypes.bool
|
||||
checked: React.PropTypes.bool
|
||||
|
|
|
@ -44,6 +44,7 @@ class Folder extends Category
|
|||
|
||||
@additionalSQLiteConfig:
|
||||
setup: ->
|
||||
['CREATE INDEX IF NOT EXISTS FolderNameIndex ON Folder(account_id,name)']
|
||||
['CREATE INDEX IF NOT EXISTS FolderNameIndex ON Folder(account_id,name)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS FolderClientIndex ON Folder(client_id)']
|
||||
|
||||
module.exports = Folder
|
||||
|
|
|
@ -43,6 +43,7 @@ class Label extends Category
|
|||
|
||||
@additionalSQLiteConfig:
|
||||
setup: ->
|
||||
['CREATE INDEX IF NOT EXISTS LabelNameIndex ON Label(account_id,name)']
|
||||
['CREATE INDEX IF NOT EXISTS LabelNameIndex ON Label(account_id,name)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS LabelClientIndex ON Label(client_id)']
|
||||
|
||||
module.exports = Label
|
||||
|
|
|
@ -17,7 +17,7 @@ PriorityUICoordinator = require '../../priority-ui-coordinator'
|
|||
serializeRegisteredObjects,
|
||||
deserializeRegisteredObjects} = require '../models/utils'
|
||||
|
||||
DatabaseVersion = 14
|
||||
DatabaseVersion = 15
|
||||
|
||||
DatabasePhase =
|
||||
Setup: 'setup'
|
||||
|
@ -344,9 +344,13 @@ class DatabaseStore extends NylasStore
|
|||
return Promise.resolve([])
|
||||
|
||||
ids = []
|
||||
clientIds = []
|
||||
for item in arr
|
||||
if item instanceof klass
|
||||
continue
|
||||
if not item.serverId
|
||||
clientIds.push(item.clientId)
|
||||
else
|
||||
continue
|
||||
else if _.isString(item)
|
||||
ids.push(item)
|
||||
else
|
||||
|
@ -355,9 +359,20 @@ class DatabaseStore extends NylasStore
|
|||
if ids.length is 0
|
||||
return Promise.resolve(arr)
|
||||
|
||||
@findAll(klass).where(klass.attributes.id.in(ids)).then (models) =>
|
||||
whereId = =>
|
||||
klass.attributes.id.in(ids)
|
||||
|
||||
whereClientId = =>
|
||||
klass.attributes.clientId.in(clientIds)
|
||||
|
||||
queries = {}
|
||||
queries.modelsFromIds = @findAll(klass).where(whereId) if ids.length
|
||||
queries.modelsFromClientIds = @findAll(klass).where(whereClientId) if clientIds.length
|
||||
|
||||
Promise.props(queries).then ({modelsFromIds, modelsFromClientIds}) =>
|
||||
modelsById = {}
|
||||
modelsById[model.id] = model for model in models
|
||||
modelsById[model.id] = model for model in modelsFromIds
|
||||
modelsById[model.id] = model for model in modelsFromClientIds
|
||||
|
||||
arr = arr.map (item) ->
|
||||
if item instanceof klass
|
||||
|
|
|
@ -11,7 +11,8 @@ class TaskQueueStatusStore extends NylasStore
|
|||
|
||||
constructor: ->
|
||||
@_queue = []
|
||||
@_waiting = []
|
||||
@_waitingLocals = []
|
||||
@_waitingRemotes = []
|
||||
@listenTo DatabaseStore, @_onChange
|
||||
|
||||
DatabaseStore.findJSONObject(TaskQueue.JSONObjectStorageKey).then (json) =>
|
||||
|
@ -21,12 +22,18 @@ class TaskQueueStatusStore extends NylasStore
|
|||
_onChange: (change) =>
|
||||
if change.objectClass is 'JSONObject' and change.objects[0].key is 'task-queue'
|
||||
@_queue = change.objects[0].json
|
||||
@_waiting = @_waiting.filter ({taskId, resolve}) =>
|
||||
@_waitingLocals = @_waitingLocals.filter ({taskId, resolve}) =>
|
||||
task = _.findWhere(@_queue, {id: taskId})
|
||||
if not task or task.queueState.localComplete
|
||||
resolve()
|
||||
return false
|
||||
return true
|
||||
@_waitingRemotes = @_waitingRemotes.filter ({taskId, resolve}) =>
|
||||
task = _.findWhere(@_queue, {id: taskId})
|
||||
if not task
|
||||
resolve()
|
||||
return false
|
||||
return true
|
||||
@trigger()
|
||||
|
||||
queue: ->
|
||||
|
@ -34,6 +41,10 @@ class TaskQueueStatusStore extends NylasStore
|
|||
|
||||
waitForPerformLocal: (task) ->
|
||||
new Promise (resolve, reject) =>
|
||||
@_waiting.push({taskId: task.id, resolve: resolve})
|
||||
@_waitingLocals.push({taskId: task.id, resolve: resolve})
|
||||
|
||||
waitForPerformRemote: (task) ->
|
||||
new Promise (resolve, reject) =>
|
||||
@_waitingRemotes.push({taskId: task.id, resolve: resolve})
|
||||
|
||||
module.exports = new TaskQueueStatusStore()
|
||||
|
|
|
@ -5,6 +5,7 @@ Thread = require '../models/thread'
|
|||
Message = require '../models/message'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
ChangeMailTask = require './change-mail-task'
|
||||
SyncbackCategoryTask = require './syncback-category-task'
|
||||
|
||||
# Public: Create a new task to apply labels to a message or thread.
|
||||
#
|
||||
|
@ -40,6 +41,8 @@ class ChangeFolderTask extends ChangeMailTask
|
|||
else
|
||||
return "Moved objects#{folderText}"
|
||||
|
||||
shouldWaitForTask: (other) -> other instanceof SyncbackCategoryTask
|
||||
|
||||
performLocal: ->
|
||||
if not @folder
|
||||
return Promise.reject(new Error("Must specify a `folder`"))
|
||||
|
|
|
@ -5,6 +5,7 @@ Thread = require '../models/thread'
|
|||
Message = require '../models/message'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
ChangeMailTask = require './change-mail-task'
|
||||
SyncbackCategoryTask = require './syncback-category-task'
|
||||
|
||||
# Public: Create a new task to apply labels to a message or thread.
|
||||
#
|
||||
|
@ -32,6 +33,8 @@ class ChangeLabelsTask extends ChangeMailTask
|
|||
return "Removed #{@labelsToRemove[0].displayName} from #{@threads.length} #{type}"
|
||||
return "Changed labels on #{@threads.length} #{type}"
|
||||
|
||||
shouldWaitForTask: (other) -> other instanceof SyncbackCategoryTask
|
||||
|
||||
performLocal: ->
|
||||
if @labelsToAdd.length is 0 and @labelsToRemove.length is 0
|
||||
return Promise.reject(new Error("ChangeLabelsTask: Must specify `labelsToAdd` or `labelsToRemove`"))
|
||||
|
|
61
src/flux/tasks/syncback-category-task.coffee
Normal file
61
src/flux/tasks/syncback-category-task.coffee
Normal file
|
@ -0,0 +1,61 @@
|
|||
CategoryStore = require '../stores/category-store'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
{generateTempId} = require '../models/utils'
|
||||
Task = require './task'
|
||||
NylasAPI = require '../nylas-api'
|
||||
{APIError} = require '../errors'
|
||||
|
||||
module.exports = class SyncbackCategoryTask extends Task
|
||||
|
||||
constructor: ({@category, @organizationUnit}={}) ->
|
||||
super
|
||||
|
||||
label: ->
|
||||
"Creating new #{@organizationUnit}..."
|
||||
|
||||
performLocal: ->
|
||||
# When we send drafts, we don't update anything in the app until
|
||||
# it actually succeeds. We don't want users to think messages have
|
||||
# already sent when they haven't!
|
||||
if not @category
|
||||
return Promise.reject(new Error("Attempt to call SyncbackCategoryTask.performLocal without @category."))
|
||||
else if @organizationUnit isnt "label" and @organizationUnit isnt "folder"
|
||||
return Promise.reject(new Error("Attempt to call SyncbackCategoryTask.performLocal with @organizationUnit which wasn't 'folder' or 'label'."))
|
||||
|
||||
if @_shouldChangeBackwards()
|
||||
DatabaseStore.unpersistModel @category
|
||||
else
|
||||
DatabaseStore.persistModel @category
|
||||
|
||||
performRemote: ->
|
||||
if @organizationUnit is "label"
|
||||
path = "/labels"
|
||||
else if @organizationUnit is "folder"
|
||||
path = "/folders"
|
||||
|
||||
NylasAPI.makeRequest
|
||||
path: path
|
||||
method: 'POST'
|
||||
accountId: @category.accountId
|
||||
body:
|
||||
display_name: @category.displayName
|
||||
# returnsModel must be false because we want to update the
|
||||
# existing model rather than returning a new model.
|
||||
returnsModel: false
|
||||
.then (json) =>
|
||||
# This is where we update the existing model with the newly
|
||||
# created serverId.
|
||||
@category.serverId = json.id
|
||||
DatabaseStore.persistModel @category
|
||||
.then ->
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
.catch APIError, (err) =>
|
||||
if err.statusCode in NylasAPI.PermanentErrorCodes
|
||||
@_isReverting = true
|
||||
@performLocal().then =>
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
else
|
||||
return Promise.resolve(Task.Status.Retry)
|
||||
|
||||
_shouldChangeBackwards: ->
|
||||
@_isReverting
|
Loading…
Add table
Reference in a new issue