mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-03 05:51:07 +08:00
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
This commit is contained in:
parent
f7c6ae0774
commit
4f34c8403f
30 changed files with 549 additions and 716 deletions
|
@ -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}) =>
|
||||
|
|
|
@ -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 <span>{parts}</span>
|
||||
|
||||
_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()
|
||||
|
||||
|
|
|
@ -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(
|
||||
<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()
|
||||
expect(TaskFactory.taskForApplyingCategory).toHaveBeenCalledWith
|
||||
threads: [@testThread]
|
||||
category: label
|
||||
expect(TaskFactory.taskForApplyingCategory.callCount).toBe(1)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
<button className="btn btn-toolbar btn-archive"
|
||||
style={order: -107}
|
||||
data-tooltip="Archive"
|
||||
onClick={@_onArchive}>
|
||||
<RetinaImg name="toolbar-archive.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
|
||||
_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
|
|
@ -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"
|
||||
|
||||
<button className="btn btn-toolbar"
|
||||
style={order: -106}
|
||||
data-tooltip={tooltip}
|
||||
onClick={@_onRemove}>
|
||||
<RetinaImg name={imgName} mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
|
||||
_onRemove: (e) =>
|
||||
return unless DOMUtils.nodeIsVisible(e.currentTarget)
|
||||
Actions.removeCurrentlyFocusedThread()
|
||||
e.stopPropagation()
|
||||
|
||||
|
||||
module.exports = ThreadRemoveButton
|
|
@ -18,12 +18,11 @@ class ThreadToggleUnreadButton extends React.Component
|
|||
</button>
|
||||
|
||||
_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
|
||||
|
|
36
internal_packages/message-list/lib/thread-trash-button.cjsx
Normal file
36
internal_packages/message-list/lib/thread-trash-button.cjsx
Normal file
|
@ -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()
|
||||
|
||||
<button className="btn btn-toolbar"
|
||||
style={order: -106}
|
||||
data-tooltip="Move to Trash"
|
||||
onClick={@_onRemove}>
|
||||
<RetinaImg name="toolbar-trash.png" mode={RetinaImg.Mode.ContentIsMask}/>
|
||||
</button>
|
||||
|
||||
_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
|
|
@ -14,11 +14,13 @@ class DraftDeleteButton extends React.Component
|
|||
<button style={order:-100}
|
||||
className="btn btn-toolbar"
|
||||
data-tooltip="Delete"
|
||||
onClick={@_destroyDraft}>
|
||||
onClick={@_destroySelected}>
|
||||
<RetinaImg name="icon-composer-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
|
||||
_destroyDraft: =>
|
||||
Actions.deleteSelection()
|
||||
_destroySelected: =>
|
||||
for item in @props.selection.items()
|
||||
Actions.queueTask(new DestroyDraftTask(draftClientId: item.clientId))
|
||||
@props.selection.clear()
|
||||
|
||||
module.exports = {DraftDeleteButton}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
<button style={order:-107}
|
||||
className="btn btn-toolbar"
|
||||
data-tooltip="Archive"
|
||||
onClick={@_onArchive}>
|
||||
<RetinaImg name="toolbar-archive.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
|
||||
_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()
|
||||
|
||||
<button style={order:-106}
|
||||
className="btn btn-toolbar"
|
||||
data-tooltip={tooltip}
|
||||
data-tooltip="Move to Trash"
|
||||
onClick={@_onRemove}>
|
||||
<RetinaImg name={imgName} mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<RetinaImg name="toolbar-trash.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
|
||||
_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
|
|||
</button>
|
||||
|
||||
_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"
|
||||
|
||||
<button style={order:-105}
|
||||
className="btn btn-toolbar"
|
||||
|
@ -85,15 +96,8 @@ class ThreadBulkToggleUnreadButton extends React.Component
|
|||
</button>
|
||||
|
||||
_onClick: =>
|
||||
Actions.toggleUnreadSelection()
|
||||
|
||||
_onStoreChange: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
_getStateFromStores: =>
|
||||
selections = ThreadListStore.view().selection.items()
|
||||
canMarkUnread: not selections.every (s) -> s.unread is true
|
||||
|
||||
task = TaskFactory.taskForInvertingUnread(threads: @props.selection.items())
|
||||
Actions.queueTask(task)
|
||||
|
||||
|
||||
ThreadNavButtonMixin =
|
||||
|
@ -171,4 +175,11 @@ UpButton = React.createClass
|
|||
UpButton.containerRequired = false
|
||||
DownButton.containerRequired = false
|
||||
|
||||
module.exports = {DownButton, UpButton, ThreadBulkRemoveButton, ThreadBulkStarButton, ThreadBulkToggleUnreadButton}
|
||||
module.exports = {
|
||||
DownButton,
|
||||
UpButton,
|
||||
ThreadBulkArchiveButton,
|
||||
ThreadBulkTrashButton,
|
||||
ThreadBulkStarButton,
|
||||
ThreadBulkToggleUnreadButton
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
React = require 'react'
|
||||
{Actions,
|
||||
RemoveThreadHelper,
|
||||
CategoryStore,
|
||||
TaskFactory,
|
||||
FocusedMailViewStore} = require 'nylas-exports'
|
||||
|
||||
class ThreadListQuickActions extends React.Component
|
||||
|
@ -9,22 +10,42 @@ class ThreadListQuickActions extends React.Component
|
|||
thread: React.PropTypes.object
|
||||
|
||||
render: =>
|
||||
focusedMailViewFilter = FocusedMailViewStore.mailView()
|
||||
return false unless focusedMailViewFilter?.canRemoveThreads()
|
||||
mailViewFilter = FocusedMailViewStore.mailView()
|
||||
archive = null
|
||||
remove = null
|
||||
|
||||
classNames = "btn action action-#{RemoveThreadHelper.removeType()}"
|
||||
if mailViewFilter?.canArchiveThreads()
|
||||
archive = <div key="archive"
|
||||
className="btn action action-archive"
|
||||
onClick={@_onArchive}></div>
|
||||
|
||||
if mailViewFilter?.canTrashThreads()
|
||||
trash = <div key="remove"
|
||||
className='btn action action-trash'
|
||||
onClick={@_onRemove}></div>
|
||||
|
||||
<div className="inner">
|
||||
<div key="remove" className={classNames} onClick={@_onRemove}></div>
|
||||
{archive}
|
||||
{trash}
|
||||
</div>
|
||||
|
||||
shouldComponentUpdate: (newProps, newState) ->
|
||||
newProps.thread.id isnt @props?.thread.id
|
||||
|
||||
_onArchive: (event) =>
|
||||
task = TaskFactory.taskForArchiving
|
||||
threads: [@props.thread]
|
||||
fromView: FocusedMailViewStore.mailView()
|
||||
Actions.queueTask(task)
|
||||
|
||||
# Don't trigger the thread row click
|
||||
event.stopPropagation()
|
||||
|
||||
_onRemove: (event) =>
|
||||
focusedMailViewFilter = FocusedMailViewStore.mailView()
|
||||
t = RemoveThreadHelper.getRemovalTask([@props.thread], focusedMailViewFilter)
|
||||
Actions.queueTask(t)
|
||||
task = TaskFactory.taskForMovingToTrash
|
||||
threads: [@props.thread]
|
||||
fromView: FocusedMailViewStore.mailView()
|
||||
Actions.queueTask(task)
|
||||
|
||||
# Don't trigger the thread row click
|
||||
event.stopPropagation()
|
||||
|
|
|
@ -9,10 +9,7 @@ NylasStore = require 'nylas-store'
|
|||
DatabaseStore,
|
||||
AccountStore,
|
||||
WorkspaceStore,
|
||||
ChangeUnreadTask,
|
||||
ChangeStarredTask,
|
||||
FocusedContentStore,
|
||||
RemoveThreadHelper,
|
||||
TaskQueueStatusStore,
|
||||
FocusedMailViewStore} = require 'nylas-exports'
|
||||
|
||||
|
@ -22,20 +19,6 @@ class ThreadListStore extends NylasStore
|
|||
constructor: ->
|
||||
@_resetInstanceVars()
|
||||
|
||||
@listenTo Actions.removeSelection, @_onRemoveSelection
|
||||
|
||||
@listenTo Actions.removeCurrentlyFocusedThread, @_onRemoveAndAuto
|
||||
@listenTo Actions.removeAndNext, @_onRemoveAndNext
|
||||
@listenTo Actions.removeAndPrevious, @_onRemoveAndPrev
|
||||
|
||||
@listenTo Actions.moveThread, @_onMoveThread
|
||||
@listenTo Actions.moveThreads, @_onMoveThreads
|
||||
|
||||
@listenTo Actions.toggleStarSelection, @_onToggleStarSelection
|
||||
@listenTo Actions.toggleStarFocused, @_onToggleStarFocused
|
||||
|
||||
@listenTo Actions.toggleUnreadSelection, @_onToggleUnreadSelection
|
||||
|
||||
@listenTo DatabaseStore, @_onDataChanged
|
||||
@listenTo AccountStore, @_onAccountChanged
|
||||
@listenTo FocusedMailViewStore, @_onMailViewChanged
|
||||
|
@ -115,139 +98,36 @@ class ThreadListStore extends NylasStore
|
|||
return unless @_view
|
||||
|
||||
if change.objectClass is Thread.name
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
keyboardId = FocusedContentStore.keyboardCursorId('thread')
|
||||
viewModeAutofocuses = WorkspaceStore.layoutMode() is 'split' or WorkspaceStore.topSheet().root is true
|
||||
|
||||
focusedIndex = @_view.indexOfId(focusedId)
|
||||
keyboardIndex = @_view.indexOfId(keyboardId)
|
||||
|
||||
shiftIndex = (i) =>
|
||||
if i > 0 and (@_view.get(i - 1)?.unread or i >= @_view.count())
|
||||
return i - 1
|
||||
else
|
||||
return i
|
||||
|
||||
@_view.invalidate({change: change, shallow: true})
|
||||
|
||||
focusedLost = focusedIndex >= 0 and @_view.indexOfId(focusedId) is -1
|
||||
keyboardLost = keyboardIndex >= 0 and @_view.indexOfId(keyboardId) is -1
|
||||
|
||||
if viewModeAutofocuses and focusedLost
|
||||
Actions.setFocus(collection: 'thread', item: @_view.get(shiftIndex(focusedIndex)))
|
||||
|
||||
if keyboardLost
|
||||
Actions.setCursorPosition(collection: 'thread', item: @_view.get(shiftIndex(keyboardIndex)))
|
||||
|
||||
if change.objectClass is Message.name
|
||||
# Important: Until we optimize this so that it detects the set change
|
||||
# and avoids a query, this should be debounced since it's very unimportant
|
||||
# and avoids a query, this should be defered since it's very unimportant
|
||||
_.defer =>
|
||||
threadIds = _.uniq _.map change.objects, (m) -> m.threadId
|
||||
@_view.invalidateMetadataFor(threadIds)
|
||||
|
||||
_onToggleStarSelection: ->
|
||||
threads = @_view.selection.items()
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
keyboardId = FocusedContentStore.keyboardCursorId('thread')
|
||||
|
||||
oneAlreadyStarred = false
|
||||
for thread in threads
|
||||
if thread.starred
|
||||
oneAlreadyStarred = true
|
||||
|
||||
starred = not oneAlreadyStarred
|
||||
task = new ChangeStarredTask({threads, starred})
|
||||
Actions.queueTask(task)
|
||||
|
||||
_onToggleStarFocused: ->
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
cursor = FocusedContentStore.keyboardCursor('thread')
|
||||
if focused
|
||||
task = new ChangeStarredTask(thread: focused, starred: !focused.starred)
|
||||
else if cursor
|
||||
task = new ChangeStarredTask(thread: cursor, starred: !cursor.starred)
|
||||
|
||||
if task
|
||||
Actions.queueTask(task)
|
||||
|
||||
_onToggleUnreadSelection: ->
|
||||
threads = @_view.selection.items()
|
||||
allUnread = threads.every (t) ->
|
||||
t.unread is true
|
||||
unread = not allUnread
|
||||
|
||||
task = new ChangeUnreadTask {threads, unread}
|
||||
Actions.queueTask task
|
||||
|
||||
_onRemoveAndAuto: ->
|
||||
@_removeAndShiftBy('auto')
|
||||
|
||||
_onRemoveAndPrev: ->
|
||||
@_removeAndShiftBy(-1)
|
||||
|
||||
_onRemoveAndNext: ->
|
||||
@_removeAndShiftBy(1)
|
||||
|
||||
_removeAndShiftBy: (offset) ->
|
||||
mailViewFilter = FocusedMailViewStore.mailView()
|
||||
return unless mailViewFilter.canApplyToThreads()
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
return unless focused
|
||||
task = RemoveThreadHelper.getRemovalTask([focused], mailViewFilter)
|
||||
@_moveAndShiftBy(offset, task)
|
||||
|
||||
_onRemoveSelection: ->
|
||||
mailViewFilter = FocusedMailViewStore.mailView()
|
||||
return unless mailViewFilter.canApplyToThreads()
|
||||
selectedThreads = @_view.selection.items()
|
||||
return unless selectedThreads.length > 0
|
||||
task = RemoveThreadHelper.getRemovalTask(selectedThreads, mailViewFilter)
|
||||
@_onMoveThreads(selectedThreads, task)
|
||||
|
||||
_onMoveThread: (thread, task) ->
|
||||
@_moveAndShiftBy('auto', task)
|
||||
|
||||
_onMoveThreads: (threads, task) ->
|
||||
threadIds = threads.map (thread) -> thread.id
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
keyboardId = FocusedContentStore.keyboardCursorId('thread')
|
||||
|
||||
if focusedId in threadIds
|
||||
changeFocused = true
|
||||
if keyboardId in threadIds
|
||||
changeKeyboardCursor = true
|
||||
|
||||
if changeFocused or changeKeyboardCursor
|
||||
newFocusIndex = Number.MAX_VALUE
|
||||
for thread in threads
|
||||
newFocusIndex = Math.min(newFocusIndex, @_view.indexOfId(thread.id))
|
||||
|
||||
TaskQueueStatusStore.waitForPerformLocal(task).then =>
|
||||
layoutMode = WorkspaceStore.layoutMode()
|
||||
if changeFocused
|
||||
item = @_view.get(newFocusIndex)
|
||||
Actions.setFocus(collection: 'thread', item: item)
|
||||
if changeKeyboardCursor
|
||||
item = @_view.get(newFocusIndex)
|
||||
Actions.setCursorPosition(collection: 'thread', item: item)
|
||||
Actions.setFocus(collection: 'thread', item: item) if layoutMode is 'split'
|
||||
|
||||
Actions.queueTask(task)
|
||||
@_view.selection.clear()
|
||||
|
||||
_moveAndShiftBy: (offset, task) ->
|
||||
layoutMode = WorkspaceStore.layoutMode()
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
explicitOffset = if offset is "auto" then false else true
|
||||
|
||||
return unless focused
|
||||
|
||||
# Determine the current index
|
||||
index = @_view.indexOfId(focused.id)
|
||||
return if index is -1
|
||||
|
||||
# Determine the next index we want to move to
|
||||
if offset is 'auto'
|
||||
if @_view.get(index - 1)?.unread
|
||||
offset = -1
|
||||
else
|
||||
offset = 1
|
||||
|
||||
index = Math.min(Math.max(index + offset, 0), @_view.count() - 2)
|
||||
nextKeyboard = nextFocus = @_view.get(index)
|
||||
|
||||
# Remove the current thread from selection
|
||||
@_view.selection.remove(focused)
|
||||
|
||||
# If the user is in list mode and removed without specifically saying
|
||||
# "remove and next" or "remove and prev", return to the thread list
|
||||
# instead of focusing on the next message.
|
||||
if layoutMode is 'list' and not explicitOffset
|
||||
nextFocus = null
|
||||
|
||||
# Remove the current thread
|
||||
TaskQueueStatusStore.waitForPerformLocal(task).then =>
|
||||
Actions.setFocus(collection: 'thread', item: nextFocus)
|
||||
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
|
||||
Actions.queueTask(task)
|
||||
|
||||
module.exports = new ThreadListStore()
|
||||
|
|
|
@ -9,11 +9,13 @@ classNames = require 'classnames'
|
|||
{timestamp, subject} = require './formatting-utils'
|
||||
{Actions,
|
||||
Utils,
|
||||
CanvasUtils,
|
||||
Thread,
|
||||
CanvasUtils,
|
||||
TaskFactory,
|
||||
WorkspaceStore,
|
||||
AccountStore,
|
||||
CategoryStore,
|
||||
FocusedContentStore,
|
||||
FocusedMailViewStore} = require 'nylas-exports'
|
||||
|
||||
ThreadListParticipants = require './thread-list-participants'
|
||||
|
@ -165,11 +167,21 @@ class ThreadList extends React.Component
|
|||
|
||||
@narrowColumns = [cNarrow]
|
||||
|
||||
_shift = ({offset, afterRunning}) =>
|
||||
view = ThreadListStore.view()
|
||||
focusedId = FocusedContentStore.focusedId('thread')
|
||||
focusedIdx = Math.min(view.count() - 1, Math.max(0, view.indexOfId(focusedId) + offset))
|
||||
item = view.get(focusedIdx)
|
||||
afterRunning()
|
||||
Actions.setFocus(collection: 'thread', item: item)
|
||||
|
||||
@commands =
|
||||
'core:remove-item': @_onRemoveItem
|
||||
'core:remove-item': @_onBackspace
|
||||
'core:star-item': @_onStarItem
|
||||
'core:remove-and-previous': -> Actions.removeAndPrevious()
|
||||
'core:remove-and-next': -> Actions.removeAndNext()
|
||||
'core:remove-and-previous': =>
|
||||
_shift(offset: 1, afterRunning: @_onBackspace)
|
||||
'core:remove-and-next': =>
|
||||
_shift(offset: -1, afterRunning: @_onBackspace)
|
||||
|
||||
@itemPropsProvider = (item) ->
|
||||
className: classNames
|
||||
|
@ -252,22 +264,37 @@ class ThreadList extends React.Component
|
|||
_onStarItem: =>
|
||||
return unless ThreadListStore.view()
|
||||
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
|
||||
if WorkspaceStore.layoutMode() is "list" and WorkspaceStore.topSheet() is WorkspaceStore.Sheet.Thread
|
||||
Actions.toggleStarFocused()
|
||||
threads = [focused]
|
||||
else if ThreadListStore.view().selection.count() > 0
|
||||
Actions.toggleStarSelection()
|
||||
threads = ThreadListStore.view().selection.items()
|
||||
else
|
||||
Actions.toggleStarFocused()
|
||||
threads = [focused]
|
||||
|
||||
_onRemoveItem: =>
|
||||
task = TaskFactory.taskForInvertingStarred({threads})
|
||||
Actions.queueTask(task)
|
||||
|
||||
_onBackspace: =>
|
||||
return unless ThreadListStore.view()
|
||||
|
||||
if WorkspaceStore.layoutMode() is "list" and WorkspaceStore.topSheet() is WorkspaceStore.Sheet.Thread
|
||||
Actions.removeCurrentlyFocusedThread()
|
||||
focused = FocusedContentStore.focused('thread')
|
||||
|
||||
if WorkspaceStore.layoutMode() is "split" and focused
|
||||
task = TaskFactory.taskForMovingToTrash
|
||||
threads: [focused]
|
||||
fromView: FocusedMailViewStore.mailView()
|
||||
Actions.queueTask(task)
|
||||
|
||||
else if ThreadListStore.view().selection.count() > 0
|
||||
Actions.removeSelection()
|
||||
else
|
||||
Actions.removeCurrentlyFocusedThread()
|
||||
task = TaskFactory.taskForMovingToTrash
|
||||
threads: ThreadListStore.view().selection.items()
|
||||
fromView: FocusedMailViewStore.mailView()
|
||||
Actions.queueTask(task)
|
||||
|
||||
else if WorkspaceStore.layoutMode() is "list" and WorkspaceStore.topSheet() is WorkspaceStore.Sheet.Thread
|
||||
Actions.popSheet()
|
||||
|
||||
|
||||
module.exports = ThreadList
|
||||
|
|
|
@ -216,9 +216,6 @@ describe "ThreadList", ->
|
|||
spyOn(ThreadStore, "_onAccountChanged")
|
||||
spyOn(DatabaseStore, "findAll").andCallFake ->
|
||||
new Promise (resolve, reject) -> resolve(test_threads())
|
||||
spyOn(Actions, "removeCurrentlyFocusedThread")
|
||||
spyOn(Actions, "removeAndNext")
|
||||
spyOn(Actions, "removeAndPrevious")
|
||||
ReactTestUtils.spyOnClass(ThreadList, "_prepareColumns").andCallFake ->
|
||||
@_columns = columns
|
||||
|
||||
|
|
|
@ -198,6 +198,7 @@ describe "DatabaseView", ->
|
|||
e.subject = subject
|
||||
@view.invalidateAfterDatabaseChange({objects:[e], type: 'persist'})
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
runs ->
|
||||
expect(@view._pages[1].items[1].id).toEqual(@e.id)
|
||||
|
@ -213,6 +214,7 @@ describe "DatabaseView", ->
|
|||
b.labels = []
|
||||
@view.invalidateAfterDatabaseChange({objects:[b], type: 'persist'})
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
|
||||
it "should optimistically remove them and shift result pages", ->
|
||||
|
@ -230,6 +232,7 @@ describe "DatabaseView", ->
|
|||
runs ->
|
||||
@view.invalidateAfterDatabaseChange({objects:[@b], type: 'unpersist'})
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
|
||||
it "should optimistically remove them and shift result pages", ->
|
||||
|
@ -301,6 +304,7 @@ describe "DatabaseView", ->
|
|||
runs ->
|
||||
@completeQuery()
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
runs ->
|
||||
expect(@view._pages[0].items).toEqual(@items)
|
||||
|
@ -311,6 +315,7 @@ describe "DatabaseView", ->
|
|||
expect(@view._pages[0].loading).toEqual(true)
|
||||
@completeQuery()
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
runs ->
|
||||
expect(@view._pages[0].loading).toEqual(false)
|
||||
|
@ -327,6 +332,7 @@ describe "DatabaseView", ->
|
|||
runs ->
|
||||
@completeQuery()
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
runs ->
|
||||
expect(@view._pages[0].items[0].metadata).toEqual('metadata-for-model-a')
|
||||
|
@ -336,6 +342,7 @@ describe "DatabaseView", ->
|
|||
runs ->
|
||||
@completeQuery()
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
runs ->
|
||||
expect(@view._pages[0].metadata).toEqual
|
||||
|
@ -366,6 +373,7 @@ describe "DatabaseView", ->
|
|||
resolve()
|
||||
|
||||
waitsFor ->
|
||||
advanceClock(1)
|
||||
@view._emitter.emit.callCount > 0
|
||||
|
||||
runs ->
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
Account = require '../../src/flux/models/account'
|
||||
CategoryStore = require '../../src/flux/stores/category-store'
|
||||
RemoveThreadHelper = require '../../src/services/remove-thread-helper'
|
||||
|
||||
ChangeFolderTask = require '../../src/flux/tasks/change-folder-task'
|
||||
ChangeLabelsTask = require '../../src/flux/tasks/change-labels-task'
|
||||
|
||||
describe "RemoveThreadHelper", ->
|
||||
describe "removeType", ->
|
||||
it "returns null if there's no current account", ->
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn null
|
||||
expect(RemoveThreadHelper.removeType()).toBe null
|
||||
|
||||
it "returns the type if it's saved", ->
|
||||
spyOn(atom.config, "get").andReturn "trash"
|
||||
expect(RemoveThreadHelper.removeType()).toBe "trash"
|
||||
|
||||
it "returns the archive category if it exists", ->
|
||||
spyOn(CategoryStore, "getStandardCategory").andReturn {name: "archive"}
|
||||
expect(RemoveThreadHelper.removeType()).toBe "archive"
|
||||
|
||||
it "defaults to archive for Gmail", ->
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn provider: "gmail"
|
||||
expect(RemoveThreadHelper.removeType()).toBe "archive"
|
||||
|
||||
it "defaults to trash for everything else", ->
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn provider: "eas"
|
||||
expect(RemoveThreadHelper.removeType()).toBe "trash"
|
||||
|
||||
describe "getRemovalTask", ->
|
||||
beforeEach ->
|
||||
spyOn(CategoryStore, "byId").andReturn({id: "inbox-id", name: "inbox"})
|
||||
@mailViewFilterStub = categoryId: -> "inbox-id"
|
||||
@categories = []
|
||||
|
||||
spyOn(CategoryStore, "getStandardCategory").andCallFake (cat) =>
|
||||
if cat in @categories
|
||||
return {id: "cat-id", name: cat}
|
||||
else return null
|
||||
|
||||
afterEach ->
|
||||
atom.testOrganizationUnit = null
|
||||
|
||||
it "returns null if there's no current account", ->
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn null
|
||||
expect(RemoveThreadHelper.getRemovalTask()).toBe null
|
||||
|
||||
it "creates the task when using labels and trashing", ->
|
||||
atom.testOrganizationUnit = "label"
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
provider: "eas"
|
||||
organizationUnit: "label"
|
||||
@categories = ["all", "trash"]
|
||||
t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
expect(t instanceof ChangeLabelsTask).toBe true
|
||||
expect(t.labelsToRemove[0].name).toBe "inbox"
|
||||
expect(t.labelsToAdd[0].name).toBe "trash"
|
||||
|
||||
it "creates the task when using labels and archiving", ->
|
||||
@categories = ["all", "archive", "trash"]
|
||||
atom.testOrganizationUnit = "label"
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
provider: "gmail"
|
||||
organizationUnit: "label"
|
||||
t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
expect(t instanceof ChangeLabelsTask).toBe true
|
||||
expect(t.labelsToRemove[0].name).toBe "inbox"
|
||||
expect(t.labelsToAdd[0].name).toBe "all"
|
||||
|
||||
it "creates the task when using folders and trashing", ->
|
||||
@categories = ["all", "trash"]
|
||||
atom.testOrganizationUnit = "folder"
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
provider: "eas"
|
||||
organizationUnit: "folder"
|
||||
t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
expect(t instanceof ChangeFolderTask).toBe true
|
||||
expect(t.folder.name).toBe "trash"
|
||||
|
||||
it "creates the task when using folders and archiving", ->
|
||||
@categories = ["all", "archive", "trash"]
|
||||
atom.testOrganizationUnit = "folder"
|
||||
spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
provider: "gmail"
|
||||
organizationUnit: "folder"
|
||||
t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
expect(t instanceof ChangeFolderTask).toBe true
|
||||
expect(t.folder.name).toBe "archive"
|
88
spec/tasks/task-factory-spec.coffee
Normal file
88
spec/tasks/task-factory-spec.coffee
Normal file
|
@ -0,0 +1,88 @@
|
|||
# Account = require '../../src/flux/models/account'
|
||||
# CategoryStore = require '../../src/flux/stores/category-store'
|
||||
# RemoveThreadHelper = require '../../src/services/remove-thread-helper'
|
||||
#
|
||||
# ChangeFolderTask = require '../../src/flux/tasks/change-folder-task'
|
||||
# ChangeLabelsTask = require '../../src/flux/tasks/change-labels-task'
|
||||
#
|
||||
# describe "RemoveThreadHelper", ->
|
||||
# describe "removeType", ->
|
||||
# it "returns null if there's no current account", ->
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn null
|
||||
# expect(RemoveThreadHelper.removeType()).toBe null
|
||||
#
|
||||
# it "returns the type if it's saved", ->
|
||||
# spyOn(atom.config, "get").andReturn "trash"
|
||||
# expect(RemoveThreadHelper.removeType()).toBe "trash"
|
||||
#
|
||||
# it "returns the archive category if it exists", ->
|
||||
# spyOn(CategoryStore, "getStandardCategory").andReturn {name: "archive"}
|
||||
# expect(RemoveThreadHelper.removeType()).toBe "archive"
|
||||
#
|
||||
# it "defaults to archive for Gmail", ->
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn provider: "gmail"
|
||||
# expect(RemoveThreadHelper.removeType()).toBe "archive"
|
||||
#
|
||||
# it "defaults to trash for everything else", ->
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn provider: "eas"
|
||||
# expect(RemoveThreadHelper.removeType()).toBe "trash"
|
||||
#
|
||||
# describe "getRemovalTask", ->
|
||||
# beforeEach ->
|
||||
# spyOn(CategoryStore, "byId").andReturn({id: "inbox-id", name: "inbox"})
|
||||
# @mailViewFilterStub = categoryId: -> "inbox-id"
|
||||
# @categories = []
|
||||
#
|
||||
# spyOn(CategoryStore, "getStandardCategory").andCallFake (cat) =>
|
||||
# if cat in @categories
|
||||
# return {id: "cat-id", name: cat}
|
||||
# else return null
|
||||
#
|
||||
# afterEach ->
|
||||
# atom.testOrganizationUnit = null
|
||||
#
|
||||
# it "returns null if there's no current account", ->
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn null
|
||||
# expect(RemoveThreadHelper.getRemovalTask()).toBe null
|
||||
#
|
||||
# it "creates the task when using labels and trashing", ->
|
||||
# atom.testOrganizationUnit = "label"
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
# provider: "eas"
|
||||
# organizationUnit: "label"
|
||||
# @categories = ["all", "trash"]
|
||||
# t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
# expect(t instanceof ChangeLabelsTask).toBe true
|
||||
# expect(t.labelsToRemove[0].name).toBe "inbox"
|
||||
# expect(t.labelsToAdd[0].name).toBe "trash"
|
||||
#
|
||||
# it "creates the task when using labels and archiving", ->
|
||||
# @categories = ["all", "archive", "trash"]
|
||||
# atom.testOrganizationUnit = "label"
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
# provider: "gmail"
|
||||
# organizationUnit: "label"
|
||||
# t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
# expect(t instanceof ChangeLabelsTask).toBe true
|
||||
# expect(t.labelsToRemove[0].name).toBe "inbox"
|
||||
# expect(t.labelsToAdd[0].name).toBe "all"
|
||||
#
|
||||
# it "creates the task when using folders and trashing", ->
|
||||
# @categories = ["all", "trash"]
|
||||
# atom.testOrganizationUnit = "folder"
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
# provider: "eas"
|
||||
# organizationUnit: "folder"
|
||||
# t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
# expect(t instanceof ChangeFolderTask).toBe true
|
||||
# expect(t.folder.name).toBe "trash"
|
||||
#
|
||||
# it "creates the task when using folders and archiving", ->
|
||||
# @categories = ["all", "archive", "trash"]
|
||||
# atom.testOrganizationUnit = "folder"
|
||||
# spyOn(RemoveThreadHelper, "_currentAccount").andReturn new Account
|
||||
# provider: "gmail"
|
||||
# organizationUnit: "folder"
|
||||
# t = RemoveThreadHelper.getRemovalTask([], @mailViewFilterStub)
|
||||
# expect(t instanceof ChangeFolderTask).toBe true
|
||||
# expect(t.folder.name).toBe "archive"
|
|
@ -31,7 +31,7 @@ The MultiselectActionBar uses the `ComponentRegistry` to find items to display f
|
|||
collection name. To add an item to the bar created in the example above, register it like this:
|
||||
|
||||
```coffee
|
||||
ComponentRegistry.register ThreadBulkRemoveButton,
|
||||
ComponentRegistry.register ThreadBulkTrashButton,
|
||||
role: 'thread:BulkAction'
|
||||
```
|
||||
|
||||
|
|
|
@ -304,29 +304,6 @@ class Actions
|
|||
###
|
||||
@destroyDraft: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Remove the currently focused {Thread}.
|
||||
|
||||
*Scope: Window*
|
||||
###
|
||||
@removeCurrentlyFocusedThread: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Removes the Thread objects currently selected in the app's main thread list.
|
||||
|
||||
*Scope: Window*
|
||||
###
|
||||
@removeSelection: ActionScopeWindow
|
||||
@removeAndNext: ActionScopeWindow
|
||||
@removeAndPrevious: ActionScopeWindow
|
||||
@toggleStarSelection: ActionScopeWindow
|
||||
@toggleStarFocused: ActionScopeWindow
|
||||
@toggleUnreadSelection: ActionScopeWindow
|
||||
@deleteSelection: ActionScopeWindow
|
||||
|
||||
@moveThread: ActionScopeWindow
|
||||
@moveThreads: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Updates the search query in the app's main search bar with the provided query text.
|
||||
|
||||
|
|
|
@ -57,6 +57,12 @@ class Account extends Model
|
|||
usesLabels: -> @organizationUnit is "label"
|
||||
usesFolders: -> @organizationUnit is "folder"
|
||||
|
||||
categoryClass: ->
|
||||
if @usesLabels()
|
||||
return require './label'
|
||||
else
|
||||
return require './folder'
|
||||
|
||||
# Public: Returns the localized, properly capitalized provider name,
|
||||
# like Gmail, Exchange, or Outlook 365
|
||||
displayProvider: ->
|
||||
|
|
|
@ -72,6 +72,9 @@ class Model
|
|||
@clientId ?= Utils.generateTempId()
|
||||
@
|
||||
|
||||
isSaved: ->
|
||||
@serverId?
|
||||
|
||||
clone: ->
|
||||
(new @constructor).fromJSON(@toJSON())
|
||||
|
||||
|
|
|
@ -154,6 +154,9 @@ class DatabaseView extends ModelView
|
|||
if items.length is 0
|
||||
return
|
||||
|
||||
@selection.updateModelReferences(items)
|
||||
@selection.removeItemsNotMatching(@_matchers)
|
||||
|
||||
if items.length > 5
|
||||
@log("invalidateAfterDatabaseChange on #{items.length} items would be expensive. Invalidating entire range.")
|
||||
@invalidateCount()
|
||||
|
@ -226,7 +229,7 @@ class DatabaseView extends ModelView
|
|||
pagesCouldHaveChanged = true
|
||||
|
||||
if didMakeOptimisticChange
|
||||
@_emitter.emit('trigger')
|
||||
@trigger()
|
||||
|
||||
if pagesCouldHaveChanged
|
||||
@invalidateCount()
|
||||
|
@ -253,8 +256,7 @@ class DatabaseView extends ModelView
|
|||
item.metadata = @_pages[page]?.metadata[item.id]
|
||||
@_pages[page]?.items[pageIdx] = item
|
||||
|
||||
@selection.updateModelReferences(items)
|
||||
@_emitter.emit('trigger')
|
||||
@trigger()
|
||||
|
||||
invalidateMetadataFor: (ids = []) ->
|
||||
# This method should be called when you know that only the metadata for
|
||||
|
@ -281,7 +283,7 @@ class DatabaseView extends ModelView
|
|||
invalidateCount: ->
|
||||
DatabaseStore.findAll(@klass).where(@_matchers).count().then (count) =>
|
||||
@_count = count
|
||||
@_emitter.emit('trigger')
|
||||
@trigger()
|
||||
|
||||
invalidateRetainedRange: ->
|
||||
@_throttler.whenReady =>
|
||||
|
@ -373,6 +375,7 @@ class DatabaseView extends ModelView
|
|||
Utils.modelFreeze(item)
|
||||
|
||||
@selection.updateModelReferences(items)
|
||||
@selection.removeItemsNotMatching(@_matchers)
|
||||
|
||||
page.items = items
|
||||
page.loading = false
|
||||
|
@ -380,7 +383,7 @@ class DatabaseView extends ModelView
|
|||
page.lastTouchTime = touchTime
|
||||
|
||||
# Trigger if this is the last page that needed to be loaded
|
||||
@_emitter.emit('trigger') if @loaded()
|
||||
@trigger() if @loaded()
|
||||
|
||||
cullPages: ->
|
||||
pagesLoaded = Object.keys(@_pages)
|
||||
|
|
|
@ -70,6 +70,12 @@ class ModelViewSelection
|
|||
@_items = without
|
||||
@trigger(@)
|
||||
|
||||
removeItemsNotMatching: (matchers) ->
|
||||
count = @_items.length
|
||||
@_items = _.filter @_items, (t) -> t.matches(matchers)
|
||||
if @_items.length isnt count
|
||||
@trigger(@)
|
||||
|
||||
expandTo: (item) ->
|
||||
return unless item
|
||||
throw new Error("expandTo must be called with a Model") unless item instanceof Model
|
||||
|
|
|
@ -11,12 +11,19 @@ class ModelView
|
|||
@_pages = {}
|
||||
@_emitter = new EventEmitter()
|
||||
|
||||
@selection = new ModelViewSelection @, => @_emitter.emit('trigger')
|
||||
@selection = new ModelViewSelection(@, @trigger)
|
||||
|
||||
@
|
||||
|
||||
# Accessing Data
|
||||
|
||||
trigger: =>
|
||||
return if @_triggering
|
||||
@_triggering = true
|
||||
_.defer =>
|
||||
@_triggering = false
|
||||
@_emitter.emit('trigger')
|
||||
|
||||
listen: (callback, bindContext) ->
|
||||
eventHandler = (args) ->
|
||||
callback.apply(bindContext, args)
|
||||
|
@ -32,7 +39,7 @@ class ModelView
|
|||
|
||||
empty: ->
|
||||
@count() <= 0
|
||||
|
||||
|
||||
get: (idx) ->
|
||||
unless _.isNumber(idx)
|
||||
throw new Error("ModelView.get() takes a numeric index. Maybe you meant getById()?")
|
||||
|
|
79
src/flux/tasks/task-factory.coffee
Normal file
79
src/flux/tasks/task-factory.coffee
Normal file
|
@ -0,0 +1,79 @@
|
|||
_ = require 'underscore'
|
||||
ChangeFolderTask = require './change-folder-task'
|
||||
ChangeLabelsTask = require './change-labels-task'
|
||||
ChangeUnreadTask = require './change-unread-task'
|
||||
ChangeStarredTask = require './change-starred-task'
|
||||
AccountStore = require '../stores/account-store'
|
||||
CategoryStore = require '../stores/category-store'
|
||||
|
||||
class TaskFactory
|
||||
|
||||
taskForApplyingCategory: ({threads, fromView, category, exclusive}) ->
|
||||
account = AccountStore.current()
|
||||
if account.usesFolders()
|
||||
return null unless category
|
||||
return new ChangeFolderTask
|
||||
folder: category
|
||||
threads: threads
|
||||
else
|
||||
labelsToRemove = []
|
||||
if exclusive
|
||||
currentLabel = CategoryStore.byId(fromView?.categoryId())
|
||||
currentLabel ?= CategoryStore.getStandardCategory("inbox")
|
||||
labelsToRemove = [currentLabel]
|
||||
|
||||
return new ChangeLabelsTask
|
||||
threads: threads
|
||||
labelsToRemove: labelsToRemove
|
||||
labelsToAdd: [category]
|
||||
|
||||
taskForRemovingCategory: ({threads, fromView, category, exclusive}) ->
|
||||
account = AccountStore.current()
|
||||
if account.usesFolders()
|
||||
return new ChangeFolderTask
|
||||
folder: CategoryStore.getStandardCategory("inbox")
|
||||
threads: threads
|
||||
else
|
||||
labelsToAdd = []
|
||||
if exclusive
|
||||
currentLabel = CategoryStore.byId(fromView?.categoryId())
|
||||
currentLabel ?= CategoryStore.getStandardCategory("inbox")
|
||||
labelsToAdd = [currentLabel]
|
||||
|
||||
return new ChangeLabelsTask
|
||||
threads: threads
|
||||
labelsToRemove: [category]
|
||||
labelsToAdd: labelsToAdd
|
||||
|
||||
taskForArchiving: ({threads, fromView}) ->
|
||||
category = @_archiveCategory()
|
||||
@taskForApplyingCategory({threads, fromView, category, exclusive: true})
|
||||
|
||||
taskForUnarchiving: ({threads, fromView}) ->
|
||||
category = @_archiveCategory()
|
||||
@taskForRemovingCategory({threads, fromView, category, exclusive: true})
|
||||
|
||||
taskForMovingToTrash: ({threads, fromView}) ->
|
||||
category = CategoryStore.getStandardCategory("trash")
|
||||
@taskForApplyingCategory({threads, fromView, category, exclusive: true})
|
||||
|
||||
taskForMovingFromTrash: ({threads, fromView}) ->
|
||||
category = CategoryStore.getStandardCategory("trash")
|
||||
@taskForRemovingCategory({threads, fromView, category, exclusive: true})
|
||||
|
||||
taskForInvertingUnread: ({threads}) ->
|
||||
unread = _.every threads, (t) -> _.isMatch(t, {unread: false})
|
||||
return new ChangeUnreadTask({threads, unread})
|
||||
|
||||
taskForInvertingStarred: ({threads}) ->
|
||||
starred = _.every threads, (t) -> _.isMatch(t, {starred: false})
|
||||
return new ChangeStarredTask({threads, starred})
|
||||
|
||||
_archiveCategory: (account) ->
|
||||
account = AccountStore.current()
|
||||
if account.usesFolders()
|
||||
return CategoryStore.getStandardCategory("archive")
|
||||
else
|
||||
return CategoryStore.getStandardCategory("all")
|
||||
|
||||
module.exports = new TaskFactory
|
|
@ -72,7 +72,8 @@ class NylasExports
|
|||
@require "Task", 'flux/tasks/task'
|
||||
@require "TaskRegistry", "task-registry"
|
||||
@require "TaskQueue", 'flux/stores/task-queue'
|
||||
@load "TaskQueueStatusStore", 'flux/stores/task-queue-status-store'
|
||||
@require "TaskFactory", 'flux/tasks/task-factory'
|
||||
@load "TaskQueueStatusStore", 'flux/stores/task-queue-status-store'
|
||||
@require "UndoRedoStore", 'flux/stores/undo-redo-store'
|
||||
|
||||
# Tasks
|
||||
|
@ -134,7 +135,6 @@ class NylasExports
|
|||
@load "SoundRegistry", 'sound-registry'
|
||||
@load "QuotedHTMLParser", 'services/quoted-html-parser'
|
||||
@load "QuotedPlainTextParser", 'services/quoted-plain-text-parser'
|
||||
@require "RemoveThreadHelper", 'services/remove-thread-helper'
|
||||
|
||||
# Errors
|
||||
@get "APIError", -> require('../flux/errors').APIError
|
||||
|
|
|
@ -49,8 +49,11 @@ class MailViewFilter
|
|||
throw new Error("canApplyToThreads: Not implemented in base class.")
|
||||
|
||||
# Whether or not the current MailViewFilter can "archive" or "trash"
|
||||
canRemoveThreads: ->
|
||||
throw new Error("canRemoveThreads: Not implemented in base class.")
|
||||
canArchiveThreads: ->
|
||||
throw new Error("canArchiveThreads: Not implemented in base class.")
|
||||
|
||||
canTrashThreads: ->
|
||||
throw new Error("canTrashThreads: Not implemented in base class.")
|
||||
|
||||
applyToThreads: (threadsOrIds) ->
|
||||
throw new Error("applyToThreads: Not implemented in base class.")
|
||||
|
@ -68,7 +71,10 @@ class SearchMailViewFilter extends MailViewFilter
|
|||
canApplyToThreads: ->
|
||||
false
|
||||
|
||||
canRemoveThreads: ->
|
||||
canArchiveThreads: ->
|
||||
false
|
||||
|
||||
canTrashThreads: ->
|
||||
false
|
||||
|
||||
categoryId: ->
|
||||
|
@ -90,7 +96,10 @@ class StarredMailViewFilter extends MailViewFilter
|
|||
canApplyToThreads: ->
|
||||
true
|
||||
|
||||
canRemoveThreads: ->
|
||||
canArchiveThreads: ->
|
||||
true
|
||||
|
||||
canTrashThreads: ->
|
||||
true
|
||||
|
||||
applyToThreads: (threadsOrIds) ->
|
||||
|
@ -128,9 +137,14 @@ class CategoryMailViewFilter extends MailViewFilter
|
|||
canApplyToThreads: ->
|
||||
not (@category.name in CategoryStore.LockedCategoryNames)
|
||||
|
||||
canRemoveThreads: ->
|
||||
return false if @category.name in ["archive", "trash", "sent", "all"]
|
||||
canArchiveThreads: ->
|
||||
return false if @category.name in ["archive", "all", "sent"]
|
||||
return false if @category.displayName is atom.config.get("core.archiveFolder")
|
||||
return false unless CategoryStore.getStandardCategory("archive")
|
||||
return true
|
||||
|
||||
canTrashThreads: ->
|
||||
return false if @category.name in ["trash"]
|
||||
return true
|
||||
|
||||
applyToThreads: (threadsOrIds) ->
|
||||
|
@ -153,5 +167,4 @@ class CategoryMailViewFilter extends MailViewFilter
|
|||
|
||||
Actions.queueTask(task)
|
||||
|
||||
|
||||
module.exports = MailViewFilter
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
CategoryStore = require '../flux/stores/category-store'
|
||||
|
||||
ChangeLabelsTask = require '../flux/tasks/change-labels-task'
|
||||
ChangeFolderTask = require '../flux/tasks/change-folder-task'
|
||||
Actions = require '../flux/actions'
|
||||
|
||||
AccountStore = require '../flux/stores/account-store'
|
||||
|
||||
class RemoveThreadHelper
|
||||
|
||||
Type:
|
||||
Trash: "trash"
|
||||
Archive: "archive"
|
||||
|
||||
removeType: ->
|
||||
currentAccount = @_currentAccount()
|
||||
return null unless currentAccount
|
||||
savedType = atom.config.get("core.#{currentAccount.id}.removeType")
|
||||
return savedType if savedType
|
||||
|
||||
archiveCategory = CategoryStore.getStandardCategory("archive")
|
||||
return @Type.Archive if archiveCategory
|
||||
|
||||
if currentAccount.provider is "gmail"
|
||||
return @Type.Archive
|
||||
else
|
||||
return @Type.Trash
|
||||
|
||||
_currentAccount: -> AccountStore.current() # To stub in testing
|
||||
|
||||
# In the case of folders, "removing" means moving the message to a
|
||||
# particular folder
|
||||
removalFolder: ->
|
||||
if @removeType() is @Type.Trash
|
||||
CategoryStore.getStandardCategory("trash")
|
||||
else if @removeType() is @Type.Archive
|
||||
CategoryStore.getStandardCategory("archive")
|
||||
|
||||
# In the case of labels, "removing" means removing the current label and
|
||||
# applying a new label indicating it's in the "removed" state.
|
||||
removalLabelToAdd: ->
|
||||
if @removeType() is @Type.Trash
|
||||
CategoryStore.getStandardCategory("trash")
|
||||
else if @removeType() is @Type.Archive
|
||||
CategoryStore.getStandardCategory("all")
|
||||
|
||||
getRemovalTask: (threads=[], focusedMailViewFilter) ->
|
||||
threads = [threads] unless threads instanceof Array
|
||||
account = @_currentAccount()
|
||||
return null unless account
|
||||
|
||||
if account.usesFolders()
|
||||
removalFolder = @removalFolder()
|
||||
if removalFolder
|
||||
return new ChangeFolderTask
|
||||
folder: removalFolder
|
||||
threads: threads
|
||||
else
|
||||
@_notifyFolderRemovalError()
|
||||
return null
|
||||
|
||||
else if account.usesLabels()
|
||||
viewCategoryId = focusedMailViewFilter.categoryId()
|
||||
currentLabel = CategoryStore.byId(viewCategoryId)
|
||||
currentLabel ?= CategoryStore.getStandardCategory("inbox")
|
||||
|
||||
params = {threads}
|
||||
params.labelsToRemove = [currentLabel]
|
||||
|
||||
removalLabelToAdd = @removalLabelToAdd()
|
||||
if removalLabelToAdd
|
||||
params.labelsToAdd = [removalLabelToAdd]
|
||||
|
||||
return new ChangeLabelsTask(params)
|
||||
else
|
||||
throw new Error("Invalid organizationUnit")
|
||||
|
||||
_notifyFolderRemovalError: ->
|
||||
# In the onboarding flow, users should have already created their
|
||||
# Removal folder. This should only happen for legacy users or if
|
||||
# there's an error somewhere.
|
||||
if @removeType() is @Type.Trash
|
||||
msg = "There is no Trash folder. Please create a folder called 'Trash' and try again."
|
||||
else if @removeType() is @Type.Archive
|
||||
msg = "We can't archive your messages because you have no 'Archive' folder. Please create a folder called 'Archive' and try again"
|
||||
Actions.postNotification
|
||||
type: 'error'
|
||||
tag: 'noRemovalFolder'
|
||||
sticky: true
|
||||
message: msg
|
||||
|
||||
module.exports = new RemoveThreadHelper()
|
Loading…
Reference in a new issue