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:
dillon 2015-09-10 10:34:09 -07:00
parent ba00ad9cbe
commit be7b52cb98
12 changed files with 532 additions and 24 deletions

View file

@ -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'

View file

@ -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>&ldquo;{searchValue}&rdquo;</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) =>

View 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()

View 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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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`"))

View file

@ -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`"))

View 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