feat(archive): archive now pops back to thread list

Summary:
add spec for message toolbar items

add thread list spec

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1344
This commit is contained in:
Evan Morikawa 2015-03-25 14:17:57 -04:00
parent 79ad424849
commit dc5941f3f1
14 changed files with 194 additions and 46 deletions

View file

@ -30,9 +30,19 @@ AccountSidebarStore = Reflux.createStore
_registerListeners: ->
@listenTo Actions.selectTagId, @_onSelectTagId
@listenTo Actions.searchQueryCommitted, @_onSearchQueryCommitted
@listenTo DatabaseStore, @_onDataChanged
@listenTo NamespaceStore, @_onNamespaceChanged
_onSearchQueryCommitted: (query) ->
if query? and query isnt ""
@_oldSelectedId = @_selectedId
@_selectedId = "search"
else
@_selectedId = @_oldSelectedId if @_oldSelectedId
@trigger(@)
_populate: ->
namespace = NamespaceStore.current()
return unless namespace
@ -116,6 +126,7 @@ AccountSidebarStore = Reflux.createStore
@_populateDraftsCount()
_onSelectTagId: (tagId) ->
Actions.searchQueryCommitted('') if @_selectedId is "search"
@_selectedId = tagId
@trigger(@)

View file

@ -1,5 +1,5 @@
React = require 'react'
{Actions, ThreadStore, Utils} = require 'inbox-exports'
{Actions, ThreadStore, Utils, WorkspaceStore} = require 'inbox-exports'
{RetinaImg} = require 'ui-components'
# Note: These always have a thread, but only sometimes get a
@ -46,7 +46,7 @@ ForwardButton = React.createClass
ArchiveButton = React.createClass
render: ->
<button className="btn btn-toolbar"
<button className="btn btn-toolbar btn-archive"
data-tooltip="Archive"
onClick={@_onArchive}>
<RetinaImg name="toolbar-archive.png" />
@ -54,13 +54,15 @@ ArchiveButton = React.createClass
_onArchive: (e) ->
return unless Utils.nodeIsVisible(e.currentTarget)
# Calling archive() sends an Actions.queueTask with an archive task
# TODO Turn into an Action
ThreadStore.selectedThread().archive()
if WorkspaceStore.selectedLayoutMode() is "list"
Actions.archiveCurrentThread()
else if WorkspaceStore.selectedLayoutMode() is "split"
Actions.archiveAndNext()
e.stopPropagation()
module.exports = React.createClass
module.exports =
MessageToolbarItems = React.createClass
getInitialState: ->
threadIsSelected: ThreadStore.selectedId()?
@ -70,7 +72,7 @@ module.exports = React.createClass
"hidden": !@state.threadIsSelected
<div className={classes}>
<ArchiveButton />
<ArchiveButton ref="archiveButton" />
</div>
componentDidMount: ->

View file

@ -0,0 +1,27 @@
React = require 'react/addons'
ReactTestUtils = React.addons.TestUtils
MessageToolbarItems = require "../lib/message-toolbar-items.cjsx"
{WorkspaceStore, Actions} = require 'inbox-exports'
describe "MessageToolbarItems", ->
beforeEach ->
@toolbarItems = ReactTestUtils.renderIntoDocument(<MessageToolbarItems />)
@archiveButton = @toolbarItems.refs["archiveButton"]
spyOn(Actions, "archiveAndNext")
spyOn(Actions, "archiveCurrentThread")
it "renders the archive button", ->
btns = ReactTestUtils.scryRenderedDOMComponentsWithClass(@toolbarItems, "btn-archive")
expect(btns.length).toBe 1
it "archives and next in split mode", ->
spyOn(WorkspaceStore, "selectedLayoutMode").andReturn "split"
ReactTestUtils.Simulate.click(@archiveButton.getDOMNode())
expect(Actions.archiveCurrentThread).not.toHaveBeenCalled()
expect(Actions.archiveAndNext).toHaveBeenCalled()
it "archives in list mode", ->
spyOn(WorkspaceStore, "selectedLayoutMode").andReturn "list"
ReactTestUtils.Simulate.click(@archiveButton.getDOMNode())
expect(Actions.archiveCurrentThread).toHaveBeenCalled()
expect(Actions.archiveAndNext).not.toHaveBeenCalled()

View file

