2015-05-20 07:06:59 +08:00
|
|
|
_ = require "underscore"
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
proxyquire = require "proxyquire"
|
|
|
|
|
|
|
|
React = require "react/addons"
|
|
|
|
ReactTestUtils = React.addons.TestUtils
|
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
{Actions,
|
2015-06-18 03:29:21 +08:00
|
|
|
File,
|
2015-03-13 05:48:56 +08:00
|
|
|
Contact,
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
Message,
|
feat(accounts): Kill namespaces, long live accounts
Summary:
This diff replaces the Namespace object with the Account object, and changes all references to namespace_id => account_id, etc. The endpoints are now `/threads` instead of `/n/<id>/threads`.
This diff also adds preliminary support for multiple accounts. When you log in, we now log you in to all the attached accounts on edgehill server. From the preferences panel, you can auth with / unlink additional accounts. Shockingly, this all seems to pretty much work.
When replying to a thread, you cannot switch from addresses. However, when creating a new message in a popout composer, you can change the from address and the SaveDraftTask will delete/re-root the draft on the new account.
Search bar doesn't need to do full refresh on clear if it never committed
Allow drafts to be switched to a different account when not in reply to an existing thread
Fix edge case where ChangeMailTask throws exception if no models are modified during performLocal
Show many dots for many accounts in long polling status bar
add/remove accounts from prefs
Spec fixes!
Test Plan: Run tests, none broken!
Reviewers: evan, dillon
Reviewed By: evan, dillon
Differential Revision: https://phab.nylas.com/D1928
2015-08-22 06:29:58 +08:00
|
|
|
Account,
|
2015-03-13 05:48:56 +08:00
|
|
|
DraftStore,
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
DatabaseStore,
|
2015-05-20 07:06:59 +08:00
|
|
|
NylasTestUtils,
|
feat(accounts): Kill namespaces, long live accounts
Summary:
This diff replaces the Namespace object with the Account object, and changes all references to namespace_id => account_id, etc. The endpoints are now `/threads` instead of `/n/<id>/threads`.
This diff also adds preliminary support for multiple accounts. When you log in, we now log you in to all the attached accounts on edgehill server. From the preferences panel, you can auth with / unlink additional accounts. Shockingly, this all seems to pretty much work.
When replying to a thread, you cannot switch from addresses. However, when creating a new message in a popout composer, you can change the from address and the SaveDraftTask will delete/re-root the draft on the new account.
Search bar doesn't need to do full refresh on clear if it never committed
Allow drafts to be switched to a different account when not in reply to an existing thread
Fix edge case where ChangeMailTask throws exception if no models are modified during performLocal
Show many dots for many accounts in long polling status bar
add/remove accounts from prefs
Spec fixes!
Test Plan: Run tests, none broken!
Reviewers: evan, dillon
Reviewed By: evan, dillon
Differential Revision: https://phab.nylas.com/D1928
2015-08-22 06:29:58 +08:00
|
|
|
AccountStore,
|
2015-06-12 02:52:49 +08:00
|
|
|
FileUploadStore,
|
2015-08-04 04:06:28 +08:00
|
|
|
ContactStore,
|
2015-06-12 02:52:49 +08:00
|
|
|
ComponentRegistry} = require "nylas-exports"
|
|
|
|
|
|
|
|
{InjectedComponent} = require 'nylas-component-kit'
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-06-12 09:48:55 +08:00
|
|
|
ParticipantsTextField = require '../lib/participants-text-field'
|
|
|
|
|
2015-05-15 08:08:30 +08:00
|
|
|
u1 = new Contact(name: "Christine Spang", email: "spang@nylas.com")
|
|
|
|
u2 = new Contact(name: "Michael Grinich", email: "mg@nylas.com")
|
|
|
|
u3 = new Contact(name: "Evan Morikawa", email: "evan@nylas.com")
|
|
|
|
u4 = new Contact(name: "Zoë Leiper", email: "zip@nylas.com")
|
|
|
|
u5 = new Contact(name: "Ben Gotow", email: "ben@nylas.com")
|
2015-06-18 03:29:21 +08:00
|
|
|
|
|
|
|
file = new File(id: 'file_1_id', filename: 'a.png', contentType: 'image/png', size: 10, object: "file")
|
|
|
|
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
users = [u1, u2, u3, u4, u5]
|
|
|
|
|
|
|
|
reactStub = (className) ->
|
|
|
|
React.createClass({render: -> <div className={className}>{@props.children}</div>})
|
|
|
|
|
2015-02-11 03:10:14 +08:00
|
|
|
textFieldStub = (className) ->
|
|
|
|
React.createClass
|
|
|
|
render: -> <div className={className}>{@props.children}</div>
|
|
|
|
focus: ->
|
|
|
|
|
2015-06-12 03:08:47 +08:00
|
|
|
passThroughStub = (props={}) ->
|
2015-06-12 02:52:49 +08:00
|
|
|
React.createClass
|
|
|
|
render: -> <div {...props}>{props.children}</div>
|
|
|
|
|
2015-08-29 02:12:53 +08:00
|
|
|
draftStoreProxyStub = (draftClientId, returnedDraft) ->
|
2015-02-19 06:09:46 +08:00
|
|
|
listen: -> ->
|
2015-03-13 05:48:56 +08:00
|
|
|
draft: -> (returnedDraft ? new Message(draft: true))
|
feat(signatures): Initial signature support
Summary:
- Draft Store extensions can now implement `prepareNewDraft` to have an opportunity to change a draft before it's displayed for the first time.
- When composers are torn down, they delete their draft if it is still pristine. This makes the behavior of closing unedited popout drafts the same as leaving unedited inline drafts.
- The DraftStoreProxy keeps the initial body of the draft *if* it started in a pristine state. This means "is the body empty" is just a simple == check, and it takes into account anything added to the body by extensions.
- Calling Actions.destroyDraft doesn't blow up anymore if the draft session can't be found. This was a bug and meant that you couldn't destroy drafts which hadn't been previously edited, and also meant that bad things(tm) happened when you called destroyDraft twice, which seemed like overkill.
- DestroyDraft task now exits gracefully when the draft cannot be found.
You can test this feature by adding the following to your config.cson:
```
signatures:
NAMESPACEID: "<br/><br/><div id=\"Signature\"><div id=\"divtagdefaultwrapper\" style=\"font-size:12pt; color:#000000; background-color:#FFFFFF; font-family:Calibri,Arial,Helvetica,sans-serif\"><p></p><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"450\" style=\"font-family:'Times New Roman'; table-layout:fixed\"><tbody><tr><td class=\"logo-td\" align=\"left\" valign=\"top\" width=\"76\"><p style=\"margin-bottom:10px; margin-right:10px; font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px\"><a href=\"http://www.nylas.com/\" class=\"clink logo-container\" style=\"text-decoration:none\"><img alt=\"Nylas\" border=\"0\" class=\"sig-logo\" height=\"80\" width=\"66\" style=\"-webkit-user-select: none;\" src=\"https://s3-us-west-2.amazonaws.com/nylas-static-assets/nylas-email-signature.png\"></a></p><p class=\"social-list\" style=\"font-size:0px; line-height:0; font-family:Helvetica,Arial,sans-serif\"></p></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"spacer-td\" width=\"16\" style=\"border-left-width:2px; border-left-style:solid; border-left-color:rgb(30,162,162)\"><img width=\"10\" style=\"-webkit-user-select: none;\" src=\"https://s3.amazonaws.com/htmlsig-assets/spacer.gif\"></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"content-td\" width=\"368\"><div class=\"content-pad\"><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; color:rgb(33,33,33); margin-bottom:10px\"><span class=\"txt signature_name-target sig-hide\" style=\"font-weight:bold; display:inline\">Gleb Polyakov</span> <span class=\"email-sep break\" style=\"display:inline\"><br></span><a class=\"link email signature_email-target sig-hide\" href=\"mailto:gleb@nylas.com\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">gleb@nylas.com</a><span class=\"signature_email-sep sep\" style=\"display:inline\"> / </span><span class=\"txt signature_mobilephone-target sig-hide\" style=\"display:inline\">404-786-4100</span></p><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; margin-bottom:10px\"><span class=\"txt signature_companyname-target sig-hide\" style=\"font-weight:bold; color:rgb(33,33,33); display:inline\">Nylas</span> <span class=\"company-sep break\" style=\"display:inline\"><br></span><span class=\"address-sep break\"></span><span class=\"address2-sep break\"></span><span class=\"website-sep break\"></span><a class=\"link signature_website-target sig-hide\" href=\"http://www.nylas.com/\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">http://www.nylas.com</a></p></div></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"><p class=\"txt signature_disclaimer-target\" style=\"font-family:Helvetica,Arial,sans-serif; color:rgb(33,33,33); font-size:9px; line-height:12px; margin-top:10px\"></p></td></tr></tbody></table><p></p></div></div>"
```
specs for draft store extension hooks, some draft store refactoring
Test Plan: Run a few new specs that make sure extensions are run
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1741
2015-07-15 03:20:06 +08:00
|
|
|
draftPristineBody: -> null
|
2015-08-29 02:12:53 +08:00
|
|
|
draftClientId: draftClientId
|
2015-07-13 22:50:27 +08:00
|
|
|
cleanup: ->
|
2015-02-11 03:10:14 +08:00
|
|
|
changes:
|
|
|
|
add: ->
|
2015-07-13 22:50:27 +08:00
|
|
|
commit: -> Promise.resolve()
|
2015-02-11 03:10:14 +08:00
|
|
|
applyToModel: ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
|
|
|
searchContactStub = (email) ->
|
|
|
|
_.filter(users, (u) u.email.toLowerCase() is email.toLowerCase())
|
|
|
|
|
2015-08-04 04:06:28 +08:00
|
|
|
isValidContactStub = (contact) ->
|
|
|
|
contact.email.indexOf('@') > 0
|
|
|
|
|
2015-03-21 08:51:49 +08:00
|
|
|
ComposerView = proxyquire "../lib/composer-view",
|
2015-06-12 02:52:49 +08:00
|
|
|
"./file-upload": reactStub("file-upload")
|
|
|
|
"./image-file-upload": reactStub("image-file-upload")
|
2015-05-15 08:08:30 +08:00
|
|
|
"nylas-exports":
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
ContactStore:
|
2015-08-04 04:06:28 +08:00
|
|
|
searchContacts: searchContactStub
|
|
|
|
isValidContact: isValidContactStub
|
2015-03-13 05:48:56 +08:00
|
|
|
DraftStore: DraftStore
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
|
|
|
beforeEach ->
|
feat(accounts): Kill namespaces, long live accounts
Summary:
This diff replaces the Namespace object with the Account object, and changes all references to namespace_id => account_id, etc. The endpoints are now `/threads` instead of `/n/<id>/threads`.
This diff also adds preliminary support for multiple accounts. When you log in, we now log you in to all the attached accounts on edgehill server. From the preferences panel, you can auth with / unlink additional accounts. Shockingly, this all seems to pretty much work.
When replying to a thread, you cannot switch from addresses. However, when creating a new message in a popout composer, you can change the from address and the SaveDraftTask will delete/re-root the draft on the new account.
Search bar doesn't need to do full refresh on clear if it never committed
Allow drafts to be switched to a different account when not in reply to an existing thread
Fix edge case where ChangeMailTask throws exception if no models are modified during performLocal
Show many dots for many accounts in long polling status bar
add/remove accounts from prefs
Spec fixes!
Test Plan: Run tests, none broken!
Reviewers: evan, dillon
Reviewed By: evan, dillon
Differential Revision: https://phab.nylas.com/D1928
2015-08-22 06:29:58 +08:00
|
|
|
# The AccountStore isn't set yet in the new window, populate it first.
|
|
|
|
AccountStore.populateItems().then ->
|
2015-08-29 02:12:53 +08:00
|
|
|
draft = new Message
|
|
|
|
from: [AccountStore.current().me()]
|
|
|
|
date: (new Date)
|
|
|
|
draft: true
|
|
|
|
accountId: AccountStore.current().id
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-08-29 02:12:53 +08:00
|
|
|
DatabaseStore.persistModel(draft).then ->
|
|
|
|
return draft
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-02-11 03:10:14 +08:00
|
|
|
describe "A blank composer view", ->
|
|
|
|
beforeEach ->
|
|
|
|
@composer = ReactTestUtils.renderIntoDocument(
|
2015-08-29 02:12:53 +08:00
|
|
|
<ComposerView draftClientId="test123" />
|
2015-02-11 03:10:14 +08:00
|
|
|
)
|
|
|
|
@composer.setState
|
|
|
|
body: ""
|
|
|
|
|
|
|
|
it 'should render into the document', ->
|
|
|
|
expect(ReactTestUtils.isCompositeComponentWithType @composer, ComposerView).toBe true
|
|
|
|
|
|
|
|
describe "testing keyboard inputs", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
it "shows and focuses on bcc field", ->
|
2015-02-11 03:10:14 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
it "shows and focuses on cc field", ->
|
2015-02-17 09:09:28 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
it "shows and focuses on bcc field when already open", ->
|
2015-02-17 09:09:28 +08:00
|
|
|
|
2015-07-13 22:50:27 +08:00
|
|
|
# This will setup the mocks necessary to make the composer element (once
|
|
|
|
# mounted) think it's attached to the given draft. This mocks out the
|
|
|
|
# proxy system used by the composer.
|
2015-08-29 02:12:53 +08:00
|
|
|
DRAFT_CLIENT_ID = "local-123"
|
2015-07-13 22:50:27 +08:00
|
|
|
useDraft = (draftAttributes={}) ->
|
|
|
|
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
|
|
|
|
draft = @draft
|
2015-08-29 02:12:53 +08:00
|
|
|
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
2015-09-04 10:41:56 +08:00
|
|
|
@proxy = proxy
|
2015-07-22 05:16:11 +08:00
|
|
|
|
|
|
|
|
2015-07-13 22:50:27 +08:00
|
|
|
spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
|
2015-09-04 10:41:56 +08:00
|
|
|
# NOTE: This is called in the context of the component.
|
2015-08-29 02:12:53 +08:00
|
|
|
@_prepareForDraft(DRAFT_CLIENT_ID)
|
2015-07-13 22:50:27 +08:00
|
|
|
@_setupSession(proxy)
|
|
|
|
|
2015-08-29 02:12:53 +08:00
|
|
|
# Normally when sessionForClientId resolves, it will call `_setupSession`
|
2015-07-22 05:16:11 +08:00
|
|
|
# and pass the new session proxy. However, in our faked
|
2015-08-29 02:12:53 +08:00
|
|
|
# `componentWillMount`, we manually call sessionForClientId to make this
|
2015-07-22 05:16:11 +08:00
|
|
|
# part of the test synchronous. We need to make the `then` block of the
|
2015-08-29 02:12:53 +08:00
|
|
|
# sessionForClientId do nothing so `_setupSession` is not called twice!
|
2015-09-04 10:41:56 +08:00
|
|
|
spyOn(DraftStore, "sessionForClientId").andCallFake -> then: ->
|
2015-07-22 05:16:11 +08:00
|
|
|
|
2015-07-13 22:50:27 +08:00
|
|
|
useFullDraft = ->
|
|
|
|
useDraft.call @,
|
|
|
|
from: [u1]
|
|
|
|
to: [u2]
|
|
|
|
cc: [u3, u4]
|
|
|
|
bcc: [u5]
|
|
|
|
subject: "Test Message 1"
|
|
|
|
body: "Hello <b>World</b><br/> This is a test"
|
feat(accounts): Kill namespaces, long live accounts
Summary:
This diff replaces the Namespace object with the Account object, and changes all references to namespace_id => account_id, etc. The endpoints are now `/threads` instead of `/n/<id>/threads`.
This diff also adds preliminary support for multiple accounts. When you log in, we now log you in to all the attached accounts on edgehill server. From the preferences panel, you can auth with / unlink additional accounts. Shockingly, this all seems to pretty much work.
When replying to a thread, you cannot switch from addresses. However, when creating a new message in a popout composer, you can change the from address and the SaveDraftTask will delete/re-root the draft on the new account.
Search bar doesn't need to do full refresh on clear if it never committed
Allow drafts to be switched to a different account when not in reply to an existing thread
Fix edge case where ChangeMailTask throws exception if no models are modified during performLocal
Show many dots for many accounts in long polling status bar
add/remove accounts from prefs
Spec fixes!
Test Plan: Run tests, none broken!
Reviewers: evan, dillon
Reviewed By: evan, dillon
Differential Revision: https://phab.nylas.com/D1928
2015-08-22 06:29:58 +08:00
|
|
|
replyToMessageId: null
|
2015-07-13 22:50:27 +08:00
|
|
|
|
|
|
|
makeComposer = ->
|
|
|
|
@composer = ReactTestUtils.renderIntoDocument(
|
2015-08-29 02:12:53 +08:00
|
|
|
<ComposerView draftClientId={DRAFT_CLIENT_ID} />
|
2015-07-13 22:50:27 +08:00
|
|
|
)
|
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "populated composer", ->
|
2015-07-13 22:50:27 +08:00
|
|
|
beforeEach ->
|
|
|
|
@isSending = {state: false}
|
|
|
|
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
|
2015-02-11 03:10:14 +08:00
|
|
|
|
2015-09-04 10:41:56 +08:00
|
|
|
describe "when sending a new message", ->
|
|
|
|
it 'makes a request with the message contents', ->
|
|
|
|
useDraft.call @
|
|
|
|
makeComposer.call @
|
|
|
|
editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
|
|
|
|
spyOn(@proxy.changes, "add")
|
|
|
|
editableNode.innerHTML = "Hello <strong>world</strong>"
|
|
|
|
ReactTestUtils.Simulate.input(editableNode)
|
|
|
|
expect(@proxy.changes.add).toHaveBeenCalled()
|
|
|
|
expect(@proxy.changes.add.calls.length).toBe 1
|
|
|
|
body = @proxy.changes.add.calls[0].args[0].body
|
|
|
|
expect(body).toBe "<head></head><body>Hello <strong>world</strong></body>"
|
|
|
|
|
|
|
|
describe "when sending a reply-to message", ->
|
|
|
|
beforeEach ->
|
|
|
|
@replyBody = """<blockquote class="gmail_quote">On Sep 3 2015, at 12:14 pm, Evan Morikawa <evan@evanmorikawa.com> wrote:<br>This is a test!</blockquote>"""
|
|
|
|
|
|
|
|
useDraft.call @,
|
|
|
|
from: [u1]
|
|
|
|
to: [u2]
|
|
|
|
subject: "Test Reply Message 1"
|
|
|
|
body: @replyBody
|
|
|
|
|
|
|
|
makeComposer.call @
|
|
|
|
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
|
|
|
|
spyOn(@proxy.changes, "add")
|
|
|
|
|
|
|
|
it 'begins with the replying message collapsed', ->
|
|
|
|
expect(@editableNode.innerHTML).toBe ""
|
|
|
|
|
|
|
|
it 'saves the full new body, plus quoted text', ->
|
|
|
|
@editableNode.innerHTML = "Hello <strong>world</strong>"
|
|
|
|
ReactTestUtils.Simulate.input(@editableNode)
|
|
|
|
expect(@proxy.changes.add).toHaveBeenCalled()
|
|
|
|
expect(@proxy.changes.add.calls.length).toBe 1
|
|
|
|
body = @proxy.changes.add.calls[0].args[0].body
|
|
|
|
expect(body).toBe """<head></head><body>Hello <strong>world</strong>#{@replyBody}</body>"""
|
|
|
|
|
|
|
|
describe "when sending a forwarded message message", ->
|
|
|
|
beforeEach ->
|
|
|
|
@fwdBody = """<br><br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
|
|
|
Begin forwarded message:
|
|
|
|
<br><br>
|
|
|
|
From: Evan Morikawa <evan@evanmorikawa.com><br>Subject: Test Forward Message 1<br>Date: Sep 3 2015, at 12:14 pm<br>To: Evan Morikawa <evan@nylas.com>
|
|
|
|
<br><br>
|
|
|
|
|
|
|
|
<meta content="text/html; charset=us-ascii">This is a test!
|
|
|
|
</blockquote>"""
|
|
|
|
|
|
|
|
useDraft.call @,
|
|
|
|
from: [u1]
|
|
|
|
to: [u2]
|
|
|
|
subject: "Fwd: Test Forward Message 1"
|
|
|
|
body: @fwdBody
|
|
|
|
|
|
|
|
makeComposer.call @
|
|
|
|
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
|
|
|
|
spyOn(@proxy.changes, "add")
|
|
|
|
|
|
|
|
it 'begins with the forwarded message expanded', ->
|
|
|
|
expect(@editableNode.innerHTML).toBe @fwdBody
|
|
|
|
|
|
|
|
it 'saves the full new body, plus forwarded text', ->
|
|
|
|
@editableNode.innerHTML = "Hello <strong>world</strong>#{@fwdBody}"
|
|
|
|
ReactTestUtils.Simulate.input(@editableNode)
|
|
|
|
expect(@proxy.changes.add).toHaveBeenCalled()
|
|
|
|
expect(@proxy.changes.add.calls.length).toBe 1
|
|
|
|
body = @proxy.changes.add.calls[0].args[0].body
|
|
|
|
expect(body).toBe """Hello <strong>world</strong>#{@fwdBody}"""
|
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "When displaying info from a draft", ->
|
|
|
|
beforeEach ->
|
|
|
|
useFullDraft.apply(@)
|
|
|
|
makeComposer.call(@)
|
2015-02-11 03:10:14 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
it "attaches the draft to the proxy", ->
|
|
|
|
expect(@draft).toBeDefined()
|
|
|
|
expect(@composer._proxy.draft()).toBe @draft
|
|
|
|
|
|
|
|
it "set the state based on the draft", ->
|
feat(accounts): Kill namespaces, long live accounts
Summary:
This diff replaces the Namespace object with the Account object, and changes all references to namespace_id => account_id, etc. The endpoints are now `/threads` instead of `/n/<id>/threads`.
This diff also adds preliminary support for multiple accounts. When you log in, we now log you in to all the attached accounts on edgehill server. From the preferences panel, you can auth with / unlink additional accounts. Shockingly, this all seems to pretty much work.
When replying to a thread, you cannot switch from addresses. However, when creating a new message in a popout composer, you can change the from address and the SaveDraftTask will delete/re-root the draft on the new account.
Search bar doesn't need to do full refresh on clear if it never committed
Allow drafts to be switched to a different account when not in reply to an existing thread
Fix edge case where ChangeMailTask throws exception if no models are modified during performLocal
Show many dots for many accounts in long polling status bar
add/remove accounts from prefs
Spec fixes!
Test Plan: Run tests, none broken!
Reviewers: evan, dillon
Reviewed By: evan, dillon
Differential Revision: https://phab.nylas.com/D1928
2015-08-22 06:29:58 +08:00
|
|
|
expect(@composer.state.from).toEqual [u1]
|
|
|
|
expect(@composer.state.showfrom).toEqual true
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(@composer.state.to).toEqual [u2]
|
|
|
|
expect(@composer.state.cc).toEqual [u3, u4]
|
|
|
|
expect(@composer.state.bcc).toEqual [u5]
|
|
|
|
expect(@composer.state.subject).toEqual "Test Message 1"
|
|
|
|
expect(@composer.state.body).toEqual "Hello <b>World</b><br/> This is a test"
|
|
|
|
|
|
|
|
describe "when deciding whether or not to show the subject", ->
|
2015-03-11 09:27:10 +08:00
|
|
|
it "shows the subject when the subject is empty", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
useDraft.call @, subject: ""
|
|
|
|
makeComposer.call @
|
2015-03-11 09:27:10 +08:00
|
|
|
expect(@composer._shouldShowSubject()).toBe true
|
|
|
|
|
|
|
|
it "shows the subject when the subject looks like a fwd", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
useDraft.call @, subject: "Fwd: This is the message"
|
|
|
|
makeComposer.call @
|
2015-03-11 09:27:10 +08:00
|
|
|
expect(@composer._shouldShowSubject()).toBe true
|
|
|
|
|
|
|
|
it "shows the subject when the subject looks like a fwd", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
useDraft.call @, subject: "fwd foo"
|
|
|
|
makeComposer.call @
|
2015-03-11 09:27:10 +08:00
|
|
|
expect(@composer._shouldShowSubject()).toBe true
|
|
|
|
|
2015-09-09 01:27:50 +08:00
|
|
|
it "doesn't show subject when replyToMessageId exists", ->
|
|
|
|
useDraft.call @, subject: "should hide", replyToMessageId: "some-id"
|
2015-03-13 05:48:56 +08:00
|
|
|
makeComposer.call @
|
2015-03-11 09:27:10 +08:00
|
|
|
expect(@composer._shouldShowSubject()).toBe false
|
|
|
|
|
2015-09-09 01:27:50 +08:00
|
|
|
it "shows the subject otherwise", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
useDraft.call @, subject: "Foo bar baz"
|
|
|
|
makeComposer.call @
|
2015-09-09 01:27:50 +08:00
|
|
|
expect(@composer._shouldShowSubject()).toBe true
|
2015-03-11 09:27:10 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "when deciding whether or not to show cc and bcc", ->
|
|
|
|
it "doesn't show cc when there's no one to cc", ->
|
|
|
|
useDraft.call @, cc: []
|
|
|
|
makeComposer.call @
|
|
|
|
expect(@composer.state.showcc).toBe false
|
|
|
|
|
|
|
|
it "shows cc when populated", ->
|
|
|
|
useDraft.call @, cc: [u1,u2]
|
|
|
|
makeComposer.call @
|
|
|
|
expect(@composer.state.showcc).toBe true
|
|
|
|
|
|
|
|
it "doesn't show bcc when there's no one to bcc", ->
|
|
|
|
useDraft.call @, bcc: []
|
|
|
|
makeComposer.call @
|
|
|
|
expect(@composer.state.showbcc).toBe false
|
|
|
|
|
|
|
|
it "shows bcc when populated", ->
|
|
|
|
useDraft.call @, bcc: [u2,u3]
|
|
|
|
makeComposer.call @
|
|
|
|
expect(@composer.state.showbcc).toBe true
|
|
|
|
|
2015-06-02 06:12:18 +08:00
|
|
|
describe "when focus() is called", ->
|
|
|
|
describe "if a field name is provided", ->
|
|
|
|
it "should focus that field", ->
|
2015-06-06 02:02:44 +08:00
|
|
|
useDraft.call(@, cc: [u2])
|
2015-06-02 06:12:18 +08:00
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer.refs['textFieldCc'], 'focus')
|
|
|
|
@composer.focus('textFieldCc')
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['textFieldCc'].focus).toHaveBeenCalled()
|
|
|
|
|
|
|
|
describe "if the draft is a forward", ->
|
|
|
|
it "should focus the to field", ->
|
|
|
|
useDraft.call(@, {subject: 'Fwd: This is a test'})
|
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer.refs['textFieldTo'], 'focus')
|
|
|
|
@composer.focus()
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['textFieldTo'].focus).toHaveBeenCalled()
|
|
|
|
|
|
|
|
describe "if the draft is a normal message", ->
|
|
|
|
it "should focus on the body", ->
|
|
|
|
useDraft.call(@)
|
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer.refs['contentBody'], 'focus')
|
|
|
|
@composer.focus()
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['contentBody'].focus).toHaveBeenCalled()
|
2015-06-18 03:29:32 +08:00
|
|
|
|
|
|
|
describe "if the draft has not yet loaded", ->
|
|
|
|
it "should set _focusOnUpdate and focus after the next render", ->
|
feat(offline-mode, undo-redo): Tasks handle network errors better and retry, undo/redo based on tasks
Summary:
This diff does a couple things:
- Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable.
- NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request.
- Aborting a NylasAPI request now makes it call it's error callback / promise reject.
- You can now run code after perform local has completed using this syntax:
```
task = new AddRemoveTagsTask(focused, ['archive'], ['inbox'])
task.waitForPerformLocal().then ->
Actions.setFocus(collection: 'thread', item: nextFocus)
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
Actions.queueTask(task)
```
- In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0).
- The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote".
- All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later.
- API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish.
- The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon.
Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero.
Known Issues:
- AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other.
- When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar.
- The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first!
Test Plan: All the tests still pass!
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1694
2015-07-08 01:38:53 +08:00
|
|
|
@draft = new Message(draft: true, body: "")
|
2015-08-29 02:12:53 +08:00
|
|
|
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
feat(offline-mode, undo-redo): Tasks handle network errors better and retry, undo/redo based on tasks
Summary:
This diff does a couple things:
- Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable.
- NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request.
- Aborting a NylasAPI request now makes it call it's error callback / promise reject.
- You can now run code after perform local has completed using this syntax:
```
task = new AddRemoveTagsTask(focused, ['archive'], ['inbox'])
task.waitForPerformLocal().then ->
Actions.setFocus(collection: 'thread', item: nextFocus)
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
Actions.queueTask(task)
```
- In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0).
- The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote".
- All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later.
- API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish.
- The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon.
Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero.
Known Issues:
- AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other.
- When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar.
- The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first!
Test Plan: All the tests still pass!
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1694
2015-07-08 01:38:53 +08:00
|
|
|
proxyResolve = null
|
2015-08-29 02:12:53 +08:00
|
|
|
spyOn(DraftStore, "sessionForClientId").andCallFake ->
|
feat(offline-mode, undo-redo): Tasks handle network errors better and retry, undo/redo based on tasks
Summary:
This diff does a couple things:
- Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable.
- NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request.
- Aborting a NylasAPI request now makes it call it's error callback / promise reject.
- You can now run code after perform local has completed using this syntax:
```
task = new AddRemoveTagsTask(focused, ['archive'], ['inbox'])
task.waitForPerformLocal().then ->
Actions.setFocus(collection: 'thread', item: nextFocus)
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
Actions.queueTask(task)
```
- In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0).
- The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote".
- All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later.
- API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish.
- The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon.
Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero.
Known Issues:
- AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other.
- When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar.
- The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first!
Test Plan: All the tests still pass!
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1694
2015-07-08 01:38:53 +08:00
|
|
|
new Promise (resolve, reject) ->
|
|
|
|
proxyResolve = resolve
|
2015-06-18 03:29:32 +08:00
|
|
|
|
feat(offline-mode, undo-redo): Tasks handle network errors better and retry, undo/redo based on tasks
Summary:
This diff does a couple things:
- Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable.
- NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request.
- Aborting a NylasAPI request now makes it call it's error callback / promise reject.
- You can now run code after perform local has completed using this syntax:
```
task = new AddRemoveTagsTask(focused, ['archive'], ['inbox'])
task.waitForPerformLocal().then ->
Actions.setFocus(collection: 'thread', item: nextFocus)
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
Actions.queueTask(task)
```
- In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0).
- The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote".
- All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later.
- API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish.
- The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon.
Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero.
Known Issues:
- AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other.
- When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar.
- The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first!
Test Plan: All the tests still pass!
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1694
2015-07-08 01:38:53 +08:00
|
|
|
makeComposer.call(@)
|
2015-06-18 03:29:32 +08:00
|
|
|
|
|
|
|
spyOn(@composer.refs['contentBody'], 'focus')
|
|
|
|
@composer.focus()
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['contentBody'].focus).not.toHaveBeenCalled()
|
|
|
|
|
feat(offline-mode, undo-redo): Tasks handle network errors better and retry, undo/redo based on tasks
Summary:
This diff does a couple things:
- Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable.
- NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request.
- Aborting a NylasAPI request now makes it call it's error callback / promise reject.
- You can now run code after perform local has completed using this syntax:
```
task = new AddRemoveTagsTask(focused, ['archive'], ['inbox'])
task.waitForPerformLocal().then ->
Actions.setFocus(collection: 'thread', item: nextFocus)
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
Actions.queueTask(task)
```
- In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0).
- The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote".
- All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later.
- API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish.
- The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon.
Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero.
Known Issues:
- AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other.
- When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar.
- The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first!
Test Plan: All the tests still pass!
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1694
2015-07-08 01:38:53 +08:00
|
|
|
proxyResolve(proxy)
|
2015-06-18 03:29:32 +08:00
|
|
|
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['contentBody'].focus).toHaveBeenCalled()
|
2015-06-02 06:12:18 +08:00
|
|
|
|
2015-07-14 07:30:02 +08:00
|
|
|
describe "when emptying cc/bcc fields", ->
|
2015-06-12 09:48:55 +08:00
|
|
|
|
|
|
|
it "focuses on to when bcc is emptied and there's no cc field", ->
|
|
|
|
useDraft.call(@, bcc: [u1])
|
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer.refs['textFieldTo'], 'focus')
|
|
|
|
spyOn(@composer.refs['textFieldBcc'], 'focus')
|
|
|
|
|
|
|
|
bcc = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, ParticipantsTextField, field: "bcc")[0]
|
2015-07-14 07:30:02 +08:00
|
|
|
@draft.bcc = []
|
2015-06-12 09:48:55 +08:00
|
|
|
bcc.props.onEmptied()
|
|
|
|
|
|
|
|
expect(@composer.state.showbcc).toBe false
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['textFieldTo'].focus).toHaveBeenCalled()
|
|
|
|
expect(@composer.refs['textFieldCc']).not.toBeDefined()
|
|
|
|
expect(@composer.refs['textFieldBcc']).not.toBeDefined()
|
|
|
|
|
|
|
|
it "focuses on cc when bcc is emptied and cc field is available", ->
|
|
|
|
useDraft.call(@, cc: [u2], bcc: [u1])
|
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer.refs['textFieldTo'], 'focus')
|
|
|
|
spyOn(@composer.refs['textFieldCc'], 'focus')
|
|
|
|
spyOn(@composer.refs['textFieldBcc'], 'focus')
|
|
|
|
|
|
|
|
bcc = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, ParticipantsTextField, field: "bcc")[0]
|
2015-07-14 07:30:02 +08:00
|
|
|
@draft.bcc = []
|
2015-06-12 09:48:55 +08:00
|
|
|
bcc.props.onEmptied()
|
|
|
|
expect(@composer.state.showbcc).toBe false
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['textFieldTo'].focus).not.toHaveBeenCalled()
|
|
|
|
expect(@composer.refs['textFieldCc'].focus).toHaveBeenCalled()
|
|
|
|
expect(@composer.refs['textFieldBcc']).not.toBeDefined()
|
|
|
|
|
|
|
|
it "focuses on to when cc is emptied", ->
|
|
|
|
useDraft.call(@, cc: [u1], bcc: [u2])
|
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer.refs['textFieldTo'], 'focus')
|
|
|
|
spyOn(@composer.refs['textFieldCc'], 'focus')
|
|
|
|
spyOn(@composer.refs['textFieldBcc'], 'focus')
|
|
|
|
|
|
|
|
cc = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, ParticipantsTextField, field: "cc")[0]
|
2015-07-14 07:30:02 +08:00
|
|
|
@draft.cc = []
|
2015-06-12 09:48:55 +08:00
|
|
|
cc.props.onEmptied()
|
|
|
|
expect(@composer.state.showcc).toBe false
|
|
|
|
advanceClock(1000)
|
|
|
|
expect(@composer.refs['textFieldTo'].focus).toHaveBeenCalled()
|
|
|
|
expect(@composer.refs['textFieldCc']).not.toBeDefined()
|
|
|
|
expect(@composer.refs['textFieldBcc'].focus).not.toHaveBeenCalled()
|
|
|
|
|
2015-07-14 07:30:02 +08:00
|
|
|
describe "when participants are added during a draft update", ->
|
|
|
|
it "shows the cc fields and bcc fields to ensure participants are never hidden", ->
|
|
|
|
useDraft.call(@, cc: [], bcc: [])
|
|
|
|
makeComposer.call(@)
|
|
|
|
expect(@composer.state.showbcc).toBe(false)
|
|
|
|
expect(@composer.state.showcc).toBe(false)
|
|
|
|
|
|
|
|
# Simulate a change event fired by the DraftStoreProxy
|
|
|
|
@draft.cc = [u1]
|
|
|
|
@composer._onDraftChanged()
|
|
|
|
|
|
|
|
expect(@composer.state.showbcc).toBe(false)
|
|
|
|
expect(@composer.state.showcc).toBe(true)
|
|
|
|
|
|
|
|
# Simulate a change event fired by the DraftStoreProxy
|
|
|
|
@draft.bcc = [u2]
|
|
|
|
@composer._onDraftChanged()
|
|
|
|
expect(@composer.state.showbcc).toBe(true)
|
|
|
|
expect(@composer.state.showcc).toBe(true)
|
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "When sending a message", ->
|
|
|
|
beforeEach ->
|
2015-05-20 03:07:08 +08:00
|
|
|
spyOn(atom, "isMainWindow").andReturn true
|
2015-03-13 05:48:56 +08:00
|
|
|
remote = require('remote')
|
|
|
|
@dialog = remote.require('dialog')
|
|
|
|
spyOn(remote, "getCurrentWindow")
|
|
|
|
spyOn(@dialog, "showMessageBox")
|
|
|
|
spyOn(Actions, "sendDraft")
|
|
|
|
|
2015-08-04 04:06:28 +08:00
|
|
|
it "shows an error if there are no recipients", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
useDraft.call @, subject: "no recipients"
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
|
|
|
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
2015-08-04 04:06:28 +08:00
|
|
|
expect(dialogArgs.detail).toEqual("You need to provide one or more recipients before sending the message.")
|
|
|
|
expect(dialogArgs.buttons).toEqual ['Edit Message']
|
|
|
|
|
|
|
|
it "shows an error if a recipient is invalid", ->
|
|
|
|
useDraft.call @,
|
|
|
|
subject: 'hello world!'
|
|
|
|
to: [new Contact(email: 'lol', name: 'lol')]
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
|
|
|
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
|
|
|
expect(dialogArgs.detail).toEqual("lol is not a valid email address - please remove or edit it before sending.")
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(dialogArgs.buttons).toEqual ['Edit Message']
|
|
|
|
|
2015-06-18 03:29:21 +08:00
|
|
|
describe "empty body warning", ->
|
feat(signatures): Initial signature support
Summary:
- Draft Store extensions can now implement `prepareNewDraft` to have an opportunity to change a draft before it's displayed for the first time.
- When composers are torn down, they delete their draft if it is still pristine. This makes the behavior of closing unedited popout drafts the same as leaving unedited inline drafts.
- The DraftStoreProxy keeps the initial body of the draft *if* it started in a pristine state. This means "is the body empty" is just a simple == check, and it takes into account anything added to the body by extensions.
- Calling Actions.destroyDraft doesn't blow up anymore if the draft session can't be found. This was a bug and meant that you couldn't destroy drafts which hadn't been previously edited, and also meant that bad things(tm) happened when you called destroyDraft twice, which seemed like overkill.
- DestroyDraft task now exits gracefully when the draft cannot be found.
You can test this feature by adding the following to your config.cson:
```
signatures:
NAMESPACEID: "<br/><br/><div id=\"Signature\"><div id=\"divtagdefaultwrapper\" style=\"font-size:12pt; color:#000000; background-color:#FFFFFF; font-family:Calibri,Arial,Helvetica,sans-serif\"><p></p><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"450\" style=\"font-family:'Times New Roman'; table-layout:fixed\"><tbody><tr><td class=\"logo-td\" align=\"left\" valign=\"top\" width=\"76\"><p style=\"margin-bottom:10px; margin-right:10px; font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px\"><a href=\"http://www.nylas.com/\" class=\"clink logo-container\" style=\"text-decoration:none\"><img alt=\"Nylas\" border=\"0\" class=\"sig-logo\" height=\"80\" width=\"66\" style=\"-webkit-user-select: none;\" src=\"https://s3-us-west-2.amazonaws.com/nylas-static-assets/nylas-email-signature.png\"></a></p><p class=\"social-list\" style=\"font-size:0px; line-height:0; font-family:Helvetica,Arial,sans-serif\"></p></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"spacer-td\" width=\"16\" style=\"border-left-width:2px; border-left-style:solid; border-left-color:rgb(30,162,162)\"><img width=\"10\" style=\"-webkit-user-select: none;\" src=\"https://s3.amazonaws.com/htmlsig-assets/spacer.gif\"></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"content-td\" width=\"368\"><div class=\"content-pad\"><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; color:rgb(33,33,33); margin-bottom:10px\"><span class=\"txt signature_name-target sig-hide\" style=\"font-weight:bold; display:inline\">Gleb Polyakov</span> <span class=\"email-sep break\" style=\"display:inline\"><br></span><a class=\"link email signature_email-target sig-hide\" href=\"mailto:gleb@nylas.com\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">gleb@nylas.com</a><span class=\"signature_email-sep sep\" style=\"display:inline\"> / </span><span class=\"txt signature_mobilephone-target sig-hide\" style=\"display:inline\">404-786-4100</span></p><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; margin-bottom:10px\"><span class=\"txt signature_companyname-target sig-hide\" style=\"font-weight:bold; color:rgb(33,33,33); display:inline\">Nylas</span> <span class=\"company-sep break\" style=\"display:inline\"><br></span><span class=\"address-sep break\"></span><span class=\"address2-sep break\"></span><span class=\"website-sep break\"></span><a class=\"link signature_website-target sig-hide\" href=\"http://www.nylas.com/\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">http://www.nylas.com</a></p></div></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"><p class=\"txt signature_disclaimer-target\" style=\"font-family:Helvetica,Arial,sans-serif; color:rgb(33,33,33); font-size:9px; line-height:12px; margin-top:10px\"></p></td></tr></tbody></table><p></p></div></div>"
```
specs for draft store extension hooks, some draft store refactoring
Test Plan: Run a few new specs that make sure extensions are run
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1741
2015-07-15 03:20:06 +08:00
|
|
|
it "warns if the body of the email is still the pristine body", ->
|
|
|
|
pristineBody = "<head></head><body><br><br></body>"
|
2015-05-16 01:53:22 +08:00
|
|
|
|
2015-06-18 03:29:21 +08:00
|
|
|
useDraft.call @,
|
|
|
|
to: [u1]
|
|
|
|
subject: "Hello World"
|
feat(signatures): Initial signature support
Summary:
- Draft Store extensions can now implement `prepareNewDraft` to have an opportunity to change a draft before it's displayed for the first time.
- When composers are torn down, they delete their draft if it is still pristine. This makes the behavior of closing unedited popout drafts the same as leaving unedited inline drafts.
- The DraftStoreProxy keeps the initial body of the draft *if* it started in a pristine state. This means "is the body empty" is just a simple == check, and it takes into account anything added to the body by extensions.
- Calling Actions.destroyDraft doesn't blow up anymore if the draft session can't be found. This was a bug and meant that you couldn't destroy drafts which hadn't been previously edited, and also meant that bad things(tm) happened when you called destroyDraft twice, which seemed like overkill.
- DestroyDraft task now exits gracefully when the draft cannot be found.
You can test this feature by adding the following to your config.cson:
```
signatures:
NAMESPACEID: "<br/><br/><div id=\"Signature\"><div id=\"divtagdefaultwrapper\" style=\"font-size:12pt; color:#000000; background-color:#FFFFFF; font-family:Calibri,Arial,Helvetica,sans-serif\"><p></p><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"450\" style=\"font-family:'Times New Roman'; table-layout:fixed\"><tbody><tr><td class=\"logo-td\" align=\"left\" valign=\"top\" width=\"76\"><p style=\"margin-bottom:10px; margin-right:10px; font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px\"><a href=\"http://www.nylas.com/\" class=\"clink logo-container\" style=\"text-decoration:none\"><img alt=\"Nylas\" border=\"0\" class=\"sig-logo\" height=\"80\" width=\"66\" style=\"-webkit-user-select: none;\" src=\"https://s3-us-west-2.amazonaws.com/nylas-static-assets/nylas-email-signature.png\"></a></p><p class=\"social-list\" style=\"font-size:0px; line-height:0; font-family:Helvetica,Arial,sans-serif\"></p></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"spacer-td\" width=\"16\" style=\"border-left-width:2px; border-left-style:solid; border-left-color:rgb(30,162,162)\"><img width=\"10\" style=\"-webkit-user-select: none;\" src=\"https://s3.amazonaws.com/htmlsig-assets/spacer.gif\"></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"content-td\" width=\"368\"><div class=\"content-pad\"><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; color:rgb(33,33,33); margin-bottom:10px\"><span class=\"txt signature_name-target sig-hide\" style=\"font-weight:bold; display:inline\">Gleb Polyakov</span> <span class=\"email-sep break\" style=\"display:inline\"><br></span><a class=\"link email signature_email-target sig-hide\" href=\"mailto:gleb@nylas.com\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">gleb@nylas.com</a><span class=\"signature_email-sep sep\" style=\"display:inline\"> / </span><span class=\"txt signature_mobilephone-target sig-hide\" style=\"display:inline\">404-786-4100</span></p><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; margin-bottom:10px\"><span class=\"txt signature_companyname-target sig-hide\" style=\"font-weight:bold; color:rgb(33,33,33); display:inline\">Nylas</span> <span class=\"company-sep break\" style=\"display:inline\"><br></span><span class=\"address-sep break\"></span><span class=\"address2-sep break\"></span><span class=\"website-sep break\"></span><a class=\"link signature_website-target sig-hide\" href=\"http://www.nylas.com/\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">http://www.nylas.com</a></p></div></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"><p class=\"txt signature_disclaimer-target\" style=\"font-family:Helvetica,Arial,sans-serif; color:rgb(33,33,33); font-size:9px; line-height:12px; margin-top:10px\"></p></td></tr></tbody></table><p></p></div></div>"
```
specs for draft store extension hooks, some draft store refactoring
Test Plan: Run a few new specs that make sure extensions are run
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1741
2015-07-15 03:20:06 +08:00
|
|
|
body: pristineBody
|
2015-06-18 03:29:21 +08:00
|
|
|
makeComposer.call(@)
|
feat(signatures): Initial signature support
Summary:
- Draft Store extensions can now implement `prepareNewDraft` to have an opportunity to change a draft before it's displayed for the first time.
- When composers are torn down, they delete their draft if it is still pristine. This makes the behavior of closing unedited popout drafts the same as leaving unedited inline drafts.
- The DraftStoreProxy keeps the initial body of the draft *if* it started in a pristine state. This means "is the body empty" is just a simple == check, and it takes into account anything added to the body by extensions.
- Calling Actions.destroyDraft doesn't blow up anymore if the draft session can't be found. This was a bug and meant that you couldn't destroy drafts which hadn't been previously edited, and also meant that bad things(tm) happened when you called destroyDraft twice, which seemed like overkill.
- DestroyDraft task now exits gracefully when the draft cannot be found.
You can test this feature by adding the following to your config.cson:
```
signatures:
NAMESPACEID: "<br/><br/><div id=\"Signature\"><div id=\"divtagdefaultwrapper\" style=\"font-size:12pt; color:#000000; background-color:#FFFFFF; font-family:Calibri,Arial,Helvetica,sans-serif\"><p></p><table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"450\" style=\"font-family:'Times New Roman'; table-layout:fixed\"><tbody><tr><td class=\"logo-td\" align=\"left\" valign=\"top\" width=\"76\"><p style=\"margin-bottom:10px; margin-right:10px; font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px\"><a href=\"http://www.nylas.com/\" class=\"clink logo-container\" style=\"text-decoration:none\"><img alt=\"Nylas\" border=\"0\" class=\"sig-logo\" height=\"80\" width=\"66\" style=\"-webkit-user-select: none;\" src=\"https://s3-us-west-2.amazonaws.com/nylas-static-assets/nylas-email-signature.png\"></a></p><p class=\"social-list\" style=\"font-size:0px; line-height:0; font-family:Helvetica,Arial,sans-serif\"></p></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"spacer-td\" width=\"16\" style=\"border-left-width:2px; border-left-style:solid; border-left-color:rgb(30,162,162)\"><img width=\"10\" style=\"-webkit-user-select: none;\" src=\"https://s3.amazonaws.com/htmlsig-assets/spacer.gif\"></td><td align=\"left\" valign=\"top\" nowrap=\"nowrap\" class=\"content-td\" width=\"368\"><div class=\"content-pad\"><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; color:rgb(33,33,33); margin-bottom:10px\"><span class=\"txt signature_name-target sig-hide\" style=\"font-weight:bold; display:inline\">Gleb Polyakov</span> <span class=\"email-sep break\" style=\"display:inline\"><br></span><a class=\"link email signature_email-target sig-hide\" href=\"mailto:gleb@nylas.com\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">gleb@nylas.com</a><span class=\"signature_email-sep sep\" style=\"display:inline\"> / </span><span class=\"txt signature_mobilephone-target sig-hide\" style=\"display:inline\">404-786-4100</span></p><p style=\"font-family:Helvetica,Arial,sans-serif; font-size:14px; line-height:16px; margin-bottom:10px\"><span class=\"txt signature_companyname-target sig-hide\" style=\"font-weight:bold; color:rgb(33,33,33); display:inline\">Nylas</span> <span class=\"company-sep break\" style=\"display:inline\"><br></span><span class=\"address-sep break\"></span><span class=\"address2-sep break\"></span><span class=\"website-sep break\"></span><a class=\"link signature_website-target sig-hide\" href=\"http://www.nylas.com/\" style=\"color:rgb(30,162,162); text-decoration:none; display:inline\">http://www.nylas.com</a></p></div></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"></td></tr><tr><td colspan=\"3\"><p class=\"txt signature_disclaimer-target\" style=\"font-family:Helvetica,Arial,sans-serif; color:rgb(33,33,33); font-size:9px; line-height:12px; margin-top:10px\"></p></td></tr></tbody></table><p></p></div></div>"
```
specs for draft store extension hooks, some draft store refactoring
Test Plan: Run a few new specs that make sure extensions are run
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1741
2015-07-15 03:20:06 +08:00
|
|
|
|
|
|
|
spyOn(@composer._proxy, 'draftPristineBody').andCallFake -> pristineBody
|
|
|
|
|
2015-06-18 03:29:21 +08:00
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
|
|
|
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
2015-08-11 03:19:40 +08:00
|
|
|
expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']
|
2015-05-16 01:53:22 +08:00
|
|
|
|
2015-06-18 03:29:21 +08:00
|
|
|
it "does not warn if the body of the email is all quoted text, but the email is a forward", ->
|
|
|
|
useDraft.call @,
|
|
|
|
to: [u1]
|
|
|
|
subject: "Fwd: Hello World"
|
|
|
|
body: "<br><br><blockquote class='gmail_quote'>This is my quoted text!</blockquote>"
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "does not warn if the user has attached a file", ->
|
|
|
|
useDraft.call @,
|
|
|
|
to: [u1]
|
|
|
|
subject: "Hello World"
|
|
|
|
body: ""
|
|
|
|
files: [file]
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
2015-05-16 01:53:22 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
it "shows a warning if there's no subject", ->
|
|
|
|
useDraft.call @, to: [u1], subject: ""
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
|
|
|
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
2015-08-11 03:19:40 +08:00
|
|
|
expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']
|
2015-03-13 05:48:56 +08:00
|
|
|
|
|
|
|
it "doesn't show a warning if requirements are satisfied", ->
|
|
|
|
useFullDraft.apply(@); makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
describe "Checking for attachments", ->
|
|
|
|
warn = (body) ->
|
|
|
|
useDraft.call @, subject: "Subject", to: [u1], body: body
|
|
|
|
makeComposer.call(@); @composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
|
|
|
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
2015-08-11 03:19:40 +08:00
|
|
|
expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']
|
2015-03-13 05:48:56 +08:00
|
|
|
|
|
|
|
noWarn = (body) ->
|
2015-03-14 03:55:52 +08:00
|
|
|
useDraft.call @, subject: "Subject", to: [u1], body: body
|
2015-03-13 05:48:56 +08:00
|
|
|
makeComposer.call(@); @composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "warns", -> warn.call(@, "Check out the attached file")
|
|
|
|
it "warns", -> warn.call(@, "I've added an attachment")
|
|
|
|
it "warns", -> warn.call(@, "I'm going to attach the file")
|
2015-07-09 00:51:33 +08:00
|
|
|
it "warns", -> warn.call(@, "Hey attach me <blockquote class='gmail_quote'>sup</blockquote>")
|
2015-03-13 05:48:56 +08:00
|
|
|
|
|
|
|
it "doesn't warn", -> noWarn.call(@, "sup yo")
|
|
|
|
it "doesn't warn", -> noWarn.call(@, "Look at the file")
|
2015-07-09 00:51:33 +08:00
|
|
|
it "doesn't warn", -> noWarn.call(@, "Hey there <blockquote class='gmail_quote'>attach</blockquote>")
|
2015-03-13 05:48:56 +08:00
|
|
|
|
|
|
|
it "doesn't show a warning if you've attached a file", ->
|
|
|
|
useDraft.call @,
|
|
|
|
subject: "Subject"
|
|
|
|
to: [u1]
|
|
|
|
body: "Check out attached file"
|
2015-06-18 03:29:21 +08:00
|
|
|
files: [file]
|
2015-03-13 05:48:56 +08:00
|
|
|
makeComposer.call(@); @composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "bypasses the warning if force bit is set", ->
|
|
|
|
useDraft.call @, to: [u1], subject: ""
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft(force: true)
|
|
|
|
expect(Actions.sendDraft).toHaveBeenCalled()
|
|
|
|
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "sends when you click the send button", ->
|
|
|
|
useFullDraft.apply(@); makeComposer.call(@)
|
2015-04-25 02:33:10 +08:00
|
|
|
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
2015-03-13 05:48:56 +08:00
|
|
|
ReactTestUtils.Simulate.click sendBtn
|
2015-08-29 02:12:53 +08:00
|
|
|
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(Actions.sendDraft.calls.length).toBe 1
|
|
|
|
|
|
|
|
it "doesn't send twice if you double click", ->
|
|
|
|
useFullDraft.apply(@); makeComposer.call(@)
|
2015-04-25 02:33:10 +08:00
|
|
|
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
2015-03-13 05:48:56 +08:00
|
|
|
ReactTestUtils.Simulate.click sendBtn
|
2015-07-13 22:50:27 +08:00
|
|
|
@isSending.state = true
|
|
|
|
DraftStore.trigger()
|
2015-03-13 05:48:56 +08:00
|
|
|
ReactTestUtils.Simulate.click sendBtn
|
2015-08-29 02:12:53 +08:00
|
|
|
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(Actions.sendDraft.calls.length).toBe 1
|
|
|
|
|
|
|
|
describe "when sending a message with keyboard inputs", ->
|
|
|
|
beforeEach ->
|
|
|
|
useFullDraft.apply(@)
|
|
|
|
makeComposer.call(@)
|
|
|
|
spyOn(@composer, "_sendDraft")
|
2015-05-22 05:41:30 +08:00
|
|
|
NylasTestUtils.loadKeymap("internal_packages/composer/keymaps/composer")
|
2015-03-13 05:48:56 +08:00
|
|
|
|
|
|
|
it "sends the draft on cmd-enter", ->
|
2015-05-20 07:06:59 +08:00
|
|
|
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(@composer._sendDraft).toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "does not send the draft on enter if the button isn't in focus", ->
|
2015-05-20 07:06:59 +08:00
|
|
|
NylasTestUtils.keyPress("enter", React.findDOMNode(@composer))
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(@composer._sendDraft).not.toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "sends the draft on enter when the button is in focus", ->
|
|
|
|
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
2015-05-20 07:06:59 +08:00
|
|
|
NylasTestUtils.keyPress("enter", React.findDOMNode(sendBtn))
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(@composer._sendDraft).toHaveBeenCalled()
|
|
|
|
|
|
|
|
it "doesn't let you send twice", ->
|
|
|
|
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
2015-05-20 07:06:59 +08:00
|
|
|
NylasTestUtils.keyPress("enter", React.findDOMNode(sendBtn))
|
2015-03-13 05:48:56 +08:00
|
|
|
expect(@composer._sendDraft).toHaveBeenCalled()
|
|
|
|
|
2015-07-18 07:36:28 +08:00
|
|
|
describe "drag and drop", ->
|
|
|
|
beforeEach ->
|
|
|
|
useDraft.call @,
|
|
|
|
to: [u1]
|
|
|
|
subject: "Hello World"
|
|
|
|
body: ""
|
|
|
|
files: [file]
|
|
|
|
makeComposer.call(@)
|
|
|
|
|
|
|
|
describe "_shouldAcceptDrop", ->
|
|
|
|
it "should return true if the event is carrying native files", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
files:[{'pretend':'imafile'}]
|
|
|
|
types:[]
|
|
|
|
expect(@composer._shouldAcceptDrop(event)).toBe(true)
|
|
|
|
|
|
|
|
it "should return true if the event is carrying a non-native file URL not on the draft", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
files:[]
|
|
|
|
types:['text/uri-list']
|
|
|
|
spyOn(@composer, '_nonNativeFilePathForDrop').andReturn("file://one-file")
|
|
|
|
spyOn(FileUploadStore, 'linkedUpload').andReturn({filePath: "file://other-file"})
|
|
|
|
|
|
|
|
expect(@composer.state.files.length).toBe(1)
|
|
|
|
expect(@composer._shouldAcceptDrop(event)).toBe(true)
|
|
|
|
|
|
|
|
it "should return false if the event is carrying a non-native file URL already on the draft", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
files:[]
|
|
|
|
types:['text/uri-list']
|
|
|
|
spyOn(@composer, '_nonNativeFilePathForDrop').andReturn("file://one-file")
|
|
|
|
spyOn(FileUploadStore, 'linkedUpload').andReturn({filePath: "file://one-file"})
|
|
|
|
|
|
|
|
expect(@composer.state.files.length).toBe(1)
|
|
|
|
expect(@composer._shouldAcceptDrop(event)).toBe(false)
|
|
|
|
|
|
|
|
it "should return false otherwise", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
files:[]
|
|
|
|
types:['text/plain']
|
|
|
|
expect(@composer._shouldAcceptDrop(event)).toBe(false)
|
|
|
|
|
|
|
|
describe "_nonNativeFilePathForDrop", ->
|
|
|
|
it "should return a path in the text/nylas-file-url data", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
types: ['text/nylas-file-url']
|
|
|
|
getData: -> "image/png:test.png:file:///Users/bengotow/Desktop/test.png"
|
|
|
|
expect(@composer._nonNativeFilePathForDrop(event)).toBe("/Users/bengotow/Desktop/test.png")
|
|
|
|
|
|
|
|
it "should return a path in the text/uri-list data", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
types: ['text/uri-list']
|
|
|
|
getData: -> "file:///Users/bengotow/Desktop/test.png"
|
|
|
|
expect(@composer._nonNativeFilePathForDrop(event)).toBe("/Users/bengotow/Desktop/test.png")
|
|
|
|
|
|
|
|
it "should return null otherwise", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
types: ['text/plain']
|
|
|
|
getData: -> "Hello world"
|
|
|
|
expect(@composer._nonNativeFilePathForDrop(event)).toBe(null)
|
|
|
|
|
|
|
|
it "should urldecode the contents of the text/uri-list field", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
types: ['text/uri-list']
|
|
|
|
getData: -> "file:///Users/bengotow/Desktop/Screen%20shot.png"
|
|
|
|
expect(@composer._nonNativeFilePathForDrop(event)).toBe("/Users/bengotow/Desktop/Screen shot.png")
|
|
|
|
|
|
|
|
it "should return null if text/uri-list contains a non-file path", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
types: ['text/uri-list']
|
|
|
|
getData: -> "http://apple.com"
|
|
|
|
expect(@composer._nonNativeFilePathForDrop(event)).toBe(null)
|
|
|
|
|
|
|
|
it "should return null if text/nylas-file-url contains a non-file path", ->
|
|
|
|
event =
|
|
|
|
dataTransfer:
|
|
|
|
types: ['text/nylas-file-url']
|
|
|
|
getData: -> "application/json:filename.json:undefined"
|
|
|
|
expect(@composer._nonNativeFilePathForDrop(event)).toBe(null)
|
|
|
|
|
2015-05-20 07:12:39 +08:00
|
|
|
describe "when scrolling to track your cursor", ->
|
|
|
|
it "it tracks when you're at the end of the text", ->
|
|
|
|
|
|
|
|
it "it doesn't track when typing in the middle of the body", ->
|
|
|
|
|
|
|
|
it "it doesn't track when typing in the middle of the body", ->
|
2015-03-13 05:48:56 +08:00
|
|
|
|
|
|
|
describe "When composing a new message", ->
|
|
|
|
it "Can add someone in the to field", ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
it "Can add someone in the cc field", ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
it "Can add someone in the bcc field", ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "When replying to a message", ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "When replying all to a message", ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "When forwarding a message", ->
|
fix(drafts): Various improvements and fixes to drafts, draft state management
Summary:
This diff contains a few major changes:
1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario:
- View thread with draft, edit draft
- Move to another thread
- Move back to thread with draft
- Move to another thread. Notice that one or more messages from thread with draft are still there.
There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great.
2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here:
- In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI.
- Previously, when you added a contact to To/CC/BCC, this happened:
<input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates
Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state.
To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source!
This diff includes a few minor fixes as well:
1. Draft.state is gone—use Message.object = draft instead
2. String model attributes should never be null
3. Pre-send checks that can cancel draft send
4. Put the entire curl history and task queue into feedback reports
5. Cache localIds for extra speed
6. Move us up to latest React
Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet
Reviewers: evan
Reviewed By: evan
Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
|
|
|
|
2015-03-13 05:48:56 +08:00
|
|
|
describe "When changing the subject of a message", ->
|
2015-06-12 02:52:49 +08:00
|
|
|
|
|
|
|
describe "A draft with files (attachments) and uploads", ->
|
|
|
|
beforeEach ->
|
2015-07-16 04:15:05 +08:00
|
|
|
@file1 = new File
|
2015-06-12 02:52:49 +08:00
|
|
|
id: "f_1"
|
|
|
|
filename: "f1.pdf"
|
|
|
|
size: 1230
|
|
|
|
|
2015-07-16 04:15:05 +08:00
|
|
|
@file2 = new File
|
2015-06-12 02:52:49 +08:00
|
|
|
id: "f_2"
|
|
|
|
filename: "f2.jpg"
|
|
|
|
size: 4560
|
|
|
|
|
2015-07-16 04:15:05 +08:00
|
|
|
@file3 = new File
|
2015-06-12 02:52:49 +08:00
|
|
|
id: "f_3"
|
|
|
|
filename: "f3.png"
|
|
|
|
size: 7890
|
|
|
|
|
|
|
|
@up1 =
|
2015-07-16 07:52:43 +08:00
|
|
|
uploadTaskId: 4
|
2015-08-29 02:12:53 +08:00
|
|
|
messageClientId: DRAFT_CLIENT_ID
|
2015-06-12 02:52:49 +08:00
|
|
|
filePath: "/foo/bar/f4.bmp"
|
|
|
|
fileName: "f4.bmp"
|
|
|
|
fileSize: 1024
|
|
|
|
|
|
|
|
@up2 =
|
2015-07-16 07:52:43 +08:00
|
|
|
uploadTaskId: 5
|
2015-08-29 02:12:53 +08:00
|
|
|
messageClientId: DRAFT_CLIENT_ID
|
2015-06-12 02:52:49 +08:00
|
|
|
filePath: "/foo/bar/f5.zip"
|
|
|
|
fileName: "f5.zip"
|
|
|
|
fileSize: 1024
|
|
|
|
|
|
|
|
spyOn(Actions, "fetchFile")
|
feat(offline-mode, undo-redo): Tasks handle network errors better and retry, undo/redo based on tasks
Summary:
This diff does a couple things:
- Undo redo with a new undo/redo store that maintains it's own queue of undo/redo tasks. This queue is separate from the TaskQueue because not all tasks should be considered for undo history! Right now just the AddRemoveTagsTask is undoable.
- NylasAPI.makeRequest now returns a promise which resolves with the result or rejects with an error. For things that still need them, there's still `success` and `error` callbacks. I also added `started:(req) ->` which allows you to get the underlying request.
- Aborting a NylasAPI request now makes it call it's error callback / promise reject.
- You can now run code after perform local has completed using this syntax:
```
task = new AddRemoveTagsTask(focused, ['archive'], ['inbox'])
task.waitForPerformLocal().then ->
Actions.setFocus(collection: 'thread', item: nextFocus)
Actions.setCursorPosition(collection: 'thread', item: nextKeyboard)
Actions.queueTask(task)
```
- In specs, you can now use `advanceClock` to get through a Promise.then/catch/finally. Turns out it was using something low level and not using setTimeout(0).
- The TaskQueue uses promises better and defers a lot of the complexity around queueState for performLocal/performRemote to a task subclass called APITask. APITask implements "perform" and breaks it into "performLocal" and "performRemote".
- All tasks either resolve or reject. They're always removed from the queue, unless they resolve with Task.Status.Retry, which means they internally did a .catch (err) => Promise.resolve(Task.Status.Retry) and they want to be run again later.
- API tasks retry until they succeed or receive a NylasAPI.PermanentErrorCode (400,404,500), in which case they revert and finish.
- The AddRemoveTags Task can now take more than one thread! This is super cool because you can undo/redo a bulk action and also because we'll probably have a bulk tag modification API endpoint soon.
Getting undo / redo working revealed that the thread versioning system we built isn't working because the server was incrementing things by more than 1 at a time. Now we count the number of unresolved "optimistic" changes we've made to a given model, and only accept the server's version of it once the number of optimistic changes is back at zero.
Known Issues:
- AddRemoveTagsTasks aren't dependent on each other, so if you (undo/redo x lots) and then come back online, all the tasks try to add / remove all the tags at the same time. To fix this we can either allow the tasks to be merged together into a minimal set or make them block on each other.
- When Offline, you still get errors in the console for GET requests. Need to catch these and display an offline status bar.
- The metadata tasks haven't been updated yet to the new API. Wanted to get it reviewed first!
Test Plan: All the tests still pass!
Reviewers: evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1694
2015-07-08 01:38:53 +08:00
|
|
|
spyOn(FileUploadStore, "linkedUpload").andReturn null
|
2015-06-12 02:52:49 +08:00
|
|
|
spyOn(FileUploadStore, "uploadsForMessage").andReturn [@up1, @up2]
|
|
|
|
|
|
|
|
useDraft.call @, files: [@file1, @file2]
|
|
|
|
makeComposer.call @
|
|
|
|
|
2015-07-18 08:13:16 +08:00
|
|
|
it 'starts fetching attached files', ->
|
|
|
|
waitsFor ->
|
|
|
|
Actions.fetchFile.callCount == 1
|
|
|
|
runs ->
|
|
|
|
expect(Actions.fetchFile).toHaveBeenCalled()
|
|
|
|
expect(Actions.fetchFile.calls.length).toBe(1)
|
|
|
|
expect(Actions.fetchFile.calls[0].args[0]).toBe @file2
|
2015-06-12 02:52:49 +08:00
|
|
|
|
2015-07-16 04:15:05 +08:00
|
|
|
it 'injects an Attachment component for non image files', ->
|
|
|
|
els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, InjectedComponent, matching: {role: "Attachment"})
|
2015-06-12 02:52:49 +08:00
|
|
|
expect(els.length).toBe 1
|
|
|
|
|
2015-07-16 04:15:05 +08:00
|
|
|
it 'injects an Attachment:Image component for image files', ->
|
|
|
|
els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, InjectedComponent, matching: {role: "Attachment:Image"})
|
2015-06-12 02:52:49 +08:00
|
|
|
expect(els.length).toBe 1
|
|
|
|
|
2015-07-13 22:50:27 +08:00
|
|
|
describe "when the DraftStore `isSending` isn't stubbed out", ->
|
|
|
|
beforeEach ->
|
feat(work): Create the "Work" window, move TaskQueue, Nylas sync workers
Summary:
Move sync workers and Edgehill token checks to work window
Move the task queue and database setup to the work window
Move ContactStore background refresh to work window
Store the task queue in the database
WIP
The TaskQueue now puts tasks in the database instead of in a file, which also means it can be observed
Move all delta sync and initial sync to a package, make NylasSyncStore which exposes read-only sync state
DraftStore no longer reads task status. Once you set the "sending" bit on a draft, it never gets unset. But that's fine actually.
If your package lists windowTypes, you *only* get loaded in those windowTypes. If you specify no windowTypes, you get loaded in the root window.
This means that onboarding, worker-ui, worker-sync, etc. no longer get loaded into the main window
ActivitySidebar has a special little store that observes the task queue since it's no longer in the window
Move "toggle component regions" / "toggle react remote" to the Developer menu
Move sync worker specs, update draft store specs to not rely on TaskQueue at all
Test Plan: Run existing tests, all pass
Reviewers: dillon, evan
Reviewed By: evan
Differential Revision: https://phab.nylas.com/D1936
2015-08-28 07:39:40 +08:00
|
|
|
DraftStore._draftsSending = {}
|
2015-07-13 22:50:27 +08:00
|
|
|
|
|
|
|
it "doesn't send twice in a popout", ->
|
|
|
|
spyOn(Actions, "queueTask")
|
|
|
|
spyOn(Actions, "sendDraft").andCallThrough()
|
|
|
|
useFullDraft.call(@)
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft.calls.length).toBe 1
|
|
|
|
|
|
|
|
it "doesn't send twice in the main window", ->
|
|
|
|
spyOn(Actions, "queueTask")
|
|
|
|
spyOn(Actions, "sendDraft").andCallThrough()
|
|
|
|
spyOn(atom, "isMainWindow").andReturn true
|
|
|
|
useFullDraft.call(@)
|
|
|
|
makeComposer.call(@)
|
|
|
|
@composer._sendDraft()
|
|
|
|
@composer._sendDraft()
|
|
|
|
expect(Actions.sendDraft.calls.length).toBe 1
|