From 4f34c8403fdf312ef4e5ff8efd1b6f7117bfd1bb Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Wed, 21 Oct 2015 10:38:00 -0700 Subject: [PATCH] feat(trash): Trash for Gmail, and architectural changes for common tasks Summary: This diff centralizes logic for creating common tasks for things like moving to trash, archive, etc. TaskFactory exposes a set of convenience methods and hides the whole "and also remove the current label" business from the user. This diff also formally separates the concept of "moving to trash" and "archiving" so that "remove" isn't used in an unclear way. I also refactored where selection is managed. Previously you'd fire some action like archiveSelection and it'd clear the selection, but if you selected some items and used another method to archive a few, they were still selected. The selection is now bound to the ModelView as intended, so if items are removed from the modelView, they are removed from it's attached selection. This means that it shouldn't /technically/ be possible to have selected items which are not in view. I haven't refactored the tests yet. They are likely broken... Fix next/prev logic Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2157 --- examples/N1-Filters/lib/filters-store.coffee | 17 +- .../category-picker/lib/category-picker.cjsx | 87 ++----- .../spec/category-picker-spec.cjsx | 214 +++++------------- internal_packages/message-list/lib/main.cjsx | 11 +- .../lib/thread-archive-button.cjsx | 35 +++ .../lib/thread-remove-button.cjsx | 37 --- .../lib/thread-toggle-unread-button.cjsx | 5 +- .../message-list/lib/thread-trash-button.cjsx | 36 +++ .../thread-list/lib/draft-buttons.cjsx | 8 +- .../thread-list/lib/draft-list-store.coffee | 9 - internal_packages/thread-list/lib/main.cjsx | 11 +- .../thread-list/lib/thread-buttons.cjsx | 85 ++++--- .../lib/thread-list-quick-actions.cjsx | 37 ++- .../thread-list/lib/thread-list-store.coffee | 166 ++------------ .../thread-list/lib/thread-list.cjsx | 53 +++-- .../thread-list/spec/thread-list-spec.cjsx | 3 - spec/database-view-spec.coffee | 8 + spec/tasks/remove-thread-helper-spec.coffee | 88 ------- spec/tasks/task-factory-spec.coffee | 88 +++++++ src/components/multiselect-action-bar.cjsx | 2 +- src/flux/actions.coffee | 23 -- src/flux/models/account.coffee | 6 + src/flux/models/model.coffee | 3 + src/flux/stores/database-view.coffee | 13 +- src/flux/stores/model-view-selection.coffee | 6 + src/flux/stores/model-view.coffee | 11 +- src/flux/tasks/task-factory.coffee | 79 +++++++ src/global/nylas-exports.coffee | 4 +- src/mail-view-filter.coffee | 27 ++- src/services/remove-thread-helper.coffee | 93 -------- 30 files changed, 549 insertions(+), 716 deletions(-) create mode 100644 internal_packages/message-list/lib/thread-archive-button.cjsx delete mode 100644 internal_packages/message-list/lib/thread-remove-button.cjsx create mode 100644 internal_packages/message-list/lib/thread-trash-button.cjsx delete mode 100644 spec/tasks/remove-thread-helper-spec.coffee create mode 100644 spec/tasks/task-factory-spec.coffee create mode 100644 src/flux/tasks/task-factory.coffee delete mode 100644 src/services/remove-thread-helper.coffee diff --git a/examples/N1-Filters/lib/filters-store.coffee b/examples/N1-Filters/lib/filters-store.coffee index 0a8d857a9..52a07638d 100644 --- a/examples/N1-Filters/lib/filters-store.coffee +++ b/examples/N1-Filters/lib/filters-store.coffee @@ -2,7 +2,7 @@ NylasStore = require 'nylas-store' _ = require 'underscore' _s = require 'underscore.string' {Actions, CategoryStore, AccountStore, ChangeLabelsTask, - ChangeFolderTask, ArchiveThreadHelper, ChangeStarredTask, + ChangeFolderTask, TaskFactory, ChangeStarredTask, ChangeUnreadTask, Utils} = require 'nylas-exports' # The FiltersStore performs all business logic for filters: the single source @@ -90,25 +90,14 @@ class FiltersStore extends NylasStore unread: false threads: [thread] else if action is "archive" and val is true - ArchiveThreadHelper.getArchiveTask [thread] + TaskFactory.taskForArchiving({threads: [thread]}) else if action is "star" and val is true new ChangeStarredTask starred: true threads: [thread] else if action is "delete" and val is true - trash = CategoryStore.getStandardCategory "trash" + TaskFactory.taskForMovingToTrash({threads: [thread]}) - # Some email providers use labels, like Gmail, and others use folders, - # like Microsoft Exchange. Labels and folders behave very differently, - # so there are different Task classes to modify records for them. - if AccountStore.current().usesFolders() - new ChangeFolderTask - folder: trash - threads: [thread] - else - new ChangeLabelsTask - labelsToAdd: [trash] - threads: [thread] .value() _getPassedFilters: ({message, thread}) => diff --git a/internal_packages/category-picker/lib/category-picker.cjsx b/internal_packages/category-picker/lib/category-picker.cjsx index b031c819f..1e7bbf031 100644 --- a/internal_packages/category-picker/lib/category-picker.cjsx +++ b/internal_packages/category-picker/lib/category-picker.cjsx @@ -7,12 +7,11 @@ React = require 'react' Thread, Actions, TaskQueue, + TaskFactory, AccountStore, CategoryStore, DatabaseStore, WorkspaceStore, - ChangeLabelsTask, - ChangeFolderTask, SyncbackCategoryTask, TaskQueueStatusStore, FocusedMailViewStore} = require 'nylas-exports' @@ -194,72 +193,36 @@ class CategoryPicker extends React.Component return {parts} _onSelectCategory: (item) => - return unless @_threads().length > 0 + threads = @_threads() + + return unless threads.length > 0 return unless @_account @refs.menu.setSelectedItem(null) - if @_account.usesLabels() - if item.newCategoryItem - cat = new Label - displayName: @state.searchValue, - accountId: AccountStore.current().id - task = new SyncbackCategoryTask - category: cat - organizationUnit: "label" + if item.newCategoryItem + category = new AccountStore.current().categoryClass() + displayName: @state.searchValue, + accountId: AccountStore.current().id + syncbackTask = new SyncbackCategoryTask({category}) + TaskQueueStatusStore.waitForPerformRemote(syncbackTask).then => + DatabaseStore.findBy(category.constructor, clientId: category.clientId).then (category) => + applyTask = TaskFactory.taskForApplyingCategory + threads: threads + category: category + Actions.queueTask(applyTask) + Actions.queueTask(syncbackTask) - 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) - - else if @_account.usesFolders() - 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 if item.usage is threads.length + applyTask = TaskFactory.taskForRemovingCategory + threads: threads + category: item.category + Actions.queueTask(applyTask) else - throw new Error("Invalid organizationUnit") + applyTask = TaskFactory.taskForApplyingCategory + threads: threads + category: item.category + Actions.queueTask(applyTask) @refs.popover.close() diff --git a/internal_packages/category-picker/spec/category-picker-spec.cjsx b/internal_packages/category-picker/spec/category-picker-spec.cjsx index 903a50699..878d9c1e8 100644 --- a/internal_packages/category-picker/spec/category-picker-spec.cjsx +++ b/internal_packages/category-picker/spec/category-picker-spec.cjsx @@ -11,13 +11,12 @@ CategoryPicker = require '../lib/category-picker' Actions, CategoryStore, DatabaseStore, - ChangeLabelsTask, - ChangeFolderTask, + TaskFactory, SyncbackCategoryTask, FocusedMailViewStore, TaskQueueStatusStore} = require 'nylas-exports' -describe 'CategoryPicker', -> +fdescribe 'CategoryPicker', -> beforeEach -> CategoryStore._categoryCache = {} @@ -78,23 +77,6 @@ describe 'CategoryPicker', -> 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 @ @@ -128,156 +110,74 @@ describe 'CategoryPicker', -> expect(count).toBe 1 describe "_onSelectCategory()", -> - describe "using labels", -> + beforeEach -> + setupForCreateNew.call @, "folder" + spyOn(TaskFactory, 'taskForRemovingCategory').andCallThrough() + spyOn(TaskFactory, 'taskForApplyingCategory').andCallThrough() + spyOn(Actions, "queueTask") + + it "closes the popover", -> + spyOn(@popover, "close") + @picker._onSelectCategory { usage: 0, category: "asdf" } + expect(@popover.close).toHaveBeenCalled() + + describe "when selecting a category currently on all the selected items", -> + it "fires a task to remove the category", -> + input = + category: "asdf" + usage: 1 + + @picker._onSelectCategory(input) + expect(TaskFactory.taskForRemovingCategory).toHaveBeenCalledWith + threads: [@testThread] + category: "asdf" + expect(Actions.queueTask).toHaveBeenCalled() + + describe "when selecting a category not on all the selected items", -> + it "fires a task to add the category", -> + input = + category: "asdf" + usage: 0 + + @picker._onSelectCategory(input) + expect(TaskFactory.taskForApplyingCategory).toHaveBeenCalledWith + threads: [@testThread] + category: "asdf" + expect(Actions.queueTask).toHaveBeenCalled() + + describe "when selecting a new category", -> 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 + input = + newCategoryItem: true + @picker.setState(searchValue: "teSTing!") + @picker._onSelectCategory(input) + it "queues a new syncback task for creating a category", -> 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") + it "queues a task for applying the category after it has saved", -> + label = new Label(displayName: "teSTing!") 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 + expect(klass).toBe(Label) + expect(typeof clientId).toBe("string") + Promise.resolve(label) + + waitsFor -> + Actions.queueTask.calls.length > 1 + label = Actions.queueTask.calls[0].args[0].category 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() + expect(TaskFactory.taskForApplyingCategory).toHaveBeenCalledWith + threads: [@testThread] + category: label + expect(TaskFactory.taskForApplyingCategory.callCount).toBe(1) diff --git a/internal_packages/message-list/lib/main.cjsx b/internal_packages/message-list/lib/main.cjsx index f5e0f62ec..fb0273258 100644 --- a/internal_packages/message-list/lib/main.cjsx +++ b/internal_packages/message-list/lib/main.cjsx @@ -9,7 +9,8 @@ MessageToolbarItems = require "./message-toolbar-items" SidebarContactList} = require "./sidebar-components" ThreadStarButton = require './thread-star-button' -ThreadRemoveButton = require './thread-remove-button' +ThreadArchiveButton = require './thread-archive-button' +ThreadTrashButton = require './thread-trash-button' ThreadToggleUnreadButton = require './thread-toggle-unread-button' AutolinkerExtension = require './plugins/autolinker-extension' @@ -36,7 +37,10 @@ module.exports = ComponentRegistry.register ThreadStarButton, role: 'message:Toolbar' - ComponentRegistry.register ThreadRemoveButton, + ComponentRegistry.register ThreadArchiveButton, + role: 'message:Toolbar' + + ComponentRegistry.register ThreadTrashButton, role: 'message:Toolbar' ComponentRegistry.register ThreadToggleUnreadButton, @@ -48,7 +52,8 @@ module.exports = deactivate: -> ComponentRegistry.unregister MessageList ComponentRegistry.unregister ThreadStarButton - ComponentRegistry.unregister ThreadRemoveButton + ComponentRegistry.unregister ThreadArchiveButton + ComponentRegistry.unregister ThreadTrashButton ComponentRegistry.unregister ThreadToggleUnreadButton ComponentRegistry.unregister MessageToolbarItems ComponentRegistry.unregister SidebarContactCard diff --git a/internal_packages/message-list/lib/thread-archive-button.cjsx b/internal_packages/message-list/lib/thread-archive-button.cjsx new file mode 100644 index 000000000..a9adf4048 --- /dev/null +++ b/internal_packages/message-list/lib/thread-archive-button.cjsx @@ -0,0 +1,35 @@ +_ = require 'underscore' +React = require 'react' +{RetinaImg} = require 'nylas-component-kit' +{Actions, + TaskFactory, + DOMUtils, + FocusedMailViewStore} = require 'nylas-exports' + +class ThreadArchiveButton extends React.Component + @displayName: "ThreadArchiveButton" + @containerRequired: false + + @propTypes: + thread: React.PropTypes.object.isRequired + + render: => + return false unless FocusedMailViewStore.mailView()?.canArchiveThreads() + + + + _onArchive: (e) => + return unless DOMUtils.nodeIsVisible(e.currentTarget) + task = TaskFactory.taskForArchiving + threads: [@props.thread], + fromView: FocusedMailViewStore.mailView() + Actions.queueTask(task) + e.stopPropagation() + + +module.exports = ThreadArchiveButton diff --git a/internal_packages/message-list/lib/thread-remove-button.cjsx b/internal_packages/message-list/lib/thread-remove-button.cjsx deleted file mode 100644 index d4b5d2807..000000000 --- a/internal_packages/message-list/lib/thread-remove-button.cjsx +++ /dev/null @@ -1,37 +0,0 @@ -_ = require 'underscore' -React = require 'react' -{Actions, - DOMUtils, - RemoveThreadHelper, - FocusedMailViewStore} = require 'nylas-exports' -{RetinaImg} = require 'nylas-component-kit' - -class ThreadRemoveButton extends React.Component - @displayName: "ThreadRemoveButton" - @containerRequired: false - - render: => - focusedMailViewFilter = FocusedMailViewStore.mailView() - return false unless focusedMailViewFilter?.canRemoveThreads() - - if RemoveThreadHelper.removeType() is RemoveThreadHelper.Type.Archive - tooltip = "Archive" - imgName = "toolbar-archive.png" - else if RemoveThreadHelper.removeType() is RemoveThreadHelper.Type.Trash - tooltip = "Trash" - imgName = "toolbar-trash.png" - - - - _onRemove: (e) => - return unless DOMUtils.nodeIsVisible(e.currentTarget) - Actions.removeCurrentlyFocusedThread() - e.stopPropagation() - - -module.exports = ThreadRemoveButton diff --git a/internal_packages/message-list/lib/thread-toggle-unread-button.cjsx b/internal_packages/message-list/lib/thread-toggle-unread-button.cjsx index dfe67757c..f2a877e1c 100644 --- a/internal_packages/message-list/lib/thread-toggle-unread-button.cjsx +++ b/internal_packages/message-list/lib/thread-toggle-unread-button.cjsx @@ -18,12 +18,11 @@ class ThreadToggleUnreadButton extends React.Component _onClick: (e) => - e.stopPropagation() - task = new ChangeUnreadTask thread: @props.thread unread: !@props.thread.unread - Actions.queueTask task + Actions.queueTask(task) Actions.popSheet() + e.stopPropagation() module.exports = ThreadToggleUnreadButton diff --git a/internal_packages/message-list/lib/thread-trash-button.cjsx b/internal_packages/message-list/lib/thread-trash-button.cjsx new file mode 100644 index 000000000..56881ee68 --- /dev/null +++ b/internal_packages/message-list/lib/thread-trash-button.cjsx @@ -0,0 +1,36 @@ +_ = require 'underscore' +React = require 'react' +{Actions, + DOMUtils, + TaskFactory, + FocusedMailViewStore} = require 'nylas-exports' +{RetinaImg} = require 'nylas-component-kit' + +class ThreadTrashButton extends React.Component + @displayName: "ThreadTrashButton" + @containerRequired: false + + @propTypes: + thread: React.PropTypes.object.isRequired + + render: => + focusedMailViewFilter = FocusedMailViewStore.mailView() + return false unless focusedMailViewFilter?.canTrashThreads() + + + + _onRemove: (e) => + return unless DOMUtils.nodeIsVisible(e.currentTarget) + task = TaskFactory.taskForMovingToTrash + threads: [@props.thread], + fromView: FocusedMailViewStore.mailView() + Actions.queueTask(task) + e.stopPropagation() + + +module.exports = ThreadTrashButton diff --git a/internal_packages/thread-list/lib/draft-buttons.cjsx b/internal_packages/thread-list/lib/draft-buttons.cjsx index fcef15476..d29ccb022 100644 --- a/internal_packages/thread-list/lib/draft-buttons.cjsx +++ b/internal_packages/thread-list/lib/draft-buttons.cjsx @@ -14,11 +14,13 @@ class DraftDeleteButton extends React.Component - _destroyDraft: => - Actions.deleteSelection() + _destroySelected: => + for item in @props.selection.items() + Actions.queueTask(new DestroyDraftTask(draftClientId: item.clientId)) + @props.selection.clear() module.exports = {DraftDeleteButton} diff --git a/internal_packages/thread-list/lib/draft-list-store.coffee b/internal_packages/thread-list/lib/draft-list-store.coffee index 11b355461..2cf646d02 100644 --- a/internal_packages/thread-list/lib/draft-list-store.coffee +++ b/internal_packages/thread-list/lib/draft-list-store.coffee @@ -13,7 +13,6 @@ class DraftListStore extends NylasStore constructor: -> @listenTo DatabaseStore, @_onDataChanged @listenTo AccountStore, @_onAccountChanged - @listenTo Actions.deleteSelection, @_onDeleteSelection # It's important to listen to sendDraftSuccess because the # _onDataChanged method will ignore our newly created draft because it @@ -52,12 +51,4 @@ class DraftListStore extends NylasStore return unless containsDraft and @_view @_view.invalidate() - _onDeleteSelection: => - selected = @_view.selection.items() - - for item in selected - Actions.queueTask(new DestroyDraftTask(draftClientId: item.clientId)) - - @_view.selection.clear() - module.exports = new DraftListStore() diff --git a/internal_packages/thread-list/lib/main.cjsx b/internal_packages/thread-list/lib/main.cjsx index ecf82fc66..6e32fb31f 100644 --- a/internal_packages/thread-list/lib/main.cjsx +++ b/internal_packages/thread-list/lib/main.cjsx @@ -2,7 +2,8 @@ _ = require 'underscore' React = require "react" {ComponentRegistry, WorkspaceStore} = require "nylas-exports" -{DownButton, UpButton, ThreadBulkRemoveButton, ThreadBulkStarButton, ThreadBulkToggleUnreadButton} = require "./thread-buttons" +{DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkTrashButton, + ThreadBulkStarButton, ThreadBulkToggleUnreadButton} = require "./thread-buttons" {DraftDeleteButton} = require "./draft-buttons" ThreadSelectionBar = require './thread-selection-bar' ThreadList = require './thread-list' @@ -44,7 +45,10 @@ module.exports = location: WorkspaceStore.Sheet.Thread.Toolbar.Right modes: ['list'] - ComponentRegistry.register ThreadBulkRemoveButton, + ComponentRegistry.register ThreadBulkArchiveButton, + role: 'thread:BulkAction' + + ComponentRegistry.register ThreadBulkTrashButton, role: 'thread:BulkAction' ComponentRegistry.register ThreadBulkStarButton, @@ -61,7 +65,8 @@ module.exports = ComponentRegistry.unregister DraftSelectionBar ComponentRegistry.unregister ThreadList ComponentRegistry.unregister ThreadSelectionBar - ComponentRegistry.unregister ThreadBulkRemoveButton + ComponentRegistry.unregister ThreadBulkArchiveButton + ComponentRegistry.unregister ThreadBulkTrashButton ComponentRegistry.unregister ThreadBulkToggleUnreadButton ComponentRegistry.unregister DownButton ComponentRegistry.unregister UpButton diff --git a/internal_packages/thread-list/lib/thread-buttons.cjsx b/internal_packages/thread-list/lib/thread-buttons.cjsx index c9da92f1f..1450bea3c 100644 --- a/internal_packages/thread-list/lib/thread-buttons.cjsx +++ b/internal_packages/thread-list/lib/thread-buttons.cjsx @@ -3,37 +3,57 @@ classNames = require 'classnames' ThreadListStore = require './thread-list-store' {RetinaImg} = require 'nylas-component-kit' {Actions, - RemoveThreadHelper, + TaskFactory, + CategoryStore, FocusedContentStore, FocusedMailViewStore} = require "nylas-exports" -class ThreadBulkRemoveButton extends React.Component - @displayName: 'ThreadBulkRemoveButton' +class ThreadBulkArchiveButton extends React.Component + @displayName: 'ThreadBulkArchiveButton' @containerRequired: false @propTypes: selection: React.PropTypes.object.isRequired render: -> - focusedMailViewFilter = FocusedMailViewStore.mailView() - return false unless focusedMailViewFilter?.canRemoveThreads() + return false unless mailViewFilter?.canArchiveThreads() - if RemoveThreadHelper.removeType() is RemoveThreadHelper.Type.Archive - tooltip = "Archive" - imgName = "toolbar-archive.png" - else if RemoveThreadHelper.removeType() is RemoveThreadHelper.Type.Trash - tooltip = "Trash" - imgName = "toolbar-trash.png" + + + _onArchive: => + task = TaskFactory.taskForArchiving + threads: @props.selection.items(), + fromView: FocusedMailViewStore.mailView() + Actions.queueTask(task) + +class ThreadBulkTrashButton extends React.Component + @displayName: 'ThreadBulkTrashButton' + @containerRequired: false + + @propTypes: + selection: React.PropTypes.object.isRequired + + render: -> + mailViewFilter = FocusedMailViewStore.mailView() + return false unless mailViewFilter?.canTrashThreads() _onRemove: => - Actions.removeSelection() + task = TaskFactory.taskForMovingToTrash + threads: @props.selection.items(), + fromView: FocusedMailViewStore.mailView() + Actions.queueTask(task) class ThreadBulkStarButton extends React.Component @@ -52,7 +72,8 @@ class ThreadBulkStarButton extends React.Component _onStar: => - Actions.toggleStarSelection() + task = TaskFactory.taskForInvertingStarred(threads: @props.selection.items()) + Actions.queueTask(task) class ThreadBulkToggleUnreadButton extends React.Component @@ -62,19 +83,9 @@ class ThreadBulkToggleUnreadButton extends React.Component @propTypes: selection: React.PropTypes.object.isRequired - constructor: -> - @state = @_getStateFromStores() - super - - componentDidMount: => - @unsubscribers = [] - @unsubscribers.push ThreadListStore.listen @_onStoreChange - - componentWillUnmount: => - unsubscribe() for unsubscribe in @unsubscribers - render: => - fragment = if @state.canMarkUnread then "unread" else "read" + canMarkUnread = not @props.selection.items().every (s) -> s.unread is true + fragment = if canMarkUnread then "unread" else "read"