@ -307,7 +307,7 @@
///////////////////////////////
.sidebar-thread-participants {
padding: @spacing-standard;
order: 10;
order: 2;
flex-shrink: 0;
.other-contact {

View file

@ -19,6 +19,7 @@
border:none;
input {
padding-top: 3.5px;
padding-left:30px;
width: 100%;
height: 30px;
@ -66,6 +67,10 @@
&.showing-suggestions {
.suggestions { display: inherit; }
.clear {
color: @input-accessory-color;
display: inherit;
}
}
&.showing-query {
.clear { display: inherit; }
@ -82,8 +87,8 @@
&.clear {
position: absolute;
top: floor(40px - 26px)/2;
color: @input-accessory-color;
top: floor(40px - 26px)/2 - 1px;
color: @input-cancel-color;
right: @padding-base-horizontal;
display: none;
}

View file

@ -1,7 +1,7 @@
.internal-sidebar {
padding: @spacing-standard;
padding-bottom: 0;
order: 2;
order: 4;
flex-shrink: 0;
a{text-decoration: none}

View file

@ -1,8 +1,11 @@
_ = require 'underscore-plus'
React = require 'react'
{ListTabular} = require 'ui-components'
{ListTabular, Spinner} = require 'ui-components'
{timestamp, subject} = require './formatting-utils'
{Actions, ThreadStore, ComponentRegistry} = require 'inbox-exports'
{Actions,
ThreadStore,
WorkspaceStore,
ComponentRegistry} = require 'inbox-exports'
module.exports =
ThreadList = React.createClass
@ -23,8 +26,9 @@ ThreadList = React.createClass
'application:previous-item': => @_onShiftSelectedIndex(-1)
'application:next-item': => @_onShiftSelectedIndex(1)
'application:focus-item': => @_onFocusSelectedIndex()
'application:remove-item': @_onArchiveSelected
'application:remove-and-previous': @_onArchiveAndPrevious
'application:remove-item': @_onArchiveCurrentThread
'application:remove-and-previous': -> Actions.archiveAndPrevious()
'application:remove-and-next': -> Actions.archiveAndNext()
'application:reply': @_onReply
'application:reply-all': @_onReplyAll
'application:forward': @_onForward
@ -36,13 +40,15 @@ ThreadList = React.createClass
@body_unsubscriber.dispose()
render: ->
<div className="thread-list">
classes = React.addons.classSet("thread-list": true, "ready": @state.ready)
<div className={classes}>
<ListTabular
columns={@state.columns}
items={@state.items}
itemClassProvider={ (item) -> if item.isUnread() then 'unread' else '' }
selectedId={@state.selectedId}
onSelect={ (item) -> Actions.selectThreadId(item.id) } />
<Spinner visible={!@state.ready} />
</div>
_computeColumns: ->
@ -106,34 +112,38 @@ ThreadList = React.createClass
index = Math.max(0, Math.min(index + delta, @state.items.length-1))
Actions.selectThreadId(@state.items[index].id)
_onArchiveSelected: ->
thread = ThreadStore.selectedThread()
thread.archive() if thread
_onStarThread: ->
thread = ThreadStore.selectedThread()
thread.toggleStar() if thread
_onReply: ->
return unless @state.selectedId?
return unless @state.selectedId? and @_actionInVisualScope()
Actions.composeReply(threadId: @state.selectedId)
_onReplyAll: ->
return unless @state.selectedId?
return unless @state.selectedId? and @_actionInVisualScope()
Actions.composeReplyAll(threadId: @state.selectedId)
_onForward: ->
return unless @state.selectedId?
return unless @state.selectedId? and @_actionInVisualScope()
Actions.composeForward(threadId: @state.selectedId)
_actionInVisualScope: ->
if WorkspaceStore.selectedLayoutMode() is "list"
WorkspaceStore.sheet().type is "Thread"
else true
_onArchiveCurrentThread: ->
if WorkspaceStore.selectedLayoutMode() is "list"
Actions.archiveCurrentThread()
else if WorkspaceStore.selectedLayoutMode() is "split"
Actions.archiveAndNext()
_onChange: ->
@setState(@_getStateFromStores())
_onArchiveAndPrevious: ->
@_onArchiveSelected()
@_onShiftSelectedIndex(-1)
_getStateFromStores: ->
ready: not ThreadStore.itemsLoading()
items: ThreadStore.items()
columns: @_computeColumns()
selectedId: ThreadStore.selectedId()

View file

@ -10,6 +10,7 @@ ReactTestUtils = _.extend ReactTestUtils, require "jasmine-react-helpers"
Namespace,
ThreadStore,
DatabaseStore,
WorkspaceStore,
InboxTestUtils,
NamespaceStore,
ComponentRegistry} = require "inbox-exports"
@ -212,6 +213,9 @@ describe "ThreadList", ->
spyOn(ThreadStore, "_onNamespaceChanged")
spyOn(DatabaseStore, "findAll").andCallFake ->
new Promise (resolve, reject) -> resolve(test_threads())
spyOn(Actions, "archiveCurrentThread")
spyOn(Actions, "archiveAndNext")
spyOn(Actions, "archiveAndPrevious")
ReactTestUtils.spyOnClass(ThreadList, "_computeColumns").andReturn(columns)
ThreadStore._resetInstanceVars()
@ -240,6 +244,47 @@ describe "ThreadList", ->
items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, ListTabular.Item)
expect(items.length).toBe 0
describe "when the workspace is in list mode", ->
beforeEach ->
spyOn(WorkspaceStore, "selectedLayoutMode").andReturn "list"
@thread_list.setState selectedId: "t111"
it "archives in list mode", ->
@thread_list._onArchiveCurrentThread()
expect(Actions.archiveCurrentThread).toHaveBeenCalled()
expect(Actions.archiveAndNext).not.toHaveBeenCalled()
it "allows reply only when the sheet type is 'Thread'", ->
spyOn(WorkspaceStore, "sheet").andCallFake -> {type: "Thread"}
spyOn(Actions, "composeReply")
@thread_list._onReply()
expect(Actions.composeReply).toHaveBeenCalled()
expect(@thread_list._actionInVisualScope()).toBe true
it "doesn't reply only when the sheet type isnt 'Thread'", ->
spyOn(WorkspaceStore, "sheet").andCallFake -> {type: "Root"}
spyOn(Actions, "composeReply")
@thread_list._onReply()
expect(Actions.composeReply).not.toHaveBeenCalled()
expect(@thread_list._actionInVisualScope()).toBe false
describe "when the workspace is in split mode", ->
beforeEach ->
spyOn(WorkspaceStore, "selectedLayoutMode").andReturn "split"
@thread_list.setState selectedId: "t111"
it "archives and next in split mode", ->
@thread_list._onArchiveCurrentThread()
expect(Actions.archiveCurrentThread).not.toHaveBeenCalled()
expect(Actions.archiveAndNext).toHaveBeenCalled()
it "allows reply and reply-all regardless of sheet type", ->
spyOn(WorkspaceStore, "sheet").andCallFake -> {type: "anything"}
spyOn(Actions, "composeReply")
@thread_list._onReply()
expect(Actions.composeReply).toHaveBeenCalled()
expect(@thread_list._actionInVisualScope()).toBe true
describe "Populated thread list", ->
beforeEach ->
ThreadStore._items = test_threads()

View file

@ -11,7 +11,7 @@
'j' : 'application:next-item' # Gmail
'down' : 'application:next-item' # Mac mail
']' : 'application:remove-and-previous' # Gmail
'[' : 'application:remove-item' # Gmail
'[' : 'application:remove-and-next' # Gmail
'e' : 'application:remove-item' # Gmail
'delete' : 'application:remove-item' # Mac mail
'backspace': 'application:remove-item' # Outlook
@ -20,6 +20,7 @@
'/' : 'application:focus-search' # Gmail
'r' : 'application:reply' # Gmail
'R' : 'application:reply-all' # Edgehill
'a' : 'application:reply-all' # Gmail
'f' : 'application:forward' # Gmail

View file

@ -70,6 +70,10 @@ windowActions = [
"sendDraft",
"destroyDraft",
"archiveAndPrevious",
"archiveCurrentThread",
"archiveAndNext",
# Actions for Search
"searchQueryChanged",
"searchQueryCommitted",

View file

@ -232,8 +232,8 @@ class InboxAPI
# API abstraction should not need to know about threads and calendars.
# They're still here because of their dependency in
# _postLaunchStartStreaming
getThreads: (namespaceId, params) ->
@getCollection(namespaceId, 'threads', params)
getThreads: (namespaceId, params, requestOptions={}) ->
@getCollection(namespaceId, 'threads', params, requestOptions)
getCalendars: (namespaceId) ->
@getCollection(namespaceId, 'calendars', {})

View file

@ -61,11 +61,6 @@ class Thread extends Model
isStarred: ->
@tagIds().indexOf('starred') != -1
markAsRead: ->
MarkThreadReadTask = require '../tasks/mark-thread-read'
task = new MarkThreadReadTask(@id)
Actions.queueTask(task)
star: ->
@addRemoveTags(['starred'], [])
@ -78,13 +73,6 @@ class Thread extends Model
else
@star()
archive: ->
Actions.postNotification({message: "Archived thread", type: 'success'})
@addRemoveTags(['archive'], ['inbox'])
unarchive: ->
@addRemoveTags(['inbox'], ['archive'])
addRemoveTags: (tagIdsToAdd, tagIdsToRemove) ->
# start web change, which will dispatch more actions
AddRemoveTagsTask = require '../tasks/add-remove-tags'

View file

@ -1,6 +1,8 @@
Reflux = require 'reflux'
DatabaseStore = require './database-store'
NamespaceStore = require './namespace-store'
AddRemoveTagsTask = require '../tasks/add-remove-tags'
MarkThreadReadTask = require '../tasks/mark-thread-read'
Actions = require '../actions'
Thread = require '../models/thread'
_ = require 'underscore-plus'
@ -11,6 +13,9 @@ ThreadStore = Reflux.createStore
@listenTo Actions.selectThreadId, @_onSelectThreadId
@listenTo Actions.selectTagId, @_onSelectTagId
@listenTo Actions.archiveAndPrevious, @_onArchiveAndPrevious
@listenTo Actions.archiveCurrentThread, @_onArchiveCurrentThread
@listenTo Actions.archiveAndNext, @_onArchiveAndNext
@listenTo Actions.searchQueryCommitted, @_onSearchCommitted
@listenTo DatabaseStore, @_onDataChanged
@listenTo NamespaceStore, -> @_onNamespaceChanged()
@ -23,6 +28,9 @@ ThreadStore = Reflux.createStore
@_namespaceId = null
@_tagId = null
@_searchQuery = null
@_itemsLoading = false
itemsLoading: -> @_itemsLoading
fetchFromCache: ->
return unless @_namespaceId
@ -56,16 +64,26 @@ ThreadStore = Reflux.createStore
newSelectedId = null
Actions.selectThreadId(newSelectedId)
@_itemsLoading = false
@trigger()
fetchFromAPI: ->
return unless @_namespaceId
@_itemsLoading = true
if @_searchQuery
atom.inbox.getThreadsForSearch @_namespaceId, @_searchQuery, (items) =>
@_items = items
@_itemsLoading = false
@trigger()
else
atom.inbox.getThreads(@_namespaceId, {tag: @_tagId})
success = =>
@_itemsLoading = false
@trigger()
error = =>
@_itemsLoading = false
@trigger()
atom.inbox.getThreads(@_namespaceId, {tag: @_tagId}, {success: success, error: error})
@trigger()
# Inbound Events
@ -83,6 +101,8 @@ ThreadStore = Reflux.createStore
@fetchFromCache()
_onSearchCommitted: (query) ->
Actions.selectThreadId(null)
if query.length > 0
@_searchQuery = query
@_items = []
@ -93,7 +113,6 @@ ThreadStore = Reflux.createStore
@fetchFromCache()
@_lastQuery = query
Actions.selectThreadId(null)
@fetchFromAPI()
_onSelectTagId: (id) ->
@ -108,10 +127,45 @@ ThreadStore = Reflux.createStore
thread = @selectedThread()
if thread && thread.isUnread()
thread.markAsRead()
Actions.queueTask(new MarkThreadReadTask(thread.id))
@trigger()
_onArchiveCurrentThread: ({silent}={}) ->
thread = @selectedThread()
return unless thread
@_archive(thread.id)
@_selectedId = null
if not silent
@trigger()
Actions.popSheet()
Actions.selectThreadId(null)
_archive: (threadId) ->
Actions.postNotification({message: "Archived thread", type: 'success'})
task = new AddRemoveTagsTask(threadId, ['archive'], ['inbox'])
Actions.queueTask(task)
_threadOffsetFromCurrentBy: (offset=0) ->
thread = @selectedThread()
index = @_items.indexOf(thread)
return null if index is -1
index += offset
index = Math.min(Math.max(index, 0), @_items.length - 1)
return @_items[index]
_onArchiveAndPrevious: ->
return unless @_selectedId
newSelectedId = @_threadOffsetFromCurrentBy(-1)?.id
@_onArchiveCurrentThread(silent: true)
Actions.selectThreadId(newSelectedId)
_onArchiveAndNext: ->
return unless @_selectedId
newSelectedId = @_threadOffsetFromCurrentBy(1)?.id
@_onArchiveCurrentThread(silent: true)
Actions.selectThreadId(newSelectedId)
# Accessing Data
selectedTagId: ->

View file

@ -261,6 +261,7 @@
@input-accessory-color-hover: @light-blue;
@input-accessory-color: @cool-gray;
@input-cancel-color: @red;
//** Text color for `<input>`s
@input-color: @gray;