From 8f7f63af3a69f533e072afd586eddd66569185bf Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Thu, 21 Jan 2016 13:46:04 -0800 Subject: [PATCH] TaskFactory now returns N tasks for performing standard actions, undo uses arrays --- .../lib/thread-archive-button.cjsx | 4 +- .../message-list/lib/thread-trash-button.cjsx | 4 +- .../thread-list/lib/thread-buttons.cjsx | 8 +- .../lib/thread-list-quick-actions.cjsx | 8 +- .../thread-list/lib/thread-list.cjsx | 18 ++-- .../undo-redo/lib/undo-redo-component.cjsx | 10 +-- src/flux/actions.coffee | 8 ++ src/flux/models/query-subscription.coffee | 33 +++++-- src/flux/models/query.coffee | 2 +- src/flux/stores/task-queue.coffee | 16 ++-- src/flux/stores/undo-redo-store.coffee | 41 +++++---- src/flux/tasks/task-factory.coffee | 88 ++++++++++--------- 12 files changed, 137 insertions(+), 103 deletions(-) diff --git a/internal_packages/message-list/lib/thread-archive-button.cjsx b/internal_packages/message-list/lib/thread-archive-button.cjsx index 39f1157c4..ecfc6efd9 100644 --- a/internal_packages/message-list/lib/thread-archive-button.cjsx +++ b/internal_packages/message-list/lib/thread-archive-button.cjsx @@ -24,10 +24,10 @@ class ThreadArchiveButton extends React.Component _onArchive: (e) => return unless DOMUtils.nodeIsVisible(e.currentTarget) - task = TaskFactory.taskForArchiving + tasks = TaskFactory.tasksForArchiving threads: [@props.thread], fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) Actions.popSheet() e.stopPropagation() diff --git a/internal_packages/message-list/lib/thread-trash-button.cjsx b/internal_packages/message-list/lib/thread-trash-button.cjsx index 07f4abc06..ca575a9de 100644 --- a/internal_packages/message-list/lib/thread-trash-button.cjsx +++ b/internal_packages/message-list/lib/thread-trash-button.cjsx @@ -26,10 +26,10 @@ class ThreadTrashButton extends React.Component _onRemove: (e) => return unless DOMUtils.nodeIsVisible(e.currentTarget) - task = TaskFactory.taskForMovingToTrash + tasks = TaskFactory.tasksForMovingToTrash threads: [@props.thread], fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) Actions.popSheet() e.stopPropagation() diff --git a/internal_packages/thread-list/lib/thread-buttons.cjsx b/internal_packages/thread-list/lib/thread-buttons.cjsx index 8532e7dfc..fc115a955 100644 --- a/internal_packages/thread-list/lib/thread-buttons.cjsx +++ b/internal_packages/thread-list/lib/thread-buttons.cjsx @@ -28,10 +28,10 @@ class ThreadBulkArchiveButton extends React.Component _onArchive: => - task = TaskFactory.taskForArchiving + tasks = TaskFactory.tasksForArchiving threads: @props.selection.items(), fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) class ThreadBulkTrashButton extends React.Component @displayName: 'ThreadBulkTrashButton' @@ -52,10 +52,10 @@ class ThreadBulkTrashButton extends React.Component _onRemove: => - task = TaskFactory.taskForMovingToTrash + tasks = TaskFactory.tasksForMovingToTrash threads: @props.selection.items(), fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) class ThreadBulkStarButton extends React.Component diff --git a/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx b/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx index f16b82004..4a0045756 100644 --- a/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx +++ b/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx @@ -25,10 +25,10 @@ class ThreadArchiveQuickAction extends React.Component newProps.thread.id isnt @props?.thread.id _onArchive: (event) => - task = TaskFactory.taskForArchiving + tasks = TaskFactory.tasksForArchiving threads: [@props.thread] fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) # Don't trigger the thread row click event.stopPropagation() @@ -54,10 +54,10 @@ class ThreadTrashQuickAction extends React.Component newProps.thread.id isnt @props?.thread.id _onRemove: (event) => - task = TaskFactory.taskForMovingToTrash + tasks = TaskFactory.tasksForMovingToTrash threads: [@props.thread] fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) # Don't trigger the thread row click event.stopPropagation() diff --git a/internal_packages/thread-list/lib/thread-list.cjsx b/internal_packages/thread-list/lib/thread-list.cjsx index 6dd6285d2..c49ddf1f7 100644 --- a/internal_packages/thread-list/lib/thread-list.cjsx +++ b/internal_packages/thread-list/lib/thread-list.cjsx @@ -176,19 +176,19 @@ class ThreadList extends React.Component if threads if backspaceDelete if FocusedPerspectiveStore.current().canTrashThreads() - removeMethod = TaskFactory.taskForMovingToTrash + removeMethod = TaskFactory.tasksForMovingToTrash else return else if FocusedPerspectiveStore.current().canArchiveThreads() - removeMethod = TaskFactory.taskForArchiving + removeMethod = TaskFactory.tasksForArchiving else - removeMethod = TaskFactory.taskForMovingToTrash + removeMethod = TaskFactory.tasksForMovingToTrash - task = removeMethod + tasks = removeMethod threads: threads fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) Actions.popSheet() @@ -196,20 +196,20 @@ class ThreadList extends React.Component return unless FocusedPerspectiveStore.current().canArchiveThreads() threads = @_threadsForKeyboardAction() if threads - task = TaskFactory.taskForArchiving + tasks = TaskFactory.tasksForArchiving threads: threads fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) Actions.popSheet() _onDeleteItem: => return unless FocusedPerspectiveStore.current().canTrashThreads() threads = @_threadsForKeyboardAction() if threads - task = TaskFactory.taskForMovingToTrash + tasks = TaskFactory.tasksForMovingToTrash threads: threads fromPerspective: FocusedPerspectiveStore.current() - Actions.queueTask(task) + Actions.queueTasks(tasks) Actions.popSheet() _onSelectRead: => diff --git a/internal_packages/undo-redo/lib/undo-redo-component.cjsx b/internal_packages/undo-redo/lib/undo-redo-component.cjsx index 623f5a8a9..e5cb773e2 100644 --- a/internal_packages/undo-redo/lib/undo-redo-component.cjsx +++ b/internal_packages/undo-redo/lib/undo-redo-component.cjsx @@ -33,12 +33,12 @@ class UndoRedoComponent extends React.Component ), 3000 _getStateFromStores: -> - task = UndoRedoStore.getMostRecentTask() + tasks = UndoRedoStore.getMostRecent() show = false - if task + if tasks show = true - return {show, task} + return {show, tasks} componentWillMount: -> @_unsubscribe = UndoRedoStore.listen(@_onChange) @@ -65,7 +65,7 @@ class UndoRedoComponent extends React.Component if @state.show
- {@state.task.description()} + {@state.tasks.map((t) -> t.description()).join(', ')}
- @setState({show: false, task: null}) + @setState({show: false, tasks: null}) module.exports = UndoRedoComponent diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 4f0c48a5b..e0d6951fb 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -90,6 +90,14 @@ class Actions ### @queueTask: ActionScopeWorkWindow + ### + Public: Queue multiple {Task} objects to the {TaskQueue}, which should be + undone as a single user action. + + *Scope: Work Window* + ### + @queueTasks: ActionScopeWorkWindow + @undoTaskId: ActionScopeWorkWindow ### diff --git a/src/flux/models/query-subscription.coffee b/src/flux/models/query-subscription.coffee index 446a359c0..921e3e5de 100644 --- a/src/flux/models/query-subscription.coffee +++ b/src/flux/models/query-subscription.coffee @@ -88,7 +88,9 @@ class QuerySubscription mustRefetchAllIds = true if @_itemSortOrderHasChanged(oldItem, item) if impactCount > 0 - @_set = null if mustRefetchAllIds + if mustRefetchAllIds + @log("Clearing result set - mustRefetchAllIds") + @_set = null @update() _itemSortOrderHasChanged: (old, updated) -> @@ -102,6 +104,9 @@ class QuerySubscription return false + log: (msg) => + console.log(msg) if @_query._klass.name is 'Thread' + update: => version = @_version += 1 @@ -116,18 +121,23 @@ class QuerySubscription entireModels = not @_set or @_set.modelCacheCount() is 0 Promise.each ranges, (range) => - return unless version is @_version - @_fetchRange(range, {entireModels}) + return @log("Update (#{version}) - Cancelled @ Step 0") unless version is @_version + @log("Update (#{version}) - Fetching range #{range}") + @_fetchRange(range, {entireModels, version}) .then => - return unless version is @_version + return @log("Update (#{version}) - Cancelled @ Step 1") unless version is @_version ids = @_set.ids().filter (id) => not @_set.modelWithId(id) - return if ids.length is 0 - return DatabaseStore.findAll(@_query._klass, {id: ids}).then(@_appendToModelCache) + return @log("Update (#{version}) - No missing Ids") if ids.length is 0 + @log("Update (#{version}) - Fetching missing Ids: #{ids}") + return DatabaseStore.findAll(@_query._klass, {id: ids}).then (models) => + @log("Update (#{version}) - Fetched missing Ids") + @_set.replaceModel(m) for m in models .then => - return unless version is @_version + return @log("Update (#{version}) - Cancelled @ Step 2") unless version is @_version + @log("Update (#{version}) - Triggering...") @_createResultAndTrigger() - _fetchRange: (range, {entireModels} = {}) -> + _fetchRange: (range, {entireModels, version} = {}) -> rangeQuery = undefined unless range.isInfinite() @@ -141,7 +151,12 @@ class QuerySubscription rangeQuery ?= @_query DatabaseStore.run(rangeQuery, {format: false}).then (results) => - @_set = null unless @_set?.range().isContiguousWith(range) + if version and version isnt @_version + return @log("Update (#{version}) - fetchRange Cancelled") + + unless @_set?.range().isContiguousWith(range) + @log("Clearing result set - #{range} isnt contiguous with #{@_set?.range()}") + @_set = null @_set ?= new MutableQueryResultSet() if entireModels diff --git a/src/flux/models/query.coffee b/src/flux/models/query.coffee index 9d304c5a4..4fa9c9b3f 100644 --- a/src/flux/models/query.coffee +++ b/src/flux/models/query.coffee @@ -233,7 +233,7 @@ class ModelQuery formatResultObjects: (objects) -> return objects[0] if @_returnOne - return objects + return [].concat(objects) # Query SQL Building diff --git a/src/flux/stores/task-queue.coffee b/src/flux/stores/task-queue.coffee index 77836c717..b581ac90a 100644 --- a/src/flux/stores/task-queue.coffee +++ b/src/flux/stores/task-queue.coffee @@ -76,14 +76,14 @@ class TaskQueue @_restoreQueue() - @listenTo(Actions.queueTask, @enqueue) - @listenTo(Actions.undoTaskId, @enqueueUndoOfTaskId) - @listenTo(Actions.dequeueTask, @dequeue) - @listenTo(Actions.dequeueAllTasks, @dequeueAll) - @listenTo(Actions.dequeueMatchingTask, @dequeueMatching) - - @listenTo(Actions.clearDeveloperConsole, @clearCompleted) - + @listenTo Actions.queueTask, @enqueue + @listenTo Actions.queueTasks, (tasks) => + @enqueue(t) for t in tasks + @listenTo Actions.undoTaskId, @enqueueUndoOfTaskId + @listenTo Actions.dequeueTask, @dequeue + @listenTo Actions.dequeueAllTasks, @dequeueAll + @listenTo Actions.dequeueMatchingTask, @dequeueMatching + @listenTo Actions.clearDeveloperConsole, @clearCompleted @listenTo Actions.longPollConnected, => @_processQueue() diff --git a/src/flux/stores/undo-redo-store.coffee b/src/flux/stores/undo-redo-store.coffee index ee3af5e78..a236aa94a 100644 --- a/src/flux/stores/undo-redo-store.coffee +++ b/src/flux/stores/undo-redo-store.coffee @@ -16,32 +16,41 @@ class UndoRedoStore @_undo = [] @_redo = [] - @listenTo(Actions.queueTask, @_onTaskQueued) + @listenTo(Actions.queueTask, @_onQueue) + @listenTo(Actions.queueTasks, @_onQueue) - NylasEnv.commands.add('body', {'core:undo': => @undo() }) - NylasEnv.commands.add('body', {'core:redo': => @redo() }) + NylasEnv.commands.add('body', {'core:undo': @undo }) + NylasEnv.commands.add('body', {'core:redo': @redo }) - _onTaskQueued: (task) => - if task.canBeUndone() + _onQueue: (tasks) => + tasks = [tasks] unless tasks instanceof Array + undoable = _.every tasks, (t) -> t.canBeUndone() + + if undoable @_redo = [] - @_undo.push(task) - @trigger() unless task._isReverting + @_undo.push(tasks) + @trigger() undo: => - topTask = @_undo.pop() - return unless topTask + topTasks = @_undo.pop() + return unless topTasks @trigger() - Actions.undoTaskId(topTask.id) - @_redo.push(topTask.createIdenticalTask()) + + for task in topTasks + Actions.undoTaskId(task.id) + + redoTasks = topTasks.map (t) -> t.createIdenticalTask() + @_redo.push(redoTasks) redo: => - redoTask = @_redo.pop() - return unless redoTask - Actions.queueTask(redoTask) + redoTasks = @_redo.pop() + return unless redoTasks + Actions.queueTasks(redoTasks) - getMostRecentTask: => + getMostRecent: => for idx in [@_undo.length-1...-1] - return @_undo[idx] unless @_undo[idx]._isReverting + allReverting = _.every @_undo[idx], (t) -> t._isReverting + return @_undo[idx] unless allReverting print: -> console.log("Undo Stack") diff --git a/src/flux/tasks/task-factory.coffee b/src/flux/tasks/task-factory.coffee index 227ece953..b69a2a8ea 100644 --- a/src/flux/tasks/task-factory.coffee +++ b/src/flux/tasks/task-factory.coffee @@ -8,27 +8,43 @@ CategoryStore = require '../stores/category-store' class TaskFactory - taskForApplyingCategory: ({threads, fromPerspective, category, exclusive}) => - # TODO Can not apply to threads across more than one account for now - account = AccountStore.accountForItems(threads) - return unless account? + tasksForApplyingCategories: ({threads, categoriesToRemove, categoryToAdd}) => + byAccount = {} + tasks = [] - if account.usesFolders() - return null unless category - return new ChangeFolderTask - folder: category - threads: threads - else - labelsToRemove = [] - if exclusive - currentLabel = fromPerspective?.category() - currentLabel ?= CategoryStore.getStandardCategory(account, "inbox") - labelsToRemove = [currentLabel] + for thread in threads + accountId = thread.accountId + byAccount[accountId] ?= + categoriesToRemove: categoriesToRemove?(accountId) ? [] + categoryToAdd: categoryToAdd(accountId) + threads: [] + byAccount[accountId].threads.push(thread) - return new ChangeLabelsTask - threads: threads - labelsToRemove: labelsToRemove - labelsToAdd: [category] + for accountId, {categoryToAdd, categoriesToRemove, threads} of byAccount + continue unless categoryToAdd and categoriesToRemove + + account = AccountStore.accountForId(accountId) + if account.usesFolders() + tasks.push new ChangeFolderTask + folder: categoryToAdd + threads: threads + else + tasks.push new ChangeLabelsTask + threads: threads + labelsToRemove: categoriesToRemove + labelsToAdd: [categoryToAdd] + + return tasks + + taskForApplyingCategory: ({threads, category}) => + tasks = @tasksForApplyingCategories + threads: threads + categoryToAdd: (accountId) -> category + + if tasks.length > 1 + throw new Error("taskForApplyingCategory: Threads must be from the same account.") + + return tasks[0] taskForRemovingCategory: ({threads, fromPerspective, category, exclusive}) => # TODO Can not apply to threads across more than one account for now @@ -51,21 +67,17 @@ class TaskFactory labelsToRemove: [category] labelsToAdd: labelsToAdd - taskForArchiving: ({threads, fromPerspective}) => - category = @_getArchiveCategory(threads) - @taskForApplyingCategory({threads, fromPerspective, category, exclusive: true}) + tasksForArchiving: ({threads, fromPerspective}) => + @tasksForApplyingCategories + threads: threads, + categoriesToRemove: (accountId) -> _.filter(fromPerspective.categories(), _.matcher({accountId})) + categoryToAdd: (accountId) -> CategoryStore.getArchiveCategory(accountId) - taskForUnarchiving: ({threads, fromPerspective}) => - category = @_getArchiveCategory(threads) - @taskForRemovingCategory({threads, fromPerspective, category, exclusive: true}) - - taskForMovingToTrash: ({threads, fromPerspective}) => - category = @_getTrashCategory(threads) - @taskForApplyingCategory({threads, fromPerspective, category, exclusive: true}) - - taskForMovingFromTrash: ({threads, fromPerspective}) => - category = @_getTrashCategory(threads) - @taskForRemovingCategory({threads, fromPerspective, category, exclusive: true}) + tasksForMovingToTrash: ({threads, fromPerspective}) => + @tasksForApplyingCategories + threads: threads, + categoriesToRemove: (accountId) -> _.filter(fromPerspective.categories(), _.matcher({accountId})) + categoryToAdd: (accountId) -> CategoryStore.getTrashCategory(accountId) taskForInvertingUnread: ({threads}) => unread = _.every threads, (t) -> _.isMatch(t, {unread: false}) @@ -75,14 +87,4 @@ class TaskFactory starred = _.every threads, (t) -> _.isMatch(t, {starred: false}) return new ChangeStarredTask({threads, starred}) - _getArchiveCategory: (threads) => - account = AccountStore.accountForItems(threads) - return unless account? - CategoryStore.getArchiveCategory(account) - - _getTrashCategory: (threads) => - account = AccountStore.accountForItems(threads) - return unless account? - CategoryStore.getTrashCategory(account) - module.exports = new TaskFactory