diff --git a/exports/nylas-exports.coffee b/exports/nylas-exports.coffee index 6a5e28907..5e5a7ad51 100644 --- a/exports/nylas-exports.coffee +++ b/exports/nylas-exports.coffee @@ -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' diff --git a/internal_packages/category-picker/lib/category-picker.cjsx b/internal_packages/category-picker/lib/category-picker.cjsx index 81fccfd20..c19e055d8 100644 --- a/internal_packages/category-picker/lib/category-picker.cjsx +++ b/internal_packages/category-picker/lib/category-picker.cjsx @@ -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 + return + 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 + _renderCreateNewItem: ({searchValue, name}) => + if @_account?.usesLabels() + picName = "tag" + else if @_account?.usesFolders() + picName = "folder" + +
+ +
+ “{searchValue}” (create new) +
+
+ _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) => diff --git a/internal_packages/category-picker/spec/category-picker-spec.cjsx b/internal_packages/category-picker/spec/category-picker-spec.cjsx new file mode 100644 index 000000000..903a50699 --- /dev/null +++ b/internal_packages/category-picker/spec/category-picker-spec.cjsx @@ -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( + + ) + + @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( + + ) + + 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( + + ) + @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() diff --git a/spec-nylas/tasks/syncback-category-task-spec.coffee b/spec-nylas/tasks/syncback-category-task-spec.coffee new file mode 100644 index 000000000..006509bbe --- /dev/null +++ b/spec-nylas/tasks/syncback-category-task-spec.coffee @@ -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" diff --git a/src/components/menu.cjsx b/src/components/menu.cjsx index d6e4dbdff..390f79e24 100644 --- a/src/components/menu.cjsx +++ b/src/components/menu.cjsx @@ -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 diff --git a/src/flux/models/folder.coffee b/src/flux/models/folder.coffee index 9a741424c..fd3538ffa 100644 --- a/src/flux/models/folder.coffee +++ b/src/flux/models/folder.coffee @@ -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 diff --git a/src/flux/models/label.coffee b/src/flux/models/label.coffee index 86c5b3909..b4a4fcb41 100644 --- a/src/flux/models/label.coffee +++ b/src/flux/models/label.coffee @@ -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 diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee index 1e1800842..80c48d57f 100644 --- a/src/flux/stores/database-store.coffee +++ b/src/flux/stores/database-store.coffee @@ -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 diff --git a/src/flux/stores/task-queue-status-store.coffee b/src/flux/stores/task-queue-status-store.coffee index dd42ee957..efb167714 100644 --- a/src/flux/stores/task-queue-status-store.coffee +++ b/src/flux/stores/task-queue-status-store.coffee @@ -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() diff --git a/src/flux/tasks/change-folder-task.coffee b/src/flux/tasks/change-folder-task.coffee index 509980fc6..494873aea 100644 --- a/src/flux/tasks/change-folder-task.coffee +++ b/src/flux/tasks/change-folder-task.coffee @@ -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`")) diff --git a/src/flux/tasks/change-labels-task.coffee b/src/flux/tasks/change-labels-task.coffee index 3093ef1b8..4363a75d5 100644 --- a/src/flux/tasks/change-labels-task.coffee +++ b/src/flux/tasks/change-labels-task.coffee @@ -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`")) diff --git a/src/flux/tasks/syncback-category-task.coffee b/src/flux/tasks/syncback-category-task.coffee new file mode 100644 index 000000000..77c14079b --- /dev/null +++ b/src/flux/tasks/syncback-category-task.coffee @@ -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