mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-04-14 08:20:11 +08:00
refactor(db): change ID system to have clientIDs and serverIDs
Summary: Major ID refactor Test Plan: edgehill --test Reviewers: bengotow, dillon Differential Revision: https://phab.nylas.com/D1946
This commit is contained in:
parent
9d995ded67
commit
f8c5f7b967
84 changed files with 654 additions and 884 deletions
build
exports
internal_packages
attachments/lib
composer
events/lib
message-list
lib
spec
message-templates
thread-list
undo-redo/lib
worker-sync/spec
worker-ui/lib
spec-nylas
action-bridge-spec.coffee
components
fixtures
models
stores
contact-store-spec.coffeedatabase-setup-query-builder-spec.coffeedatabase-store-spec.coffeedraft-store-spec.coffeeevent-store-spec.coffeefile-download-store-spec.coffeefile-upload-store-spec.coffeefocused-category-store-spec.coffeefocused-contacts-store-spec.coffeetask-queue-spec.coffeeunread-count-store-spec.coffee
task-spec.coffeetasks
spec
fixtures
spec-helper.coffeesrc
browser
components
flux
static/package-template
|
@ -208,9 +208,9 @@ module.exports = (grunt) ->
|
|||
'exports/**/*.coffee'
|
||||
'src/**/*.coffee'
|
||||
'src/**/*.cjsx'
|
||||
'spec/*.coffee'
|
||||
'spec-nylas/*.cjsx'
|
||||
'spec-nylas/*.coffee'
|
||||
'spec/**/*.coffee'
|
||||
'spec-nylas/**/*.cjsx'
|
||||
'spec-nylas/**/*.coffee'
|
||||
]
|
||||
|
||||
coffeelint:
|
||||
|
@ -229,9 +229,13 @@ module.exports = (grunt) ->
|
|||
'build/Gruntfile.coffee'
|
||||
]
|
||||
test: [
|
||||
'spec/*.coffee'
|
||||
'spec-nylas/*.cjsx'
|
||||
'spec-nylas/*.coffee'
|
||||
'spec/**/*.coffee'
|
||||
'spec-nylas/**/*.cjsx'
|
||||
'spec-nylas/**/*.coffee'
|
||||
]
|
||||
static: [
|
||||
'static/**/*.coffee'
|
||||
'static/**/*.cjsx'
|
||||
]
|
||||
target:
|
||||
grunt.option("target")?.split(" ") or []
|
||||
|
|
|
@ -18,4 +18,4 @@ module.exports = (grunt) ->
|
|||
done(new Error("#{f} contains a bad require including an coffee / cjsx / jsx extension. Remove the extension!"))
|
||||
return
|
||||
|
||||
done(null)
|
||||
done(null)
|
||||
|
|
|
@ -60,7 +60,6 @@ class NylasExports
|
|||
@require "Contact", 'flux/models/contact'
|
||||
@require "Calendar", 'flux/models/calendar'
|
||||
@require "Metadata", 'flux/models/metadata'
|
||||
@require "LocalLink", 'flux/models/local-link'
|
||||
@require "DatabaseObjectRegistry", "database-object-registry"
|
||||
|
||||
# Exported so 3rd party packages can subclass Model
|
||||
|
|
|
@ -13,7 +13,7 @@ class AttachmentComponent extends React.Component
|
|||
download: React.PropTypes.object
|
||||
removable: React.PropTypes.bool
|
||||
targetPath: React.PropTypes.string
|
||||
messageLocalId: React.PropTypes.string
|
||||
messageClientId: React.PropTypes.string
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = progressPercent: 0
|
||||
|
@ -79,7 +79,7 @@ class AttachmentComponent extends React.Component
|
|||
_onClickRemove: (event) =>
|
||||
Actions.removeFile
|
||||
file: @props.file
|
||||
messageLocalId: @props.messageLocalId
|
||||
messageClientId: @props.messageClientId
|
||||
event.stopPropagation() # Prevent 'onClickView'
|
||||
|
||||
_onClickDownload: (event) =>
|
||||
|
|
|
@ -37,7 +37,7 @@ class ComposerView extends React.Component
|
|||
@containerRequired: false
|
||||
|
||||
@propTypes:
|
||||
localId: React.PropTypes.string.isRequired
|
||||
draftClientId: React.PropTypes.string.isRequired
|
||||
|
||||
# Either "inline" or "fullwindow"
|
||||
mode: React.PropTypes.string
|
||||
|
@ -65,10 +65,10 @@ class ComposerView extends React.Component
|
|||
showbcc: false
|
||||
showsubject: false
|
||||
showQuotedText: false
|
||||
uploads: FileUploadStore.uploadsForMessage(@props.localId) ? []
|
||||
uploads: FileUploadStore.uploadsForMessage(@props.draftClientId) ? []
|
||||
|
||||
componentWillMount: =>
|
||||
@_prepareForDraft(@props.localId)
|
||||
@_prepareForDraft(@props.draftClientId)
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
|
@ -113,24 +113,24 @@ class ComposerView extends React.Component
|
|||
|
||||
componentWillReceiveProps: (newProps) =>
|
||||
@_ignoreNextTrigger = false
|
||||
if newProps.localId isnt @props.localId
|
||||
# When we're given a new draft localId, we have to stop listening to our
|
||||
if newProps.draftClientId isnt @props.draftClientId
|
||||
# When we're given a new draft draftClientId, we have to stop listening to our
|
||||
# current DraftStoreProxy, create a new one and listen to that. The simplest
|
||||
# way to do this is to just re-call registerListeners.
|
||||
@_teardownForDraft()
|
||||
@_prepareForDraft(newProps.localId)
|
||||
@_prepareForDraft(newProps.draftClientId)
|
||||
|
||||
_prepareForDraft: (localId) =>
|
||||
_prepareForDraft: (draftClientId) =>
|
||||
@unlisteners = []
|
||||
return unless localId
|
||||
return unless draftClientId
|
||||
|
||||
# UndoManager must be ready before we call _onDraftChanged for the first time
|
||||
@undoManager = new UndoManager
|
||||
DraftStore.sessionForLocalId(localId).then(@_setupSession)
|
||||
DraftStore.sessionForClientId(draftClientId).then(@_setupSession)
|
||||
|
||||
_setupSession: (proxy) =>
|
||||
return if @_unmounted
|
||||
return unless proxy.draftLocalId is @props.localId
|
||||
return unless proxy.draftClientId is @props.draftClientId
|
||||
@_proxy = proxy
|
||||
@_preloadImages(@_proxy.draft()?.files)
|
||||
@unlisteners.push @_proxy.listen(@_onDraftChanged)
|
||||
|
@ -317,12 +317,12 @@ class ComposerView extends React.Component
|
|||
tabIndex="109" />
|
||||
|
||||
_renderFooterRegions: =>
|
||||
return <div></div> unless @props.localId
|
||||
return <div></div> unless @props.draftClientId
|
||||
|
||||
<div className="composer-footer-region">
|
||||
<InjectedComponentSet
|
||||
matching={role: "Composer:Footer"}
|
||||
exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}/>
|
||||
exposedProps={draftClientId:@props.draftClientId, threadId: @props.threadId}/>
|
||||
</div>
|
||||
|
||||
_renderAttachments: ->
|
||||
|
@ -347,7 +347,7 @@ class ComposerView extends React.Component
|
|||
file: file
|
||||
removable: true
|
||||
targetPath: targetPath
|
||||
messageLocalId: @props.localId
|
||||
messageClientId: @props.draftClientId
|
||||
|
||||
if role is "Attachment"
|
||||
className = "file-wrap"
|
||||
|
@ -397,14 +397,14 @@ class ComposerView extends React.Component
|
|||
_.compact(uploads.concat(@state.files))
|
||||
|
||||
_onFileUploadStoreChange: =>
|
||||
@setState uploads: FileUploadStore.uploadsForMessage(@props.localId)
|
||||
@setState uploads: FileUploadStore.uploadsForMessage(@props.draftClientId)
|
||||
|
||||
_renderActionsRegion: =>
|
||||
return <div></div> unless @props.localId
|
||||
return <div></div> unless @props.draftClientId
|
||||
|
||||
<InjectedComponentSet className="composer-action-bar-content"
|
||||
matching={role: "Composer:ActionButton"}
|
||||
exposedProps={draftLocalId:@props.localId, threadId: @props.threadId}>
|
||||
exposedProps={draftClientId:@props.draftClientId, threadId: @props.threadId}>
|
||||
|
||||
<button className="btn btn-toolbar btn-trash" style={order: 100}
|
||||
data-tooltip="Delete draft"
|
||||
|
@ -532,14 +532,14 @@ class ComposerView extends React.Component
|
|||
_onDrop: (e) =>
|
||||
# Accept drops of real files from other applications
|
||||
for file in e.dataTransfer.files
|
||||
Actions.attachFilePath({path: file.path, messageLocalId: @props.localId})
|
||||
Actions.attachFilePath({path: file.path, messageClientId: @props.draftClientId})
|
||||
|
||||
# Accept drops from attachment components / images within the app
|
||||
if (uri = @_nonNativeFilePathForDrop(e))
|
||||
Actions.attachFilePath({path: uri, messageLocalId: @props.localId})
|
||||
Actions.attachFilePath({path: uri, messageClientId: @props.draftClientId})
|
||||
|
||||
_onFilePaste: (path) =>
|
||||
Actions.attachFilePath({path: path, messageLocalId: @props.localId})
|
||||
Actions.attachFilePath({path: path, messageClientId: @props.draftClientId})
|
||||
|
||||
_onChangeParticipants: (changes={}) =>
|
||||
@_addToProxy(changes)
|
||||
|
@ -589,7 +589,7 @@ class ComposerView extends React.Component
|
|||
@_saveToHistory(selections) unless source.fromUndoManager
|
||||
|
||||
_popoutComposer: =>
|
||||
Actions.composePopoutDraft @props.localId
|
||||
Actions.composePopoutDraft @props.draftClientId
|
||||
|
||||
_sendDraft: (options = {}) =>
|
||||
return unless @_proxy
|
||||
|
@ -598,7 +598,7 @@ class ComposerView extends React.Component
|
|||
# immediately and synchronously updated as soon as this function
|
||||
# fires. Since `setState` is asynchronous, if we used that as our only
|
||||
# check, then we might get a false reading.
|
||||
return if DraftStore.isSendingDraft(@props.localId)
|
||||
return if DraftStore.isSendingDraft(@props.draftClientId)
|
||||
|
||||
draft = @_proxy.draft()
|
||||
remote = require('remote')
|
||||
|
@ -651,17 +651,17 @@ class ComposerView extends React.Component
|
|||
@_sendDraft({force: true})
|
||||
return
|
||||
|
||||
Actions.sendDraft(@props.localId)
|
||||
Actions.sendDraft(@props.draftClientId)
|
||||
|
||||
_mentionsAttachment: (body) =>
|
||||
body = QuotedHTMLParser.removeQuotedHTML(body.toLowerCase().trim())
|
||||
return body.indexOf("attach") >= 0
|
||||
|
||||
_destroyDraft: =>
|
||||
Actions.destroyDraft(@props.localId)
|
||||
Actions.destroyDraft(@props.draftClientId)
|
||||
|
||||
_attachFile: =>
|
||||
Actions.attachFile({messageLocalId: @props.localId})
|
||||
Actions.attachFile({messageClientId: @props.draftClientId})
|
||||
|
||||
_showAndFocusBcc: =>
|
||||
@setState {showbcc: true}
|
||||
|
@ -734,7 +734,7 @@ class ComposerView extends React.Component
|
|||
|
||||
_deleteDraftIfEmpty: =>
|
||||
return unless @_proxy
|
||||
if @_proxy.draft().pristine then Actions.destroyDraft(@props.localId)
|
||||
if @_proxy.draft().pristine then Actions.destroyDraft(@props.draftClientId)
|
||||
|
||||
|
||||
module.exports = ComposerView
|
||||
|
|
|
@ -28,7 +28,7 @@ class ComposerWithWindowProps extends React.Component
|
|||
|
||||
render: ->
|
||||
<div className="composer-full-window">
|
||||
<ComposerView mode="fullwindow" localId={@state.draftLocalId} />
|
||||
<ComposerView mode="fullwindow" draftClientId={@state.draftClientId} />
|
||||
</div>
|
||||
|
||||
_showInitialErrorDialog: (msg) ->
|
||||
|
|
|
@ -30,8 +30,6 @@ u5 = new Contact(name: "Ben Gotow", email: "ben@nylas.com")
|
|||
file = new File(id: 'file_1_id', filename: 'a.png', contentType: 'image/png', size: 10, object: "file")
|
||||
|
||||
users = [u1, u2, u3, u4, u5]
|
||||
AccountStore._current = new Account(
|
||||
{name: u1.name, provider: "inbox", emailAddress: u1.email})
|
||||
|
||||
reactStub = (className) ->
|
||||
React.createClass({render: -> <div className={className}>{@props.children}</div>})
|
||||
|
@ -45,11 +43,11 @@ passThroughStub = (props={}) ->
|
|||
React.createClass
|
||||
render: -> <div {...props}>{props.children}</div>
|
||||
|
||||
draftStoreProxyStub = (localId, returnedDraft) ->
|
||||
draftStoreProxyStub = (draftClientId, returnedDraft) ->
|
||||
listen: -> ->
|
||||
draft: -> (returnedDraft ? new Message(draft: true))
|
||||
draftPristineBody: -> null
|
||||
draftLocalId: localId
|
||||
draftClientId: draftClientId
|
||||
cleanup: ->
|
||||
changes:
|
||||
add: ->
|
||||
|
@ -72,27 +70,21 @@ ComposerView = proxyquire "../lib/composer-view",
|
|||
DraftStore: DraftStore
|
||||
|
||||
beforeEach ->
|
||||
# spyOn(ComponentRegistry, "findComponentsMatching").andCallFake (matching) ->
|
||||
# return passThroughStub
|
||||
# spyOn(ComponentRegistry, "showComponentRegions").andReturn true
|
||||
|
||||
# The AccountStore isn't set yet in the new window, populate it first.
|
||||
AccountStore.populateItems().then ->
|
||||
new Promise (resolve, reject) ->
|
||||
draft = new Message
|
||||
from: [AccountStore.current().me()]
|
||||
date: (new Date)
|
||||
draft: true
|
||||
accountId: AccountStore.current().id
|
||||
draft = new Message
|
||||
from: [AccountStore.current().me()]
|
||||
date: (new Date)
|
||||
draft: true
|
||||
accountId: AccountStore.current().id
|
||||
|
||||
DatabaseStore.persistModel(draft).then ->
|
||||
DatabaseStore.localIdForModel(draft).then(resolve).catch(reject)
|
||||
.catch(reject)
|
||||
DatabaseStore.persistModel(draft).then ->
|
||||
return draft
|
||||
|
||||
describe "A blank composer view", ->
|
||||
beforeEach ->
|
||||
@composer = ReactTestUtils.renderIntoDocument(
|
||||
<ComposerView localId="test123" />
|
||||
<ComposerView draftClientId="test123" />
|
||||
)
|
||||
@composer.setState
|
||||
body: ""
|
||||
|
@ -110,23 +102,23 @@ describe "A blank composer view", ->
|
|||
# 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.
|
||||
DRAFT_LOCAL_ID = "local-123"
|
||||
DRAFT_CLIENT_ID = "local-123"
|
||||
useDraft = (draftAttributes={}) ->
|
||||
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
|
||||
draft = @draft
|
||||
proxy = draftStoreProxyStub(DRAFT_LOCAL_ID, @draft)
|
||||
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
||||
|
||||
|
||||
spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
|
||||
@_prepareForDraft(DRAFT_LOCAL_ID)
|
||||
@_prepareForDraft(DRAFT_CLIENT_ID)
|
||||
@_setupSession(proxy)
|
||||
|
||||
# Normally when sessionForLocalId resolves, it will call `_setupSession`
|
||||
# Normally when sessionForClientId resolves, it will call `_setupSession`
|
||||
# and pass the new session proxy. However, in our faked
|
||||
# `componentWillMount`, we manually call sessionForLocalId to make this
|
||||
# `componentWillMount`, we manually call sessionForClientId to make this
|
||||
# part of the test synchronous. We need to make the `then` block of the
|
||||
# sessionForLocalId do nothing so `_setupSession` is not called twice!
|
||||
spyOn(DraftStore, "sessionForLocalId").andCallFake ->
|
||||
# sessionForClientId do nothing so `_setupSession` is not called twice!
|
||||
spyOn(DraftStore, "sessionForClientId").andCallFake ->
|
||||
then: ->
|
||||
|
||||
useFullDraft = ->
|
||||
|
@ -141,7 +133,7 @@ useFullDraft = ->
|
|||
|
||||
makeComposer = ->
|
||||
@composer = ReactTestUtils.renderIntoDocument(
|
||||
<ComposerView localId={DRAFT_LOCAL_ID} />
|
||||
<ComposerView draftClientId={DRAFT_CLIENT_ID} />
|
||||
)
|
||||
|
||||
describe "populated composer", ->
|
||||
|
@ -245,9 +237,9 @@ describe "populated composer", ->
|
|||
describe "if the draft has not yet loaded", ->
|
||||
it "should set _focusOnUpdate and focus after the next render", ->
|
||||
@draft = new Message(draft: true, body: "")
|
||||
proxy = draftStoreProxyStub(DRAFT_LOCAL_ID, @draft)
|
||||
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
|
||||
proxyResolve = null
|
||||
spyOn(DraftStore, "sessionForLocalId").andCallFake ->
|
||||
spyOn(DraftStore, "sessionForClientId").andCallFake ->
|
||||
new Promise (resolve, reject) ->
|
||||
proxyResolve = resolve
|
||||
|
||||
|
@ -462,7 +454,7 @@ describe "populated composer", ->
|
|||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
it "doesn't send twice if you double click", ->
|
||||
|
@ -472,7 +464,7 @@ describe "populated composer", ->
|
|||
@isSending.state = true
|
||||
DraftStore.trigger()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
describe "when sending a message with keyboard inputs", ->
|
||||
|
@ -630,14 +622,14 @@ describe "populated composer", ->
|
|||
|
||||
@up1 =
|
||||
uploadTaskId: 4
|
||||
messageLocalId: DRAFT_LOCAL_ID
|
||||
messageClientId: DRAFT_CLIENT_ID
|
||||
filePath: "/foo/bar/f4.bmp"
|
||||
fileName: "f4.bmp"
|
||||
fileSize: 1024
|
||||
|
||||
@up2 =
|
||||
uploadTaskId: 5
|
||||
messageLocalId: DRAFT_LOCAL_ID
|
||||
messageClientId: DRAFT_CLIENT_ID
|
||||
filePath: "/foo/bar/f5.zip"
|
||||
fileName: "f5.zip"
|
||||
fileSize: 1024
|
||||
|
|
|
@ -14,17 +14,22 @@ ParticipantsTextField = proxyquire '../lib/participants-text-field',
|
|||
'nylas-exports': {Contact, ContactStore}
|
||||
|
||||
participant1 = new Contact
|
||||
id: 'local-1'
|
||||
email: 'ben@nylas.com'
|
||||
participant2 = new Contact
|
||||
id: 'local-2'
|
||||
email: 'ben@example.com'
|
||||
name: 'Ben Gotow'
|
||||
participant3 = new Contact
|
||||
id: 'local-3'
|
||||
email: 'evan@nylas.com'
|
||||
name: 'Evan Morikawa'
|
||||
participant4 = new Contact
|
||||
id: 'local-4',
|
||||
email: 'ben@elsewhere.com',
|
||||
name: 'ben Again'
|
||||
participant5 = new Contact
|
||||
id: 'local-5',
|
||||
email: 'evan@elsewhere.com',
|
||||
name: 'EVAN'
|
||||
|
||||
|
@ -54,7 +59,7 @@ describe 'ParticipantsTextField', ->
|
|||
ReactTestUtils.Simulate.keyDown(@renderedInput, {key: 'Enter', keyCode: 9})
|
||||
|
||||
reviver = (k,v) ->
|
||||
return undefined if k in ["id", "object"]
|
||||
return undefined if k in ["id", "client_id", "server_id", "object"]
|
||||
return v
|
||||
found = @propChange.mostRecentCall.args[0]
|
||||
found = JSON.parse(JSON.stringify(found), reviver)
|
||||
|
@ -102,7 +107,7 @@ describe 'ParticipantsTextField', ->
|
|||
|
||||
describe "when text contains Name (Email) formatted data", ->
|
||||
it "should correctly parse it into named Contact objects", ->
|
||||
newContact1 = new Contact(name:'Ben Imposter', email:'imposter@nylas.com')
|
||||
newContact1 = new Contact(id: "b1", name:'Ben Imposter', email:'imposter@nylas.com')
|
||||
newContact2 = new Contact(name:'Nylas Team', email:'feedback@nylas.com')
|
||||
|
||||
inputs = [
|
||||
|
@ -120,8 +125,8 @@ describe 'ParticipantsTextField', ->
|
|||
|
||||
describe "when text contains emails mixed with garbage text", ->
|
||||
it "should still parse out emails into Contact objects", ->
|
||||
newContact1 = new Contact(name:'garbage-man@nylas.com', email:'garbage-man@nylas.com')
|
||||
newContact2 = new Contact(name:'recycling-guy@nylas.com', email:'recycling-guy@nylas.com')
|
||||
newContact1 = new Contact(id: 'gm', name:'garbage-man@nylas.com', email:'garbage-man@nylas.com')
|
||||
newContact2 = new Contact(id: 'rm', name:'recycling-guy@nylas.com', email:'recycling-guy@nylas.com')
|
||||
|
||||
inputs = [
|
||||
"Hello world I real. \n asd. garbage-man@nylas.com—he's cool Also 'recycling-guy@nylas.com'!",
|
||||
|
|
|
@ -10,4 +10,4 @@ module.exports =
|
|||
deactivate: ->
|
||||
ComponentRegistry.unregister EventComponent
|
||||
|
||||
serialize: -> @state
|
||||
serialize: -> @state
|
||||
|
|
|
@ -100,16 +100,15 @@ class MessageControls extends React.Component
|
|||
body: @props.message.body
|
||||
|
||||
DatabaseStore.persistModel(draft).then =>
|
||||
DatabaseStore.localIdForModel(draft).then (localId) =>
|
||||
Actions.sendDraft(localId)
|
||||
Actions.sendDraft(draft.clientId)
|
||||
|
||||
dialog = remote.require('dialog')
|
||||
dialog.showMessageBox remote.getCurrentWindow(), {
|
||||
type: 'warning'
|
||||
buttons: ['OK'],
|
||||
message: "Thank you."
|
||||
detail: "The contents of this message have been sent to the Edgehill team and we added to a test suite."
|
||||
}
|
||||
dialog = remote.require('dialog')
|
||||
dialog.showMessageBox remote.getCurrentWindow(), {
|
||||
type: 'warning'
|
||||
buttons: ['OK'],
|
||||
message: "Thank you."
|
||||
detail: "The contents of this message have been sent to the Edgehill team and we added to a test suite."
|
||||
}
|
||||
|
||||
_onShowOriginal: =>
|
||||
fs = require 'fs'
|
||||
|
|
|
@ -15,11 +15,6 @@ class MessageItemContainer extends React.Component
|
|||
@propTypes =
|
||||
thread: React.PropTypes.object.isRequired
|
||||
message: React.PropTypes.object.isRequired
|
||||
|
||||
# The localId (in the case of draft's local ID) is a derived
|
||||
# property that only the parent MessageList knows about.
|
||||
localId: React.PropTypes.string
|
||||
|
||||
collapsed: React.PropTypes.bool
|
||||
isLastMsg: React.PropTypes.bool
|
||||
isBeforeReplyArea: React.PropTypes.bool
|
||||
|
@ -65,7 +60,7 @@ class MessageItemContainer extends React.Component
|
|||
_renderComposer: =>
|
||||
props =
|
||||
mode: "inline"
|
||||
localId: @props.localId
|
||||
draftClientId: @props.message.clientId
|
||||
threadId: @props.thread.id
|
||||
onRequestScrollTo: @props.onRequestScrollTo
|
||||
|
||||
|
@ -81,10 +76,10 @@ class MessageItemContainer extends React.Component
|
|||
"message-item-wrap": true
|
||||
"before-reply-area": @props.isBeforeReplyArea
|
||||
|
||||
_onSendingStateChanged: (draftLocalId) =>
|
||||
@setState(@_getStateFromStores()) if draftLocalId is @props.localId
|
||||
_onSendingStateChanged: (draftClientId) =>
|
||||
@setState(@_getStateFromStores()) if draftClientId is @props.message.clientId
|
||||
|
||||
_getStateFromStores: ->
|
||||
isSending: DraftStore.isSendingDraft(@props.localId)
|
||||
isSending: DraftStore.isSendingDraft(@props.message.clientId)
|
||||
|
||||
module.exports = MessageItemContainer
|
||||
|
|
|
@ -128,8 +128,8 @@ class MessageList extends React.Component
|
|||
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
|
||||
return _.difference(newDraftIds, oldDraftIds) ? []
|
||||
|
||||
_getMessageContainer: (id) =>
|
||||
@refs["message-container-#{id}"]
|
||||
_getMessageContainer: (messageId) =>
|
||||
@refs["message-container-#{messageId}"]
|
||||
|
||||
_focusDraft: (draftElement) =>
|
||||
# Note: We don't want the contenteditable view competing for scroll offset,
|
||||
|
@ -155,7 +155,7 @@ class MessageList extends React.Component
|
|||
# in reply to and the draft session and change the participants.
|
||||
if last.draft is true
|
||||
data =
|
||||
session: DraftStore.sessionForLocalId(@state.messageLocalIds[last.id])
|
||||
session: DraftStore.sessionForClientId(last.clientId)
|
||||
replyToMessage: Promise.resolve(@state.messages[@state.messages.length - 2])
|
||||
type: type
|
||||
|
||||
|
@ -299,14 +299,11 @@ class MessageList extends React.Component
|
|||
isLastMsg = (messages.length - 1 is idx)
|
||||
isBeforeReplyArea = isLastMsg and hasReplyArea
|
||||
|
||||
localId = @state.messageLocalIds[message.id]
|
||||
|
||||
elements.push(
|
||||
<MessageItemContainer key={idx}
|
||||
ref={"message-container-#{message.id}"}
|
||||
thread={@state.currentThread}
|
||||
message={message}
|
||||
localId={localId}
|
||||
collapsed={collapsed}
|
||||
isLastMsg={isLastMsg}
|
||||
isBeforeReplyArea={isBeforeReplyArea}
|
||||
|
@ -404,7 +401,6 @@ class MessageList extends React.Component
|
|||
|
||||
_getStateFromStores: =>
|
||||
messages: (MessageStore.items() ? [])
|
||||
messageLocalIds: MessageStore.itemLocalIds()
|
||||
messagesExpandedState: MessageStore.itemsExpandedState()
|
||||
currentThread: MessageStore.thread()
|
||||
loading: MessageStore.itemsLoading()
|
||||
|
|
|
@ -16,7 +16,7 @@ MessageItemContainer = proxyquire '../lib/message-item-container',
|
|||
{InjectedComponent} = require 'nylas-component-kit'
|
||||
|
||||
testThread = new Thread(id: "t1")
|
||||
testLocalId = "local-id"
|
||||
testClientId = "local-id"
|
||||
testMessage = new Message(id: "m1", draft: false, unread: true)
|
||||
testDraft = new Message(id: "d1", draft: true, unread: true)
|
||||
|
||||
|
@ -30,7 +30,7 @@ describe 'MessageItemContainer', ->
|
|||
ReactTestUtils.renderIntoDocument(
|
||||
<MessageItemContainer thread={testThread}
|
||||
message={message}
|
||||
localId={testLocalId} />
|
||||
draftClientId={testClientId} />
|
||||
)
|
||||
|
||||
it "shows composer if it's a draft", ->
|
||||
|
|
|
@ -114,7 +114,7 @@ describe "MessageItem", ->
|
|||
snippet: "snippet one..."
|
||||
subject: "Subject One"
|
||||
threadId: "thread_12345"
|
||||
accountId: "test_account_id"
|
||||
accountId: TEST_ACCOUNT_ID
|
||||
|
||||
@thread = new Thread
|
||||
id: 'thread-111'
|
||||
|
|
|
@ -33,12 +33,6 @@ MessageItemContainer = proxyquire("../lib/message-item-container", {
|
|||
MessageList = proxyquire '../lib/message-list',
|
||||
"./message-item-container": MessageItemContainer
|
||||
|
||||
me = new Account
|
||||
name: "User One",
|
||||
emailAddress: "user1@nylas.com"
|
||||
provider: "inbox"
|
||||
AccountStore._current = me
|
||||
|
||||
user_1 = new Contact
|
||||
name: "User One"
|
||||
email: "user1@nylas.com"
|
||||
|
@ -70,7 +64,7 @@ m1 = (new Message).fromJSON({
|
|||
"snippet" : "snippet one...",
|
||||
"subject" : "Subject One",
|
||||
"thread_id" : "thread_12345",
|
||||
"account_id" : "test_account_id"
|
||||
"account_id" : TEST_ACCOUNT_ID
|
||||
})
|
||||
m2 = (new Message).fromJSON({
|
||||
"id" : "222",
|
||||
|
@ -87,7 +81,7 @@ m2 = (new Message).fromJSON({
|
|||
"snippet" : "snippet Two...",
|
||||
"subject" : "Subject Two",
|
||||
"thread_id" : "thread_12345",
|
||||
"account_id" : "test_account_id"
|
||||
"account_id" : TEST_ACCOUNT_ID
|
||||
})
|
||||
m3 = (new Message).fromJSON({
|
||||
"id" : "333",
|
||||
|
@ -104,7 +98,7 @@ m3 = (new Message).fromJSON({
|
|||
"snippet" : "snippet Three...",
|
||||
"subject" : "Subject Three",
|
||||
"thread_id" : "thread_12345",
|
||||
"account_id" : "test_account_id"
|
||||
"account_id" : TEST_ACCOUNT_ID
|
||||
})
|
||||
m4 = (new Message).fromJSON({
|
||||
"id" : "444",
|
||||
|
@ -121,7 +115,7 @@ m4 = (new Message).fromJSON({
|
|||
"snippet" : "snippet Four...",
|
||||
"subject" : "Subject Four",
|
||||
"thread_id" : "thread_12345",
|
||||
"account_id" : "test_account_id"
|
||||
"account_id" : TEST_ACCOUNT_ID
|
||||
})
|
||||
m5 = (new Message).fromJSON({
|
||||
"id" : "555",
|
||||
|
@ -138,7 +132,7 @@ m5 = (new Message).fromJSON({
|
|||
"snippet" : "snippet Five...",
|
||||
"subject" : "Subject Five",
|
||||
"thread_id" : "thread_12345",
|
||||
"account_id" : "test_account_id"
|
||||
"account_id" : TEST_ACCOUNT_ID
|
||||
})
|
||||
testMessages = [m1, m2, m3, m4, m5]
|
||||
draftMessages = [
|
||||
|
@ -157,11 +151,12 @@ draftMessages = [
|
|||
"snippet" : "draft snippet one...",
|
||||
"subject" : "Draft One",
|
||||
"thread_id" : "thread_12345",
|
||||
"account_id" : "test_account_id"
|
||||
"account_id" : TEST_ACCOUNT_ID
|
||||
}),
|
||||
]
|
||||
|
||||
test_thread = (new Thread).fromJSON({
|
||||
"id": "12345"
|
||||
"id" : "thread_12345"
|
||||
"subject" : "Subject 12345"
|
||||
})
|
||||
|
@ -170,8 +165,6 @@ describe "MessageList", ->
|
|||
beforeEach ->
|
||||
MessageStore._items = []
|
||||
MessageStore._threadId = null
|
||||
spyOn(MessageStore, "itemLocalIds").andCallFake ->
|
||||
{"666": "666"}
|
||||
spyOn(MessageStore, "itemsLoading").andCallFake ->
|
||||
false
|
||||
|
||||
|
@ -224,7 +217,7 @@ describe "MessageList", ->
|
|||
messages: msgs.concat(draftMessages)
|
||||
|
||||
expect(@messageList._focusDraft).toHaveBeenCalled()
|
||||
expect(@messageList._focusDraft.mostRecentCall.args[0].props.localId).toEqual(draftMessages[0].id)
|
||||
expect(@messageList._focusDraft.mostRecentCall.args[0].props.draftClientId).toEqual(draftMessages[0].draftClientId)
|
||||
|
||||
it "includes drafts as message item containers", ->
|
||||
msgs = @messageList.state.messages
|
||||
|
@ -306,7 +299,7 @@ describe "MessageList", ->
|
|||
draft: => @draft
|
||||
changes:
|
||||
add: jasmine.createSpy('session.changes.add')
|
||||
spyOn(DraftStore, 'sessionForLocalId').andCallFake =>
|
||||
spyOn(DraftStore, 'sessionForClientId').andCallFake =>
|
||||
Promise.resolve(@sessionStub)
|
||||
|
||||
it "should not fire a composer action", ->
|
||||
|
|
|
@ -38,4 +38,4 @@ describe "MessageTimestamp", ->
|
|||
|
||||
it "displays month, day, and year for messages over a year ago", ->
|
||||
now = msgTime().add(2, 'years')
|
||||
expect(@item._formattedDate(msgTime(), now)).toBe "Feb 14, 2010"
|
||||
expect(@item._formattedDate(msgTime(), now)).toBe "Feb 14, 2010"
|
||||
|
|
|
@ -74,14 +74,14 @@ class TemplatePicker extends React.Component
|
|||
templates: @_filteredTemplates(newSearch)
|
||||
|
||||
_onChooseTemplate: (template) =>
|
||||
Actions.insertTemplateId({templateId:template.id, draftLocalId: @props.draftLocalId})
|
||||
Actions.insertTemplateId({templateId:template.id, draftClientId: @props.draftClientId})
|
||||
@refs.popover.close()
|
||||
|
||||
_onManageTemplates: =>
|
||||
Actions.showTemplates()
|
||||
|
||||
_onNewTemplate: =>
|
||||
Actions.createTemplate({draftLocalId: @props.draftLocalId})
|
||||
Actions.createTemplate({draftClientId: @props.draftClientId})
|
||||
|
||||
|
||||
module.exports = TemplatePicker
|
||||
|
|
|
@ -11,15 +11,15 @@ class TemplateStatusBar extends React.Component
|
|||
margin:'auto'
|
||||
|
||||
@propTypes:
|
||||
draftLocalId: React.PropTypes.string
|
||||
draftClientId: React.PropTypes.string
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = draft: null
|
||||
|
||||
componentDidMount: =>
|
||||
DraftStore.sessionForLocalId(@props.draftLocalId).then (_proxy) =>
|
||||
DraftStore.sessionForClientId(@props.draftClientId).then (_proxy) =>
|
||||
return if @_unmounted
|
||||
return unless _proxy.draftLocalId is @props.draftLocalId
|
||||
return unless _proxy.draftClientId is @props.draftClientId
|
||||
@_proxy = _proxy
|
||||
@unsubscribe = @_proxy.listen(@_onDraftChange, @)
|
||||
@_onDraftChange()
|
||||
|
|
|
@ -54,9 +54,9 @@ TemplateStore = Reflux.createStore
|
|||
path: path.join(@_templatesDir, filename)
|
||||
@trigger(@)
|
||||
|
||||
_onCreateTemplate: ({draftLocalId, name, contents} = {}) ->
|
||||
if draftLocalId
|
||||
DraftStore.sessionForLocalId(draftLocalId).then (session) =>
|
||||
_onCreateTemplate: ({draftClientId, name, contents} = {}) ->
|
||||
if draftClientId
|
||||
DraftStore.sessionForClientId(draftClientId).then (session) =>
|
||||
draft = session.draft()
|
||||
name ?= draft.subject
|
||||
contents ?= draft.body
|
||||
|
@ -92,13 +92,13 @@ TemplateStore = Reflux.createStore
|
|||
path: templatePath
|
||||
@trigger(@)
|
||||
|
||||
_onInsertTemplateId: ({templateId, draftLocalId} = {}) ->
|
||||
_onInsertTemplateId: ({templateId, draftClientId} = {}) ->
|
||||
template = _.find @_items, (item) -> item.id is templateId
|
||||
return unless template
|
||||
|
||||
fs.readFile template.path, (err, data) ->
|
||||
body = data.toString()
|
||||
DraftStore.sessionForLocalId(draftLocalId).then (session) ->
|
||||
DraftStore.sessionForClientId(draftClientId).then (session) ->
|
||||
session.changes.add(body: body)
|
||||
|
||||
module.exports = TemplateStore
|
||||
|
|
|
@ -59,13 +59,13 @@ describe "TemplateStore", ->
|
|||
it "should insert the template with the given id into the draft with the given id", ->
|
||||
|
||||
add = jasmine.createSpy('add')
|
||||
spyOn(DraftStore, 'sessionForLocalId').andCallFake ->
|
||||
spyOn(DraftStore, 'sessionForClientId').andCallFake ->
|
||||
Promise.resolve(changes: {add})
|
||||
|
||||
runs ->
|
||||
TemplateStore._onInsertTemplateId
|
||||
templateId: 'template1.html',
|
||||
draftLocalId: 'localid-draft'
|
||||
draftClientId: 'localid-draft'
|
||||
waitsFor ->
|
||||
add.calls.length > 0
|
||||
runs ->
|
||||
|
@ -75,8 +75,8 @@ describe "TemplateStore", ->
|
|||
describe "onCreateTemplate", ->
|
||||
beforeEach ->
|
||||
TemplateStore.init()
|
||||
spyOn(DraftStore, 'sessionForLocalId').andCallFake (draftLocalId) ->
|
||||
if draftLocalId is 'localid-nosubject'
|
||||
spyOn(DraftStore, 'sessionForClientId').andCallFake (draftClientId) ->
|
||||
if draftClientId is 'localid-nosubject'
|
||||
d = new Message(subject: '', body: '<p>Body</p>')
|
||||
else
|
||||
d = new Message(subject: 'Subject', body: '<p>Body</p>')
|
||||
|
@ -118,7 +118,7 @@ describe "TemplateStore", ->
|
|||
spyOn(TemplateStore, 'trigger')
|
||||
spyOn(TemplateStore, '_populate')
|
||||
runs ->
|
||||
TemplateStore._onCreateTemplate({draftLocalId: 'localid-b'})
|
||||
TemplateStore._onCreateTemplate({draftClientId: 'localid-b'})
|
||||
waitsFor ->
|
||||
TemplateStore.trigger.callCount > 0
|
||||
runs ->
|
||||
|
@ -127,7 +127,7 @@ describe "TemplateStore", ->
|
|||
it "should display an error if the draft has no subject", ->
|
||||
spyOn(TemplateStore, '_displayError')
|
||||
runs ->
|
||||
TemplateStore._onCreateTemplate({draftLocalId: 'localid-nosubject'})
|
||||
TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'})
|
||||
waitsFor ->
|
||||
TemplateStore._displayError.callCount > 0
|
||||
runs ->
|
||||
|
|
|
@ -51,11 +51,6 @@ DraftListStore = Reflux.createStore
|
|||
selected = @_view.selection.items()
|
||||
|
||||
for item in selected
|
||||
DatabaseStore.localIdForModel(item).then (localId) =>
|
||||
Actions.queueTask(new DestroyDraftTask(draftLocalId: localId))
|
||||
# if thread.id is focusedId
|
||||
# Actions.setFocus(collection: 'thread', item: null)
|
||||
# if thread.id is keyboardId
|
||||
# Actions.setCursorPosition(collection: 'thread', item: null)
|
||||
Actions.queueTask(new DestroyDraftTask(draftClientId: item.clientId))
|
||||
|
||||
@_view.selection.clear()
|
||||
|
|
|
@ -64,16 +64,14 @@ class DraftList extends React.Component
|
|||
collection="draft" />
|
||||
|
||||
_onDoubleClick: (item) =>
|
||||
DatabaseStore.localIdForModel(item).then (localId) ->
|
||||
Actions.composePopoutDraft(localId)
|
||||
Actions.composePopoutDraft(item.clientId)
|
||||
|
||||
# Additional Commands
|
||||
|
||||
_onDelete: ({focusedId}) =>
|
||||
item = DraftListStore.view().getById(focusedId)
|
||||
return unless item
|
||||
DatabaseStore.localIdForModel(item).then (localId) ->
|
||||
Actions.destroyDraft(localId)
|
||||
Actions.destroyDraft(item.clientId)
|
||||
|
||||
|
||||
module.exports = DraftList
|
||||
|
|
|
@ -35,7 +35,7 @@ class ThreadListIcon extends React.Component
|
|||
_nonDraftMessages: =>
|
||||
msgs = @props.thread.metadata
|
||||
return [] unless msgs and msgs instanceof Array
|
||||
msgs = _.filter msgs, (m) -> m.isSaved() and not m.draft
|
||||
msgs = _.filter msgs, (m) -> m.serverId and not m.draft
|
||||
return msgs
|
||||
|
||||
shouldComponentUpdate: (nextProps) =>
|
||||
|
|
|
@ -34,20 +34,13 @@ ThreadList = require "../lib/thread-list"
|
|||
ParticipantsItem = React.createClass
|
||||
render: -> <div></div>
|
||||
|
||||
me = new Account(
|
||||
"name": "User One",
|
||||
"email": "user1@nylas.com"
|
||||
"provider": "inbox"
|
||||
)
|
||||
AccountStore._current = me
|
||||
|
||||
test_threads = -> [
|
||||
(new Thread).fromJSON({
|
||||
"id": "111",
|
||||
"object": "thread",
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"account_id": "test_account_id",
|
||||
"account_id": TEST_ACCOUNT_ID,
|
||||
"snippet": "snippet 111",
|
||||
"subject": "Subject 111",
|
||||
"tags": [
|
||||
|
@ -103,7 +96,7 @@ test_threads = -> [
|
|||
"object": "thread",
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"account_id": "test_account_id",
|
||||
"account_id": TEST_ACCOUNT_ID,
|
||||
"snippet": "snippet 222",
|
||||
"subject": "Subject 222",
|
||||
"tags": [
|
||||
|
@ -153,7 +146,7 @@ test_threads = -> [
|
|||
"object": "thread",
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"account_id": "test_account_id",
|
||||
"account_id": TEST_ACCOUNT_ID,
|
||||
"snippet": "snippet 333",
|
||||
"subject": "Subject 333",
|
||||
"tags": [
|
||||
|
|
|
@ -10,4 +10,4 @@ module.exports =
|
|||
deactivate: ->
|
||||
ComponentRegistry.unregister UndoRedoComponent
|
||||
|
||||
serialize: -> @state
|
||||
serialize: -> @state
|
||||
|
|
|
@ -18,7 +18,7 @@ describe "NylasSyncWorker", ->
|
|||
|
||||
spyOn(DatabaseStore, 'persistJSONObject').andReturn(Promise.resolve())
|
||||
spyOn(DatabaseStore, 'findJSONObject').andCallFake (key) =>
|
||||
expected = "NylasSyncWorker:account-id"
|
||||
expected = "NylasSyncWorker:#{TEST_ACCOUNT_ID}"
|
||||
return throw new Error("Not stubbed! #{key}") unless key is expected
|
||||
Promise.resolve _.extend {}, {
|
||||
"contacts":
|
||||
|
@ -29,7 +29,7 @@ describe "NylasSyncWorker", ->
|
|||
complete: true
|
||||
}
|
||||
|
||||
@account = new Account(id: 'account-id', organizationUnit: 'label')
|
||||
@account = new Account(clientId: TEST_ACCOUNT_CLIENT_ID, serverId: TEST_ACCOUNT_ID, organizationUnit: 'label')
|
||||
@worker = new NylasSyncWorker(@api, @account)
|
||||
@connection = @worker.connection()
|
||||
advanceClock()
|
||||
|
|
|
@ -140,7 +140,6 @@ DeveloperBarStore = Reflux.createStore
|
|||
#{debugData}
|
||||
"""
|
||||
DatabaseStore.persistModel(draft).then ->
|
||||
DatabaseStore.localIdForModel(draft).then (localId) ->
|
||||
Actions.composePopoutDraft(localId)
|
||||
Actions.composePopoutDraft(draft.clientId)
|
||||
|
||||
module.exports = DeveloperBarStore
|
||||
|
|
|
@ -51,7 +51,7 @@ describe "ActionBridge", ->
|
|||
@bridge = new ActionBridge(ipc)
|
||||
@message = new Message
|
||||
id: 'test-id'
|
||||
accountId: 'test-account-id'
|
||||
accountId: TEST_ACCOUNT_ID
|
||||
|
||||
it "should have the role Role.SECONDARY", ->
|
||||
expect(@bridge.role).toBe(ActionBridge.Role.SECONDARY)
|
||||
|
@ -83,19 +83,19 @@ describe "ActionBridge", ->
|
|||
describe "when called with TargetWindows.ALL", ->
|
||||
it "should broadcast the action over IPC to all windows", ->
|
||||
spyOn(ipc, 'send')
|
||||
Actions.didSwapModel.firing = false
|
||||
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'didSwapModel', [{oldModel: '1', newModel: 2}])
|
||||
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'didSwapModel', '[{"oldModel":"1","newModel":2}]')
|
||||
Actions.logout.firing = false
|
||||
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'logout', [{oldModel: '1', newModel: 2}])
|
||||
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'logout', '[{"oldModel":"1","newModel":2}]')
|
||||
|
||||
describe "when called with TargetWindows.WORK", ->
|
||||
it "should broadcast the action over IPC to the main window only", ->
|
||||
spyOn(ipc, 'send')
|
||||
Actions.didSwapModel.firing = false
|
||||
@bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'didSwapModel', [{oldModel: '1', newModel: 2}])
|
||||
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'didSwapModel', '[{"oldModel":"1","newModel":2}]')
|
||||
Actions.logout.firing = false
|
||||
@bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'logout', [{oldModel: '1', newModel: 2}])
|
||||
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'logout', '[{"oldModel":"1","newModel":2}]')
|
||||
|
||||
it "should not do anything if the current invocation of the Action was triggered by itself", ->
|
||||
spyOn(ipc, 'send')
|
||||
Actions.didSwapModel.firing = true
|
||||
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'didSwapModel', [{oldModel: '1', newModel: 2}])
|
||||
Actions.logout.firing = true
|
||||
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'logout', [{oldModel: '1', newModel: 2}])
|
||||
expect(ipc.send).not.toHaveBeenCalled()
|
||||
|
|
|
@ -9,12 +9,6 @@ ReactTestUtils = React.addons.TestUtils
|
|||
} = require 'nylas-exports'
|
||||
{TokenizingTextField, Menu} = require 'nylas-component-kit'
|
||||
|
||||
me = new Account
|
||||
name: 'Test User'
|
||||
email: 'test@example.com'
|
||||
provider: 'inbox'
|
||||
AccountStore._current = me
|
||||
|
||||
CustomToken = React.createClass
|
||||
render: ->
|
||||
<span>{@props.item.email}</span>
|
||||
|
@ -125,6 +119,8 @@ describe 'TokenizingTextField', ->
|
|||
|
||||
describe "when the user drags and drops a token between two fields", ->
|
||||
it "should work properly", ->
|
||||
participant2.clientId = '123'
|
||||
|
||||
tokensA = [participant1, participant2, participant3]
|
||||
fieldA = @rebuildRenderedField(tokensA)
|
||||
|
||||
|
@ -142,7 +138,7 @@ describe 'TokenizingTextField', ->
|
|||
ReactTestUtils.Simulate.dragStart(token, dragStartEvent)
|
||||
|
||||
expect(dragStartEventData).toEqual({
|
||||
'nylas-token-item': '{"id":"2","name":"Nylas Burger Basket","email":"burgers@nylas.com","__constructorName":"Contact"}'
|
||||
'nylas-token-item': '{"client_id":"123","server_id":"2","name":"Nylas Burger Basket","email":"burgers@nylas.com","id":"2","__constructorName":"Contact"}'
|
||||
'text/plain': 'Nylas Burger Basket <burgers@nylas.com>'
|
||||
})
|
||||
|
||||
|
|
|
@ -8,12 +8,30 @@ class TestModel extends Model
|
|||
queryable: true
|
||||
modelKey: 'id'
|
||||
|
||||
'clientId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
|
||||
'serverId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
|
||||
TestModel.configureBasic = ->
|
||||
TestModel.additionalSQLiteConfig = undefined
|
||||
TestModel.attributes =
|
||||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
'clientId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
'serverId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
|
||||
TestModel.configureWithAllAttributes = ->
|
||||
TestModel.additionalSQLiteConfig = undefined
|
||||
|
@ -40,6 +58,14 @@ TestModel.configureWithCollectionAttribute = ->
|
|||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
'clientId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
'serverId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
'labels': Attributes.Collection
|
||||
queryable: true
|
||||
modelKey: 'labels'
|
||||
|
@ -52,6 +78,14 @@ TestModel.configureWithJoinedDataAttribute = ->
|
|||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
'clientId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
'serverId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
'body': Attributes.JoinedData
|
||||
modelTable: 'TestModelBody'
|
||||
modelKey: 'body'
|
||||
|
@ -62,6 +96,12 @@ TestModel.configureWithAdditionalSQLiteConfig = ->
|
|||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
'clientId': Attributes.String
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
'serverId': Attributes.ServerId
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
'body': Attributes.JoinedData
|
||||
modelTable: 'TestModelBody'
|
||||
modelKey: 'body'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
Model = require '../../src/flux/models/model'
|
||||
Utils = require '../../src/flux/models/utils'
|
||||
Attributes = require '../../src/flux/attributes'
|
||||
{isTempId} = require '../../src/flux/models/utils'
|
||||
_ = require 'underscore'
|
||||
|
@ -13,22 +14,43 @@ describe "Model", ->
|
|||
expect(m.id).toBe(attrs.id)
|
||||
expect(m.accountId).toBe(attrs.accountId)
|
||||
|
||||
it "should assign a local- ID to the model if no ID is provided", ->
|
||||
it "by default assigns things passed into the id constructor to the serverId", ->
|
||||
attrs =
|
||||
id: "A",
|
||||
m = new Model(attrs)
|
||||
expect(m.serverId).toBe(attrs.id)
|
||||
|
||||
it "by default assigns values passed into the id constructor that look like localIds to be a localID", ->
|
||||
attrs =
|
||||
id: "A",
|
||||
m = new Model(attrs)
|
||||
expect(m.serverId).toBe(attrs.id)
|
||||
|
||||
it "assigns serverIds and clientIds", ->
|
||||
attrs =
|
||||
clientId: "local-A",
|
||||
serverId: "A",
|
||||
m = new Model(attrs)
|
||||
expect(m.serverId).toBe(attrs.serverId)
|
||||
expect(m.clientId).toBe(attrs.clientId)
|
||||
expect(m.id).toBe(attrs.serverId)
|
||||
|
||||
it "throws an error if you attempt to manually assign the id", ->
|
||||
m = new Model(id: "foo")
|
||||
expect( -> m.id = "bar" ).toThrow()
|
||||
|
||||
it "automatically assigns a clientId (and id) to the model if no id is provided", ->
|
||||
m = new Model
|
||||
expect(isTempId(m.id)).toBe(true)
|
||||
expect(Utils.isTempId(m.id)).toBe true
|
||||
expect(Utils.isTempId(m.clientId)).toBe true
|
||||
expect(m.serverId).toBeUndefined()
|
||||
|
||||
describe "attributes", ->
|
||||
it "should return the attributes of the class", ->
|
||||
it "should return the attributes of the class EXCEPT the id field", ->
|
||||
m = new Model()
|
||||
expect(m.attributes()).toBe(m.constructor.attributes)
|
||||
|
||||
describe "isSaved", ->
|
||||
it "should return false if the object has a temp ID", ->
|
||||
a = new Model()
|
||||
expect(a.isSaved()).toBe(false)
|
||||
|
||||
b = new Model({id: "b"})
|
||||
expect(b.isSaved()).toBe(true)
|
||||
retAttrs = _.clone(m.constructor.attributes)
|
||||
delete retAttrs["id"]
|
||||
expect(m.attributes()).toEqual(retAttrs)
|
||||
|
||||
describe "clone", ->
|
||||
it "should return a deep copy of the object", ->
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
{generateTempId} = require '../../src/flux/models/utils'
|
||||
Message = require '../../src/flux/models/message'
|
||||
Thread = require '../../src/flux/models/thread'
|
||||
_ = require 'underscore'
|
||||
|
|
|
@ -11,8 +11,6 @@ describe "ContactStore", ->
|
|||
ContactStore._contactCache = []
|
||||
ContactStore._fetchOffset = 0
|
||||
ContactStore._accountId = null
|
||||
AccountStore._current =
|
||||
id: "test_account_id"
|
||||
|
||||
afterEach ->
|
||||
atom.testOrganizationUnit = null
|
||||
|
@ -36,9 +34,7 @@ describe "ContactStore", ->
|
|||
spyOn(ContactStore, "_refreshCache")
|
||||
ContactStore._contactCache = [1,2,3]
|
||||
ContactStore._fetchOffset = 3
|
||||
ContactStore._accountId = "test_account_id"
|
||||
AccountStore._current =
|
||||
id: "test_account_id"
|
||||
ContactStore._accountId = TEST_ACCOUNT_ID
|
||||
AccountStore.trigger()
|
||||
expect(ContactStore._contactCache).toEqual [1,2,3]
|
||||
expect(ContactStore._fetchOffset).toBe 3
|
||||
|
|
|
@ -30,7 +30,7 @@ describe "DatabaseSetupQueryBuilder", ->
|
|||
TestModel.configureWithCollectionAttribute()
|
||||
queries = @builder.setupQueriesForTable(TestModel)
|
||||
expected = [
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB)',
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,client_id TEXT,server_id TEXT)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',
|
||||
'CREATE TABLE IF NOT EXISTS `TestModel-Label` (id TEXT KEY, `value` TEXT)'
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_Label_id_val` ON `TestModel-Label` (`id`,`value`)',
|
||||
|
|
|
@ -7,9 +7,9 @@ ModelQuery = require '../../src/flux/models/query'
|
|||
DatabaseStore = require '../../src/flux/stores/database-store'
|
||||
|
||||
testMatchers = {'id': 'b'}
|
||||
testModelInstance = new TestModel(id: '1234')
|
||||
testModelInstanceA = new TestModel(id: 'AAA')
|
||||
testModelInstanceB = new TestModel(id: 'BBB')
|
||||
testModelInstance = new TestModel(id: "1234")
|
||||
testModelInstanceA = new TestModel(id: "AAA")
|
||||
testModelInstanceB = new TestModel(id: "BBB")
|
||||
|
||||
describe "DatabaseStore", ->
|
||||
beforeEach ->
|
||||
|
@ -146,7 +146,7 @@ describe "DatabaseStore", ->
|
|||
it "should compose a REPLACE INTO query to save the model", ->
|
||||
TestModel.configureWithCollectionAttribute()
|
||||
DatabaseStore._writeModels([testModelInstance])
|
||||
expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data) VALUES (?,?)")
|
||||
expect(@performed[0].query).toBe("REPLACE INTO `TestModel` (id,data,client_id,server_id) VALUES (?,?,?,?)")
|
||||
|
||||
it "should save the model JSON into the data column", ->
|
||||
DatabaseStore._writeModels([testModelInstance])
|
||||
|
@ -234,9 +234,9 @@ describe "DatabaseStore", ->
|
|||
TestModel.configureWithJoinedDataAttribute()
|
||||
|
||||
it "should not include the value to the joined attribute in the JSON written to the main model table", ->
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
|
||||
@m = new TestModel(clientId: 'local-6806434c-b0cd', serverId: 'server-1', body: 'hello world')
|
||||
DatabaseStore._writeModels([@m])
|
||||
expect(@performed[0].values).toEqual(['local-6806434c-b0cd', '{"id":"local-6806434c-b0cd"}'])
|
||||
expect(@performed[0].values).toEqual(['server-1', '{"client_id":"local-6806434c-b0cd","server_id":"server-1","id":"server-1"}', 'local-6806434c-b0cd', 'server-1'])
|
||||
|
||||
it "should write the value to the joined table if it is defined", ->
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd', body: 'hello world')
|
||||
|
@ -244,7 +244,7 @@ describe "DatabaseStore", ->
|
|||
expect(@performed[1].query).toBe('REPLACE INTO `TestModelBody` (`id`, `value`) VALUES (?, ?)')
|
||||
expect(@performed[1].values).toEqual([@m.id, @m.body])
|
||||
|
||||
it "should not write the valeu to the joined table if it undefined", ->
|
||||
it "should not write the value to the joined table if it undefined", ->
|
||||
@m = new TestModel(id: 'local-6806434c-b0cd')
|
||||
DatabaseStore._writeModels([@m])
|
||||
expect(@performed.length).toBe(1)
|
||||
|
|
|
@ -107,7 +107,6 @@ describe "DraftStore", ->
|
|||
return Promise.resolve(fakeMessage2) if query._klass is Message
|
||||
return Promise.reject(new Error('Not Stubbed'))
|
||||
spyOn(DatabaseStore, 'persistModel').andCallFake -> Promise.resolve()
|
||||
spyOn(DatabaseStore, 'bindToLocalId')
|
||||
|
||||
afterEach ->
|
||||
# Have to cleanup the DraftStoreProxy objects or we'll get a memory
|
||||
|
@ -277,21 +276,13 @@ describe "DraftStore", ->
|
|||
, (model) ->
|
||||
expect(model.constructor).toBe(Message)
|
||||
|
||||
it "should assign and save a local Id for the new message", ->
|
||||
it "should setup a draft session for the draftClientId, so that a subsequent request for the session's draft resolves immediately.", ->
|
||||
@_callNewMessageWithContext {threadId: fakeThread.id}
|
||||
, (thread, message) ->
|
||||
{}
|
||||
, (model) ->
|
||||
expect(DatabaseStore.bindToLocalId).toHaveBeenCalled()
|
||||
|
||||
it "should setup a draft session for the draftLocalId, so that a subsequent request for the session's draft resolves immediately.", ->
|
||||
@_callNewMessageWithContext {threadId: fakeThread.id}
|
||||
, (thread, message) ->
|
||||
{}
|
||||
, (model) ->
|
||||
[draft, localId] = DatabaseStore.bindToLocalId.mostRecentCall.args
|
||||
session = DraftStore.sessionForLocalId(localId).value()
|
||||
expect(session.draft()).toBe(draft)
|
||||
session = DraftStore.sessionForClientId(model.id).value()
|
||||
expect(session.draft()).toBe(model)
|
||||
|
||||
it "should set the subject of the new message automatically", ->
|
||||
@_callNewMessageWithContext {threadId: fakeThread.id}
|
||||
|
@ -559,7 +550,7 @@ describe "DraftStore", ->
|
|||
expect(DraftStore._onBeforeUnload()).toBe(true)
|
||||
|
||||
describe "sending a draft", ->
|
||||
draftLocalId = "local-123"
|
||||
draftClientId = "local-123"
|
||||
beforeEach ->
|
||||
DraftStore._draftSessions = {}
|
||||
DraftStore._draftsSending = {}
|
||||
|
@ -569,7 +560,7 @@ describe "DraftStore", ->
|
|||
draft: -> {}
|
||||
changes:
|
||||
commit: -> Promise.resolve()
|
||||
DraftStore._draftSessions[draftLocalId] = proxy
|
||||
DraftStore._draftSessions[draftClientId] = proxy
|
||||
spyOn(DraftStore, "_doneWithSession").andCallThrough()
|
||||
spyOn(DraftStore, "trigger")
|
||||
|
||||
|
@ -577,23 +568,23 @@ describe "DraftStore", ->
|
|||
spyOn(atom, "isMainWindow").andReturn true
|
||||
spyOn(Actions, "queueTask").andCallThrough()
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
Actions.queueTask.calls.length > 0
|
||||
runs ->
|
||||
expect(DraftStore.isSendingDraft(draftLocalId)).toBe true
|
||||
expect(DraftStore.isSendingDraft(draftClientId)).toBe true
|
||||
expect(DraftStore.trigger).toHaveBeenCalled()
|
||||
|
||||
it "returns false if the draft hasn't been seen", ->
|
||||
spyOn(atom, "isMainWindow").andReturn true
|
||||
expect(DraftStore.isSendingDraft(draftLocalId)).toBe false
|
||||
expect(DraftStore.isSendingDraft(draftClientId)).toBe false
|
||||
|
||||
it "closes the window if it's a popout", ->
|
||||
spyOn(atom, "getWindowType").andReturn "composer"
|
||||
spyOn(atom, "isMainWindow").andReturn false
|
||||
spyOn(atom, "close")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor "Atom to close", ->
|
||||
atom.close.calls.length > 0
|
||||
|
||||
|
@ -603,7 +594,7 @@ describe "DraftStore", ->
|
|||
spyOn(atom, "close")
|
||||
spyOn(DraftStore, "_isPopout").andCallThrough()
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
DraftStore._isPopout.calls.length > 0
|
||||
runs ->
|
||||
|
@ -612,14 +603,14 @@ describe "DraftStore", ->
|
|||
it "queues a SendDraftTask", ->
|
||||
spyOn(Actions, "queueTask")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task instanceof SendDraftTask).toBe true
|
||||
expect(task.draftLocalId).toBe draftLocalId
|
||||
expect(task.draftClientId).toBe draftClientId
|
||||
expect(task.fromPopout).toBe false
|
||||
|
||||
it "queues a SendDraftTask with popout info", ->
|
||||
|
@ -628,7 +619,7 @@ describe "DraftStore", ->
|
|||
spyOn(atom, "close")
|
||||
spyOn(Actions, "queueTask")
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
DraftStore._onSendDraft(draftClientId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
|
@ -640,7 +631,7 @@ describe "DraftStore", ->
|
|||
beforeEach ->
|
||||
@draftTeardown = jasmine.createSpy('draft teardown')
|
||||
@session =
|
||||
draftLocalId: "abc"
|
||||
draftClientId: "abc"
|
||||
draft: ->
|
||||
pristine: false
|
||||
changes:
|
||||
|
@ -675,7 +666,7 @@ describe "DraftStore", ->
|
|||
received = null
|
||||
spyOn(DraftStore, '_finalizeAndPersistNewMessage').andCallFake (draft) ->
|
||||
received = draft
|
||||
Promise.resolve({draftLocalId: 123})
|
||||
Promise.resolve({draftClientId: 123})
|
||||
|
||||
expected = "EmailSubjectLOLOL"
|
||||
DraftStore._onHandleMailtoLink('mailto:asdf@asdf.com?subject=' + expected)
|
||||
|
|
|
@ -10,8 +10,6 @@ describe "EventStore", ->
|
|||
atom.testOrganizationUnit = "folder"
|
||||
EventStore._eventCache = {}
|
||||
EventStore._accountId = null
|
||||
AccountStore._current =
|
||||
id: "test_account_id"
|
||||
|
||||
afterEach ->
|
||||
atom.testOrganizationUnit = null
|
||||
|
@ -36,29 +34,27 @@ describe "EventStore", ->
|
|||
it "does nothing", ->
|
||||
spyOn(EventStore, "_refreshCache")
|
||||
EventStore._eventCache = {1: '', 2: '', 3: ''}
|
||||
EventStore._accountId = "test_account_id"
|
||||
AccountStore._current =
|
||||
id: "test_account_id"
|
||||
EventStore._accountId = TEST_ACCOUNT_ID
|
||||
AccountStore.trigger()
|
||||
expect(EventStore._eventCache).toEqual {1: '', 2: '', 3: ''}
|
||||
expect(EventStore._refreshCache).not.toHaveBeenCalled()
|
||||
|
||||
describe "getEvent", ->
|
||||
beforeEach ->
|
||||
@e1 = new Event(id: 1, title:'Test1', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e2 = new Event(id: 2, title:'Test2', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e3 = new Event(id: 3, title:'Test3', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e4 = new Event(id: 4, title:'Test4', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e1 = new Event(id: 'a', title:'Test1', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e2 = new Event(id: 'b', title:'Test2', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e3 = new Event(id: 'c', title:'Test3', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e4 = new Event(id: 'd', title:'Test4', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
EventStore._eventCache = {}
|
||||
for e in [@e1, @e2, @e3, @e4]
|
||||
EventStore._eventCache[e.id] = e
|
||||
|
||||
it "returns event object based on id", ->
|
||||
first = EventStore.getEvent(1)
|
||||
first = EventStore.getEvent('a')
|
||||
expect(first.title).toBe 'Test1'
|
||||
second = EventStore.getEvent(2)
|
||||
second = EventStore.getEvent('b')
|
||||
expect(second.title).toBe 'Test2'
|
||||
third = EventStore.getEvent(3)
|
||||
third = EventStore.getEvent('c')
|
||||
expect(third.title).toBe 'Test3'
|
||||
fourth = EventStore.getEvent(4)
|
||||
fourth = EventStore.getEvent('d')
|
||||
expect(fourth.title).toBe 'Test4'
|
||||
|
|
|
@ -41,7 +41,7 @@ describe "FileDownloadStore", ->
|
|||
beforeEach ->
|
||||
spyOn(shell, 'showItemInFolder')
|
||||
spyOn(shell, 'openItem')
|
||||
@testfile = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
|
||||
@testfile = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
|
||||
FileDownloadStore._downloads = {}
|
||||
FileDownloadStore._downloadDirectory = "/Users/testuser/.nylas/downloads"
|
||||
|
||||
|
@ -60,7 +60,7 @@ describe "FileDownloadStore", ->
|
|||
|
||||
describe "_checkForDownloadedFile", ->
|
||||
it "should return true if the file exists at the path and is the right size", ->
|
||||
f = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
|
||||
f = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
|
||||
spyOn(fs, 'stat').andCallFake (path, callback) ->
|
||||
callback(null, {size: 100})
|
||||
waitsForPromise ->
|
||||
|
@ -68,7 +68,7 @@ describe "FileDownloadStore", ->
|
|||
expect(downloaded).toBe(true)
|
||||
|
||||
it "should return false if the file does not exist", ->
|
||||
f = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
|
||||
f = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
|
||||
spyOn(fs, 'stat').andCallFake (path, callback) ->
|
||||
callback(new Error("File does not exist"))
|
||||
waitsForPromise ->
|
||||
|
@ -76,7 +76,7 @@ describe "FileDownloadStore", ->
|
|||
expect(downloaded).toBe(false)
|
||||
|
||||
it "should return false if the file is too small", ->
|
||||
f = new File(filename: '123.png', contentType: 'image/png', id: 'id', size: 100)
|
||||
f = new File(filename: '123.png', contentType: 'image/png', id: "id", size: 100)
|
||||
spyOn(fs, 'stat').andCallFake (path, callback) ->
|
||||
callback(null, {size: 50})
|
||||
waitsForPromise ->
|
||||
|
|
|
@ -14,7 +14,7 @@ describe 'FileUploadStore', ->
|
|||
size: 12345
|
||||
@uploadData =
|
||||
uploadTaskId: 123
|
||||
messageLocalId: msgId
|
||||
messageClientId: msgId
|
||||
filePath: fpath
|
||||
fileSize: 12345
|
||||
|
||||
|
@ -29,11 +29,11 @@ describe 'FileUploadStore', ->
|
|||
|
||||
it "throws if the message id is blank", ->
|
||||
spyOn(Actions, "attachFilePath")
|
||||
Actions.attachFile messageLocalId: msgId
|
||||
Actions.attachFile messageClientId: msgId
|
||||
expect(atom.showOpenDialog).toHaveBeenCalled()
|
||||
expect(Actions.attachFilePath).toHaveBeenCalled()
|
||||
args = Actions.attachFilePath.calls[0].args[0]
|
||||
expect(args.messageLocalId).toBe msgId
|
||||
expect(args.messageClientId).toBe msgId
|
||||
expect(args.path).toBe fpath
|
||||
|
||||
describe 'attachFilePath', ->
|
||||
|
@ -44,19 +44,19 @@ describe 'FileUploadStore', ->
|
|||
spyOn(fs, 'stat').andCallFake (path, callback) ->
|
||||
callback(null, {isDirectory: -> false})
|
||||
Actions.attachFilePath
|
||||
messageLocalId: msgId
|
||||
messageClientId: msgId
|
||||
path: fpath
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
t = Actions.queueTask.calls[0].args[0]
|
||||
expect(t.filePath).toBe fpath
|
||||
expect(t.messageLocalId).toBe msgId
|
||||
expect(t.messageClientId).toBe msgId
|
||||
|
||||
it 'displays an error if the file path given is a directory', ->
|
||||
spyOn(FileUploadStore, '_onAttachFileError')
|
||||
spyOn(fs, 'stat').andCallFake (path, callback) ->
|
||||
callback(null, {isDirectory: -> true})
|
||||
Actions.attachFilePath
|
||||
messageLocalId: msgId
|
||||
messageClientId: msgId
|
||||
path: fpath
|
||||
expect(Actions.queueTask).not.toHaveBeenCalled()
|
||||
expect(FileUploadStore._onAttachFileError).toHaveBeenCalled()
|
||||
|
|
|
@ -24,7 +24,7 @@ describe "FocusedCategoryStore", ->
|
|||
|
||||
it "should set the current category to Inbox when the current category no longer exists in the CategoryStore", ->
|
||||
otherAccountInbox = @inboxCategory.clone()
|
||||
otherAccountInbox.id = 'other-id'
|
||||
otherAccountInbox.serverId = 'other-id'
|
||||
FocusedCategoryStore._category = otherAccountInbox
|
||||
FocusedCategoryStore._onCategoryStoreChanged()
|
||||
expect(FocusedCategoryStore.category().id).toEqual(@inboxCategory.id)
|
||||
|
|
|
@ -1,17 +1,7 @@
|
|||
proxyquire = require 'proxyquire'
|
||||
Reflux = require 'reflux'
|
||||
|
||||
MessageStoreStub = Reflux.createStore
|
||||
items: -> []
|
||||
extensions: -> []
|
||||
threadId: -> null
|
||||
|
||||
AccountStoreStub = Reflux.createStore
|
||||
current: -> null
|
||||
|
||||
FocusedContactsStore = proxyquire '../../src/flux/stores/focused-contacts-store',
|
||||
"./message-store": MessageStoreStub
|
||||
"./account-store": AccountStoreStub
|
||||
FocusedContactsStore = require '../../src/flux/stores/focused-contacts-store'
|
||||
|
||||
describe "FocusedContactsStore", ->
|
||||
beforeEach ->
|
||||
|
|
|
@ -2,8 +2,6 @@ Actions = require '../../src/flux/actions'
|
|||
TaskQueue = require '../../src/flux/stores/task-queue'
|
||||
Task = require '../../src/flux/tasks/task'
|
||||
|
||||
{isTempId} = require '../../src/flux/models/utils'
|
||||
|
||||
{APIError,
|
||||
OfflineError,
|
||||
TimeoutError} = require '../../src/flux/errors'
|
||||
|
|
|
@ -19,7 +19,7 @@ describe "UnreadCountStore", ->
|
|||
atom.testOrganizationUnit = 'folder'
|
||||
UnreadCountStore._fetchCount()
|
||||
advanceClock()
|
||||
expect(DatabaseStore.findBy).toHaveBeenCalledWith(Folder, {name: 'inbox', accountId: 'test_account_id'})
|
||||
expect(DatabaseStore.findBy).toHaveBeenCalledWith(Folder, {name: 'inbox', accountId: TEST_ACCOUNT_ID})
|
||||
|
||||
[Model, Matchers] = DatabaseStore.count.calls[0].args
|
||||
expect(Model).toBe(Thread)
|
||||
|
@ -33,7 +33,7 @@ describe "UnreadCountStore", ->
|
|||
atom.testOrganizationUnit = 'label'
|
||||
UnreadCountStore._fetchCount()
|
||||
advanceClock()
|
||||
expect(DatabaseStore.findBy).toHaveBeenCalledWith(Label, {name: 'inbox', accountId: 'test_account_id'})
|
||||
expect(DatabaseStore.findBy).toHaveBeenCalledWith(Label, {name: 'inbox', accountId: TEST_ACCOUNT_ID})
|
||||
|
||||
[Model, Matchers] = DatabaseStore.count.calls[0].args
|
||||
expect(Matchers[0].attr.modelKey).toBe('accountId')
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
Task = require '../src/task'
|
||||
|
||||
xdescribe "Task", ->
|
||||
describe "@once(taskPath, args..., callback)", ->
|
||||
it "terminates the process after it completes", ->
|
||||
handlerResult = null
|
||||
task = Task.once require.resolve('./fixtures/task-spec-handler'), (result) ->
|
||||
handlerResult = result
|
||||
|
||||
processErrored = false
|
||||
childProcess = task.childProcess
|
||||
spyOn(childProcess, 'kill').andCallThrough()
|
||||
task.childProcess.on 'error', -> processErrored = true
|
||||
|
||||
waitsFor ->
|
||||
handlerResult?
|
||||
|
||||
runs ->
|
||||
expect(handlerResult).toBe 'hello'
|
||||
expect(childProcess.kill).toHaveBeenCalled()
|
||||
expect(processErrored).toBe false
|
||||
|
||||
it "calls listeners registered with ::on when events are emitted in the task", ->
|
||||
task = new Task(require.resolve('./fixtures/task-spec-handler'))
|
||||
|
||||
eventSpy = jasmine.createSpy('eventSpy')
|
||||
task.on("some-event", eventSpy)
|
||||
|
||||
waitsFor (done) -> task.start(done)
|
||||
|
||||
runs ->
|
||||
expect(eventSpy).toHaveBeenCalledWith(1, 2, 3)
|
||||
|
||||
it "unregisters listeners when the Disposable returned by ::on is disposed", ->
|
||||
task = new Task(require.resolve('./fixtures/task-spec-handler'))
|
||||
|
||||
eventSpy = jasmine.createSpy('eventSpy')
|
||||
disposable = task.on("some-event", eventSpy)
|
||||
disposable.dispose()
|
||||
|
||||
waitsFor (done) -> task.start(done)
|
||||
|
||||
runs ->
|
||||
expect(eventSpy).not.toHaveBeenCalled()
|
|
@ -15,7 +15,7 @@ describe "EventRSVPTask", ->
|
|||
@myEmail = "tester@nylas.com"
|
||||
@event = new Event
|
||||
id: '12233AEDF5'
|
||||
accountId: 'test_account_id'
|
||||
accountId: TEST_ACCOUNT_ID
|
||||
title: 'Meeting with Ben Bitdiddle'
|
||||
description: ''
|
||||
location: ''
|
||||
|
|
|
@ -26,7 +26,7 @@ test_file_paths = [
|
|||
|
||||
noop = ->
|
||||
|
||||
localId = "local-id_1234"
|
||||
messageClientId = "local-id_1234"
|
||||
|
||||
fake_draft = new Message
|
||||
id: "draft-id_1234"
|
||||
|
@ -52,13 +52,13 @@ describe "FileUploadTask", ->
|
|||
|
||||
@uploadData =
|
||||
startDate: DATE
|
||||
messageLocalId: localId
|
||||
messageClientId: messageClientId
|
||||
filePath: test_file_paths[0]
|
||||
fileSize: 1234
|
||||
fileName: "file.txt"
|
||||
bytesUploaded: 0
|
||||
|
||||
@task = new FileUploadTask(test_file_paths[0], localId)
|
||||
@task = new FileUploadTask(test_file_paths[0], messageClientId)
|
||||
|
||||
@req = jasmine.createSpyObj('req', ['abort'])
|
||||
@simulateRequestSuccessImmediately = false
|
||||
|
@ -82,13 +82,13 @@ describe "FileUploadTask", ->
|
|||
(new FileUploadTask).performLocal().catch (err) ->
|
||||
expect(err instanceof Error).toBe true
|
||||
|
||||
it "rejects if not initialized with a messageLocalId", ->
|
||||
it "rejects if not initialized with a messageClientId", ->
|
||||
waitsForPromise ->
|
||||
(new FileUploadTask(test_file_paths[0])).performLocal().catch (err) ->
|
||||
expect(err instanceof Error).toBe true
|
||||
|
||||
it 'initializes the upload start', ->
|
||||
task = new FileUploadTask(test_file_paths[0], localId)
|
||||
task = new FileUploadTask(test_file_paths[0], messageClientId)
|
||||
expect(task._startDate).toBe DATE
|
||||
|
||||
it "notifies when the task locally starts", ->
|
||||
|
@ -159,7 +159,7 @@ describe "FileUploadTask", ->
|
|||
@simulateRequestSuccessImmediately = true
|
||||
|
||||
spyOn(Actions, "uploadStateChanged")
|
||||
spyOn(DraftStore, "sessionForLocalId").andCallFake =>
|
||||
spyOn(DraftStore, "sessionForClientId").andCallFake =>
|
||||
Promise.resolve(
|
||||
draft: => files: @testFiles
|
||||
changes:
|
||||
|
@ -178,12 +178,14 @@ describe "FileUploadTask", ->
|
|||
waitsForPromise => @task.performRemote().then ->
|
||||
options = NylasAPI.makeRequest.mostRecentCall.args[0]
|
||||
expect(options.path).toBe("/files")
|
||||
expect(options.accountId).toBe("test_account_id")
|
||||
expect(options.accountId).toBe(TEST_ACCOUNT_ID)
|
||||
expect(options.method).toBe('POST')
|
||||
expect(options.formData.file.value).toBe("Read Stream")
|
||||
|
||||
it "attaches the file to the draft", ->
|
||||
waitsForPromise => @task.performRemote().then =>
|
||||
delete @changes[0].clientId
|
||||
delete equivalentFile.clientId
|
||||
expect(@changes).toEqual [equivalentFile]
|
||||
|
||||
describe "file upload notifications", ->
|
||||
|
@ -201,15 +203,17 @@ describe "FileUploadTask", ->
|
|||
bytesUploaded: 1000
|
||||
|
||||
[{file, uploadData}] = Actions.fileUploaded.calls[0].args
|
||||
delete file.clientId
|
||||
delete equivalentFile.clientId
|
||||
expect(file).toEqual(equivalentFile)
|
||||
expect(_.isMatch(uploadData, uploadDataExpected)).toBe(true)
|
||||
|
||||
describe "when attaching a lot of files", ->
|
||||
it "attaches them all to the draft", ->
|
||||
t1 = new FileUploadTask("1.a", localId)
|
||||
t2 = new FileUploadTask("2.b", localId)
|
||||
t3 = new FileUploadTask("3.c", localId)
|
||||
t4 = new FileUploadTask("4.d", localId)
|
||||
t1 = new FileUploadTask("1.a", messageClientId)
|
||||
t2 = new FileUploadTask("2.b", messageClientId)
|
||||
t3 = new FileUploadTask("3.c", messageClientId)
|
||||
t4 = new FileUploadTask("4.d", messageClientId)
|
||||
|
||||
@simulateRequestSuccessImmediately = true
|
||||
waitsForPromise => Promise.all([
|
||||
|
|
|
@ -3,7 +3,6 @@ Actions = require '../../src/flux/actions'
|
|||
SyncbackDraftTask = require '../../src/flux/tasks/syncback-draft'
|
||||
SendDraftTask = require '../../src/flux/tasks/send-draft'
|
||||
DatabaseStore = require '../../src/flux/stores/database-store'
|
||||
{generateTempId} = require '../../src/flux/models/utils'
|
||||
{APIError} = require '../../src/flux/errors'
|
||||
Message = require '../../src/flux/models/message'
|
||||
TaskQueue = require '../../src/flux/stores/task-queue'
|
||||
|
@ -39,7 +38,7 @@ describe "SendDraftTask", ->
|
|||
expect(@sendA.shouldWaitForTask(@saveA)).toBe(true)
|
||||
|
||||
describe "performLocal", ->
|
||||
it "should throw an exception if the first parameter is not a localId", ->
|
||||
it "should throw an exception if the first parameter is not a clientId", ->
|
||||
badTasks = [new SendDraftTask()]
|
||||
goodTasks = [new SendDraftTask('localid-a')]
|
||||
caught = []
|
||||
|
@ -60,8 +59,10 @@ describe "SendDraftTask", ->
|
|||
|
||||
describe "performRemote", ->
|
||||
beforeEach ->
|
||||
@draftClientId = "local-123"
|
||||
@draft = new Message
|
||||
version: '1'
|
||||
clientId: @draftClientId
|
||||
id: '1233123AEDF1'
|
||||
accountId: 'A12ADE'
|
||||
subject: 'New Draft'
|
||||
|
@ -70,12 +71,13 @@ describe "SendDraftTask", ->
|
|||
to:
|
||||
name: 'Dummy'
|
||||
email: 'dummy@nylas.com'
|
||||
@draftLocalId = "local-123"
|
||||
@task = new SendDraftTask(@draftLocalId)
|
||||
@task = new SendDraftTask(@draftClientId)
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
|
||||
options.success?(@draft.toJSON())
|
||||
Promise.resolve(@draft.toJSON())
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) =>
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake (klass, id) =>
|
||||
Promise.resolve(@draft)
|
||||
spyOn(DatabaseStore, 'find').andCallFake (klass, id) =>
|
||||
Promise.resolve(@draft)
|
||||
spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) ->
|
||||
Promise.resolve()
|
||||
|
@ -90,7 +92,7 @@ describe "SendDraftTask", ->
|
|||
it "should notify the draft was sent", ->
|
||||
waitsForPromise => @task.performRemote().then =>
|
||||
args = Actions.sendDraftSuccess.calls[0].args[0]
|
||||
expect(args.draftLocalId).toBe @draftLocalId
|
||||
expect(args.draftClientId).toBe @draftClientId
|
||||
|
||||
it "get an object back on success", ->
|
||||
waitsForPromise => @task.performRemote().then =>
|
||||
|
@ -117,12 +119,12 @@ describe "SendDraftTask", ->
|
|||
expect(NylasAPI.makeRequest.calls.length).toBe(1)
|
||||
options = NylasAPI.makeRequest.mostRecentCall.args[0]
|
||||
expect(options.body.version).toBe(@draft.version)
|
||||
expect(options.body.draft_id).toBe(@draft.id)
|
||||
expect(options.body.draft_id).toBe(@draft.serverId)
|
||||
|
||||
describe "when the draft has not been saved", ->
|
||||
beforeEach ->
|
||||
@draft = new Message
|
||||
id: generateTempId()
|
||||
id: "local-12345"
|
||||
accountId: 'A12ADE'
|
||||
subject: 'New Draft'
|
||||
draft: true
|
||||
|
@ -130,7 +132,7 @@ describe "SendDraftTask", ->
|
|||
to:
|
||||
name: 'Dummy'
|
||||
email: 'dummy@nylas.com'
|
||||
@task = new SendDraftTask(@draftLocalId)
|
||||
@task = new SendDraftTask(@draftClientId)
|
||||
|
||||
it "should send the draft JSON", ->
|
||||
waitsForPromise =>
|
||||
|
@ -157,7 +159,7 @@ describe "SendDraftTask", ->
|
|||
beforeEach ->
|
||||
@draft = new Message
|
||||
version: '1'
|
||||
id: '1233123AEDF1'
|
||||
clientId: 'local-1234'
|
||||
accountId: 'A12ADE'
|
||||
threadId: 'threadId'
|
||||
replyToMessageId: 'replyToMessageId'
|
||||
|
@ -167,15 +169,15 @@ describe "SendDraftTask", ->
|
|||
to:
|
||||
name: 'Dummy'
|
||||
email: 'dummy@nylas.com'
|
||||
@task = new SendDraftTask(@draft.id)
|
||||
@task = new SendDraftTask("local-1234")
|
||||
spyOn(Actions, "dequeueTask")
|
||||
spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) ->
|
||||
Promise.resolve()
|
||||
|
||||
describe "when the server responds with `Invalid message public ID`", ->
|
||||
it "should resend the draft without the reply_to_message_id key set", ->
|
||||
@draft.id = generateTempId()
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake => Promise.resolve(@draft)
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake =>
|
||||
Promise.resolve(@draft)
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake ({body, success, error}) =>
|
||||
if body.reply_to_message_id
|
||||
err = new APIError(body: "Invalid message public id", statusCode: 400)
|
||||
|
@ -193,8 +195,7 @@ describe "SendDraftTask", ->
|
|||
|
||||
describe "when the server responds with `Invalid thread ID`", ->
|
||||
it "should resend the draft without the thread_id or reply_to_message_id keys set", ->
|
||||
@draft.id = generateTempId()
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake => Promise.resolve(@draft)
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake => Promise.resolve(@draft)
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake ({body, success, error}) =>
|
||||
new Promise (resolve, reject) =>
|
||||
if body.thread_id
|
||||
|
@ -214,21 +215,21 @@ describe "SendDraftTask", ->
|
|||
console.log(err.trace)
|
||||
|
||||
it "throws an error if the draft can't be found", ->
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake (klass, clientId) ->
|
||||
Promise.resolve()
|
||||
waitsForPromise =>
|
||||
@task.performRemote().catch (error) ->
|
||||
expect(error.message).toBeDefined()
|
||||
|
||||
it "throws an error if the draft isn't saved", ->
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
|
||||
Promise.resolve(isSaved: false)
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake (klass, clientId) ->
|
||||
Promise.resolve(serverId: null)
|
||||
waitsForPromise =>
|
||||
@task.performRemote().catch (error) ->
|
||||
expect(error.message).toBeDefined()
|
||||
|
||||
it "throws an error if the DB store has issues", ->
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake (klass, clientId) ->
|
||||
Promise.reject("DB error")
|
||||
waitsForPromise =>
|
||||
@task.performRemote().catch (error) ->
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
_ = require 'underscore'
|
||||
{generateTempId, isTempId} = require '../../src/flux/models/utils'
|
||||
|
||||
NylasAPI = require '../../src/flux/nylas-api'
|
||||
Task = require '../../src/flux/tasks/task'
|
||||
|
@ -33,30 +32,27 @@ testData =
|
|||
accountId: "abc123"
|
||||
body: '<body>123</body>'
|
||||
|
||||
localDraft = new Message _.extend {}, testData, {id: "local-id"}
|
||||
remoteDraft = new Message _.extend {}, testData, {id: "remoteid1234"}
|
||||
localDraft = -> new Message _.extend {}, testData, {clientId: "local-id"}
|
||||
remoteDraft = -> new Message _.extend {}, testData, {clientId: "local-id", serverId: "remoteid1234"}
|
||||
|
||||
describe "SyncbackDraftTask", ->
|
||||
beforeEach ->
|
||||
spyOn(DatabaseStore, "findByLocalId").andCallFake (klass, localId) ->
|
||||
if localId is "localDraftId" then Promise.resolve(localDraft)
|
||||
else if localId is "remoteDraftId" then Promise.resolve(remoteDraft)
|
||||
else if localId is "missingDraftId" then Promise.resolve()
|
||||
|
||||
spyOn(DatabaseStore, 'findBy').andCallFake (klass, matchers) ->
|
||||
spyOn(DatabaseStore, "findBy").andCallFake (klass, {clientId}) ->
|
||||
if klass is Account
|
||||
Promise.resolve(new Account(id: 'abc123'))
|
||||
return Promise.resolve(new Account(clientId: 'local-abc123', serverId: 'abc123'))
|
||||
|
||||
if clientId is "localDraftId" then Promise.resolve(localDraft())
|
||||
else if clientId is "remoteDraftId" then Promise.resolve(remoteDraft())
|
||||
else if clientId is "missingDraftId" then Promise.resolve()
|
||||
else return Promise.resolve()
|
||||
|
||||
spyOn(DatabaseStore, "persistModel").andCallFake ->
|
||||
Promise.resolve()
|
||||
|
||||
spyOn(DatabaseStore, "swapModel").andCallFake ->
|
||||
Promise.resolve()
|
||||
|
||||
describe "performRemote", ->
|
||||
beforeEach ->
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake (opts) ->
|
||||
Promise.resolve(remoteDraft.toJSON())
|
||||
Promise.resolve(remoteDraft().toJSON())
|
||||
|
||||
it "does nothing if no draft can be found in the db", ->
|
||||
task = new SyncbackDraftTask("missingDraftId")
|
||||
|
@ -101,33 +97,7 @@ describe "SyncbackDraftTask", ->
|
|||
options = NylasAPI.makeRequest.mostRecentCall.args[0]
|
||||
expect(options.returnsModel).toBe(false)
|
||||
|
||||
it "should swap the ids if we got a new one from the DB", ->
|
||||
task = new SyncbackDraftTask("localDraftId")
|
||||
waitsForPromise =>
|
||||
task.performRemote().then ->
|
||||
expect(DatabaseStore.swapModel).toHaveBeenCalled()
|
||||
expect(DatabaseStore.persistModel).not.toHaveBeenCalled()
|
||||
|
||||
it "should not swap the ids if we're using a persisted one", ->
|
||||
task = new SyncbackDraftTask("remoteDraftId")
|
||||
waitsForPromise =>
|
||||
task.performRemote().then ->
|
||||
expect(DatabaseStore.swapModel).not.toHaveBeenCalled()
|
||||
expect(DatabaseStore.persistModel).toHaveBeenCalled()
|
||||
|
||||
describe "When the api throws a 404 error", ->
|
||||
beforeEach ->
|
||||
spyOn(NylasAPI, "makeRequest").andCallFake (opts) ->
|
||||
Promise.reject(testError(opts))
|
||||
|
||||
it "resets the id", ->
|
||||
task = new SyncbackDraftTask("remoteDraftId")
|
||||
taskStatus = null
|
||||
task.performRemote().then (status) => taskStatus = status
|
||||
|
||||
waitsFor ->
|
||||
DatabaseStore.swapModel.calls.length > 0
|
||||
runs ->
|
||||
newDraft = DatabaseStore.swapModel.mostRecentCall.args[0].newModel
|
||||
expect(isTempId(newDraft.id)).toBe true
|
||||
expect(taskStatus).toBe(Task.Status.Retry)
|
||||
|
|
4
spec/fixtures/coffee.coffee
vendored
4
spec/fixtures/coffee.coffee
vendored
|
@ -1,4 +1,4 @@
|
|||
class quicksort
|
||||
class Quicksort
|
||||
sort: (items) ->
|
||||
return items if items.length <= 1
|
||||
|
||||
|
@ -20,4 +20,4 @@ class quicksort
|
|||
noop: ->
|
||||
# just a noop
|
||||
|
||||
exports.modules = quicksort
|
||||
exports.modules = Quicksort
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# This is a comment
|
||||
if this.studyingEconomics
|
||||
buy() while supply > demand
|
||||
sell() until supply > demand
|
4
spec/fixtures/sample-with-tabs.coffee
vendored
4
spec/fixtures/sample-with-tabs.coffee
vendored
|
@ -1,4 +0,0 @@
|
|||
# Econ 101
|
||||
if this.studyingEconomics
|
||||
buy() while supply > demand
|
||||
sell() until supply > demand
|
|
@ -19,6 +19,7 @@ ServiceHub = require 'service-hub'
|
|||
pathwatcher = require 'pathwatcher'
|
||||
clipboard = require 'clipboard'
|
||||
|
||||
Account = require "../src/flux/models/account"
|
||||
AccountStore = require "../src/flux/stores/account-store"
|
||||
Contact = require '../src/flux/models/contact'
|
||||
{TaskQueue, ComponentRegistry} = require "nylas-exports"
|
||||
|
@ -104,6 +105,10 @@ Promise.setScheduler (fn) ->
|
|||
setTimeout(fn, 0)
|
||||
process.nextTick -> advanceClock(1)
|
||||
|
||||
# So it passes the Utils.isTempId test
|
||||
window.TEST_ACCOUNT_CLIENT_ID = "local-test-account-client-id"
|
||||
window.TEST_ACCOUNT_ID = "test-account-server-id"
|
||||
|
||||
beforeEach ->
|
||||
atom.testOrganizationUnit = null
|
||||
Grim.clearDeprecations() if isCoreSpec
|
||||
|
@ -146,9 +151,13 @@ beforeEach ->
|
|||
spyOn(atom.menu, 'sendToBrowserProcess')
|
||||
|
||||
# Log in a fake user
|
||||
spyOn(AccountStore, 'current').andCallFake ->
|
||||
spyOn(AccountStore, 'current').andCallFake -> new Account
|
||||
name: "Nylas Test"
|
||||
provider: "gmail"
|
||||
emailAddress: 'tester@nylas.com'
|
||||
id: 'test_account_id'
|
||||
organizationUnit: atom.testOrganizationUnit
|
||||
clientId: TEST_ACCOUNT_CLIENT_ID
|
||||
serverId: TEST_ACCOUNT_ID
|
||||
usesLabels: -> atom.testOrganizationUnit is "label"
|
||||
usesFolders: -> atom.testOrganizationUnit is "folder"
|
||||
me: ->
|
||||
|
|
|
@ -250,7 +250,7 @@ class Application
|
|||
|
||||
@on 'application:send-feedback', => @windowManager.sendToMainWindow('send-feedback')
|
||||
@on 'application:open-preferences', => @windowManager.sendToMainWindow('open-preferences')
|
||||
@on 'application:show-main-window', => @windowManager.ensurePrimaryWindowOnscreen()
|
||||
@on 'application:show-main-window', => @windowManager.openWindowsForTokenState()
|
||||
@on 'application:show-work-window', => @windowManager.showWorkWindow()
|
||||
@on 'application:check-for-update', => @autoUpdateManager.check()
|
||||
@on 'application:install-update', =>
|
||||
|
@ -259,9 +259,9 @@ class Application
|
|||
@autoUpdateManager.install()
|
||||
@on 'application:open-dev', =>
|
||||
@devMode = true
|
||||
@windowManager.closeMainWindow()
|
||||
@windowManager.closeAllWindows()
|
||||
@windowManager.devMode = true
|
||||
@windowManager.ensurePrimaryWindowOnscreen()
|
||||
@windowManager.openWindowsForTokenState()
|
||||
|
||||
@on 'application:toggle-theme', =>
|
||||
themes = @config.get('core.themes') ? []
|
||||
|
@ -326,7 +326,7 @@ class Application
|
|||
@windowManager.sendToMainWindow('from-react-remote-window', json)
|
||||
|
||||
app.on 'activate-with-no-open-windows', (event) =>
|
||||
@windowManager.ensurePrimaryWindowOnscreen()
|
||||
@windowManager.openWindowsForTokenState()
|
||||
event.preventDefault()
|
||||
|
||||
ipc.on 'update-application-menu', (event, template, keystrokesByCommand) =>
|
||||
|
|
|
@ -13,11 +13,11 @@ components inside of your React render method. Rather than explicitly render
|
|||
a component, such as a `<Composer>`, you can use InjectedComponent:
|
||||
|
||||
```coffee
|
||||
<InjectedComponent matching={role:"Composer"} exposedProps={draftId:123} />
|
||||
<InjectedComponent matching={role:"Composer"} exposedProps={draftClientId:123} />
|
||||
```
|
||||
|
||||
InjectedComponent will look up the component registered with that role in the
|
||||
{ComponentRegistry} and render it, passing the exposedProps (`draftId={123}`) along.
|
||||
{ComponentRegistry} and render it, passing the exposedProps (`draftClientId={123}`) along.
|
||||
|
||||
InjectedComponent monitors the ComponentRegistry for changes. If a new component
|
||||
is registered that matches the descriptor you provide, InjectedComponent will refresh.
|
||||
|
|
|
@ -58,13 +58,6 @@ Section: General
|
|||
###
|
||||
class Actions
|
||||
|
||||
###
|
||||
Public: Fired when the {DatabaseStore} has changed the ID of a {Model}.
|
||||
|
||||
*Scope: Global*
|
||||
###
|
||||
@didSwapModel: ActionScopeGlobal
|
||||
|
||||
###
|
||||
Public: Fired when the Nylas API Connector receives new data from the API.
|
||||
|
||||
|
@ -427,7 +420,7 @@ class Actions
|
|||
```
|
||||
Actions.removeFile
|
||||
file: fileObject
|
||||
messageLocalId: draftLocalId
|
||||
messageClientId: draftClientId
|
||||
```
|
||||
###
|
||||
@removeFile: ActionScopeWindow
|
||||
|
|
|
@ -8,6 +8,7 @@ AttributeBoolean = require './attributes/attribute-boolean'
|
|||
AttributeDateTime = require './attributes/attribute-datetime'
|
||||
AttributeCollection = require './attributes/attribute-collection'
|
||||
AttributeJoinedData = require './attributes/attribute-joined-data'
|
||||
AttributeServerId = require './attributes/attribute-serverid'
|
||||
|
||||
module.exports =
|
||||
Matcher: Matcher
|
||||
|
@ -20,6 +21,7 @@ module.exports =
|
|||
DateTime: -> new AttributeDateTime(arguments...)
|
||||
Collection: -> new AttributeCollection(arguments...)
|
||||
JoinedData: -> new AttributeJoinedData(arguments...)
|
||||
ServerId: -> new AttributeServerId(arguments...)
|
||||
|
||||
AttributeNumber: AttributeNumber
|
||||
AttributeString: AttributeString
|
||||
|
@ -28,3 +30,4 @@ module.exports =
|
|||
AttributeDateTime: AttributeDateTime
|
||||
AttributeCollection: AttributeCollection
|
||||
AttributeJoinedData: AttributeJoinedData
|
||||
AttributeServerId: AttributeServerId
|
||||
|
|
|
@ -10,8 +10,7 @@ For example, Threads in N1 have a collection of Labels or Folders.
|
|||
When Collection attributes are marked as `queryable`, the DatabaseStore
|
||||
automatically creates a join table and maintains it as you create, save,
|
||||
and delete models. When you call `persistModel`, entries are added to the
|
||||
join table associating the ID of the model with the IDs of models in the
|
||||
collection.
|
||||
join table associating the ID of the model with the IDs of models in the collection.
|
||||
|
||||
Collection attributes have an additional clause builder, `contains`:
|
||||
|
||||
|
@ -28,9 +27,7 @@ WHERE `M1`.`value` = 'inbox'
|
|||
ORDER BY `Thread`.`last_message_received_timestamp` DESC
|
||||
```
|
||||
|
||||
The value of this attribute is always an array of ff other model objects. To use
|
||||
a Collection attribute, the JSON for the parent object must contain the nested
|
||||
objects, complete with their `object` field.
|
||||
The value of this attribute is always an array of other model objects.
|
||||
|
||||
Section: Database
|
||||
###
|
||||
|
@ -58,10 +55,11 @@ class AttributeCollection extends Attribute
|
|||
objs = []
|
||||
for objJSON in json
|
||||
obj = new @itemClass(objJSON)
|
||||
# Important: if no ids are in the JSON, don't make them up randomly.
|
||||
# This causes an object to be "different" each time it's de-serialized
|
||||
# even if it's actually the same, makes React components re-render!
|
||||
obj.id = undefined
|
||||
# Important: if no ids are in the JSON, don't make them up
|
||||
# randomly. This causes an object to be "different" each time it's
|
||||
# de-serialized even if it's actually the same, makes React
|
||||
# components re-render!
|
||||
obj.clientId = undefined
|
||||
obj.fromJSON(objJSON) if obj.fromJSON?
|
||||
objs.push(obj)
|
||||
objs
|
||||
|
|
|
@ -23,7 +23,7 @@ class AttributeObject extends Attribute
|
|||
# Important: if no ids are in the JSON, don't make them up randomly.
|
||||
# This causes an object to be "different" each time it's de-serialized
|
||||
# even if it's actually the same, makes React components re-render!
|
||||
obj.id = undefined
|
||||
obj.clientId = undefined
|
||||
# Warning: typeof(null) is object
|
||||
if obj.fromJSON and val and typeof(val) is 'object'
|
||||
obj.fromJSON(val)
|
||||
|
|
22
src/flux/attributes/attribute-serverid.coffee
Normal file
22
src/flux/attributes/attribute-serverid.coffee
Normal file
|
@ -0,0 +1,22 @@
|
|||
AttributeString = require './attribute-string'
|
||||
Matcher = require './matcher'
|
||||
|
||||
###
|
||||
Public: The value of this attribute is always a string or `null`.
|
||||
|
||||
String attributes can be queries using `equal`, `not`, and `startsWith`. Matching on
|
||||
`greaterThan` and `lessThan` is not supported.
|
||||
|
||||
Section: Database
|
||||
###
|
||||
class AttributeServerId extends AttributeString
|
||||
toJSON: (val) ->
|
||||
if val and Utils.isTempId(val)
|
||||
throw (new Error "AttributeServerId::toJSON (#{@modelKey}) #{val} does not look like a valid server id")
|
||||
|
||||
equal: (val) ->
|
||||
if val and Utils.isTempId(val)
|
||||
throw (new Error "AttributeServerId::equal (#{@modelKey}) #{val} is not a valid value for this field.")
|
||||
super
|
||||
|
||||
module.exports = AttributeString
|
|
@ -6,11 +6,6 @@ _ = require 'underscore'
|
|||
class Event extends Model
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
jsonKey: 'id'
|
||||
|
||||
'title': Attributes.String
|
||||
modelKey: 'title'
|
||||
jsonKey: 'title'
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
Model = require './model'
|
||||
Attributes = require '../attributes'
|
||||
|
||||
class LocalLink extends Model
|
||||
@attributes:
|
||||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
|
||||
'objectId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'objectId'
|
||||
|
||||
constructor: ({@id, @objectId} = {}) ->
|
||||
@
|
||||
|
||||
module.exports = LocalLink
|
|
@ -118,7 +118,7 @@ class Message extends Model
|
|||
'snippet': Attributes.String
|
||||
modelKey: 'snippet'
|
||||
|
||||
'threadId': Attributes.String
|
||||
'threadId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'threadId'
|
||||
jsonKey: 'thread_id'
|
||||
|
@ -140,7 +140,7 @@ class Message extends Model
|
|||
modelKey: 'version'
|
||||
queryable: true
|
||||
|
||||
'replyToMessageId': Attributes.String
|
||||
'replyToMessageId': Attributes.ServerId
|
||||
modelKey: 'replyToMessageId'
|
||||
jsonKey: 'reply_to_message_id'
|
||||
|
||||
|
@ -160,6 +160,7 @@ class Message extends Model
|
|||
@additionalSQLiteConfig:
|
||||
setup: ->
|
||||
['CREATE INDEX IF NOT EXISTS MessageListIndex ON Message(account_id, thread_id, date ASC)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS MessageDraftIndex ON Message(client_id)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS MessageBodyIndex ON MessageBody(id)']
|
||||
|
||||
constructor: ->
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
Attributes = require '../attributes'
|
||||
ModelQuery = require './query'
|
||||
{isTempId, generateTempId} = require './utils'
|
||||
_ = require 'underscore'
|
||||
Utils = require './utils'
|
||||
Attributes = require '../attributes'
|
||||
|
||||
###
|
||||
Public: A base class for API objects that provides abstract support for
|
||||
|
@ -9,7 +8,19 @@ serialization and deserialization, matching by attributes, and ID-based equality
|
|||
|
||||
## Attributes
|
||||
|
||||
`id`: {AttributeString} The ID of the model. Queryable.
|
||||
`id`: {AttributeString} The resolved canonical ID of the model used in the
|
||||
database and generally throughout the app. The id property is a custom
|
||||
getter that resolves to the serverId first, and then the clientId.
|
||||
|
||||
`clientId`: {AttributeString} An ID created at object construction and
|
||||
persists throughout the lifetime of the object. This is extremely useful
|
||||
for optimistically creating objects (like drafts and categories) and
|
||||
having a constant reference to it. In all other cases, use the resolved
|
||||
`id` field.
|
||||
|
||||
`serverId`: {AttributeServerId} The server ID of the model. In most cases,
|
||||
except optimistic creation, this will also be the canonical id of the
|
||||
object.
|
||||
|
||||
`object`: {AttributeString} The model's type. This field is used by the JSON
|
||||
deserializer to create an instance of the correct class when inflating the object.
|
||||
|
@ -20,15 +31,31 @@ Section: Models
|
|||
###
|
||||
class Model
|
||||
|
||||
Object.defineProperty @prototype, "id",
|
||||
enumerable: false
|
||||
get: -> @serverId ? @clientId
|
||||
set: ->
|
||||
throw new Error("You may not directly set the ID of an object. Set either the `clientId` or the `serverId` instead.")
|
||||
|
||||
@attributes:
|
||||
# Lookups will go through the custom getter.
|
||||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
|
||||
'clientId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
|
||||
'serverId': Attributes.ServerId
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
|
||||
'object': Attributes.String
|
||||
modelKey: 'object'
|
||||
|
||||
'accountId': Attributes.String
|
||||
'accountId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'accountId'
|
||||
jsonKey: 'account_id'
|
||||
|
@ -36,9 +63,13 @@ class Model
|
|||
@naturalSortOrder: -> null
|
||||
|
||||
constructor: (values = {}) ->
|
||||
if values["id"] and Utils.isTempId(values["id"])
|
||||
values["clientId"] ?= values["id"]
|
||||
else
|
||||
values["serverId"] ?= values["id"]
|
||||
for key, definition of @attributes()
|
||||
@[key] = values[key] if values[key]?
|
||||
@id ||= generateTempId()
|
||||
@clientId ?= Utils.generateTempId()
|
||||
@
|
||||
|
||||
clone: ->
|
||||
|
@ -47,12 +78,9 @@ class Model
|
|||
# Public: Returns an {Array} of {Attribute} objects defined on the Model's constructor
|
||||
#
|
||||
attributes: ->
|
||||
@constructor.attributes
|
||||
|
||||
# Public Returns true if the object has a server-provided ID, false otherwise.
|
||||
#
|
||||
isSaved: ->
|
||||
!isTempId(@id)
|
||||
attrs = _.clone(@constructor.attributes)
|
||||
delete attrs["id"]
|
||||
return attrs
|
||||
|
||||
##
|
||||
# Public: Inflates the model object from JSON, using the defined attributes to
|
||||
|
@ -63,6 +91,8 @@ class Model
|
|||
# This method is chainable.
|
||||
#
|
||||
fromJSON: (json) ->
|
||||
if json["id"] and not Utils.isTempId(json["id"])
|
||||
@serverId = json["id"]
|
||||
for key, attr of @attributes()
|
||||
@[key] = attr.fromJSON(json[attr.jsonKey]) unless json[attr.jsonKey] is undefined
|
||||
@
|
||||
|
@ -82,6 +112,7 @@ class Model
|
|||
if attr instanceof Attributes.AttributeJoinedData and options.joined is false
|
||||
continue
|
||||
json[attr.jsonKey] = value
|
||||
json["id"] = @id
|
||||
json
|
||||
|
||||
toString: ->
|
||||
|
|
|
@ -94,20 +94,6 @@ class Thread extends Model
|
|||
@lastMessageReceivedTimestamp ||= new Date(json['last_message_timestamp'] * 1000)
|
||||
@
|
||||
|
||||
# Public: Returns true if the thread has a {Category} with the given ID.
|
||||
#
|
||||
# * `id` A {String} {Category} ID
|
||||
#
|
||||
hasCategoryId: (id) ->
|
||||
return false unless id
|
||||
for folder in (@folders ? [])
|
||||
return true if folder.id is id
|
||||
for label in (@labels ? [])
|
||||
return true if label.id is id
|
||||
return false
|
||||
hasLabelId: (id) -> @hasCategoryId(id)
|
||||
hasFolderId: (id) -> @hasCategoryId(id)
|
||||
|
||||
# Public: Returns true if the thread has a {Category} with the given
|
||||
# name. Note, only `CategoryStore::standardCategories` have valid
|
||||
# `names`
|
||||
|
|
|
@ -211,7 +211,7 @@ Utils =
|
|||
|
||||
isEqualReact: (a, b, options={}) ->
|
||||
options.functionsAreEqual = true
|
||||
options.ignoreKeys = (options.ignoreKeys ? []).push("localId")
|
||||
options.ignoreKeys = (options.ignoreKeys ? []).push("clientId")
|
||||
Utils.isEqual(a, b, options)
|
||||
|
||||
# Customized version of Underscore 1.8.2's isEqual function
|
||||
|
|
|
@ -213,8 +213,9 @@ class NylasAPI
|
|||
|
||||
return Promise.resolve()
|
||||
|
||||
# Returns a Promsie that resolves when any parsed out models (if any)
|
||||
# Returns a Promise that resolves when any parsed out models (if any)
|
||||
# have been created and persisted to the database.
|
||||
#
|
||||
_handleModelResponse: (jsons) ->
|
||||
if not jsons
|
||||
return Promise.reject(new Error("handleModelResponse with no JSON provided"))
|
||||
|
@ -223,26 +224,49 @@ class NylasAPI
|
|||
if jsons.length is 0
|
||||
return Promise.resolve([])
|
||||
|
||||
# Run a few assertions to make sure we're not going to run into problems
|
||||
uniquedJSONs = _.uniq jsons, false, (model) -> model.id
|
||||
if uniquedJSONs.length < jsons.length
|
||||
console.warn("NylasAPI.handleModelResponse: called with non-unique object set. Maybe an API request returned the same object more than once?")
|
||||
|
||||
type = jsons[0].object
|
||||
klass = @_apiObjectToClassMap[type]
|
||||
if not klass
|
||||
console.warn("NylasAPI::handleModelResponse: Received unknown API object type: #{type}")
|
||||
return Promise.resolve([])
|
||||
|
||||
accepted = Promise.resolve(uniquedJSONs)
|
||||
if type is "thread" or type is "draft"
|
||||
accepted = @_acceptableModelsInResponse(klass, uniquedJSONs)
|
||||
# Step 1: Make sure the list of objects contains no duplicates, which cause
|
||||
# problems downstream when we try to write to the database.
|
||||
uniquedJSONs = _.uniq jsons, false, (model) -> model.id
|
||||
if uniquedJSONs.length < jsons.length
|
||||
console.warn("NylasAPI.handleModelResponse: called with non-unique object set. Maybe an API request returned the same object more than once?")
|
||||
|
||||
mapper = (json) -> (new klass).fromJSON(json)
|
||||
# Step 2: Filter out any objects locked by the optimistic change tracker.
|
||||
unlockedJSONs = _.filter uniquedJSONs, (json) =>
|
||||
if @_optimisticChangeTracker.acceptRemoteChangesTo(klass, json.id) is false
|
||||
json._delta?.ignoredBecause = "This model is locked by the optimistic change tracker"
|
||||
return false
|
||||
return true
|
||||
|
||||
accepted.map(mapper).then (objects) ->
|
||||
DatabaseStore.persistModels(objects).then ->
|
||||
return Promise.resolve(objects)
|
||||
# Step 3: Retrieve any existing models from the database for the given IDs.
|
||||
ids = _.pluck(unlockedJSONs, 'id')
|
||||
DatabaseStore = require './stores/database-store'
|
||||
DatabaseStore.findAll(klass).where(klass.attributes.id.in(ids)).then (models) ->
|
||||
existingModels = {}
|
||||
existingModels[model.id] = model for model in models
|
||||
|
||||
responseModels = []
|
||||
changedModels = []
|
||||
|
||||
# Step 4: Merge the response data into the existing data for each model,
|
||||
# skipping changes when we already have the given version
|
||||
unlockedJSONs.forEach (json) =>
|
||||
model = existingModels[json.id]
|
||||
unless model and model.version? and json.version? and model.version is json.version
|
||||
model ?= new klass()
|
||||
model.fromJSON(json)
|
||||
changedModels.push(model)
|
||||
responseModels.push(model)
|
||||
|
||||
# Step 5: Save models that have changed, and then return all of the models
|
||||
# that were in the response body.
|
||||
DatabaseStore.persistModels(changedModels).then ->
|
||||
return Promise.resolve(responseModels)
|
||||
|
||||
_apiObjectToClassMap:
|
||||
"file": require('./models/file')
|
||||
|
@ -257,25 +281,6 @@ class NylasAPI
|
|||
"calendar": require('./models/calendar')
|
||||
"metadata": require('./models/metadata')
|
||||
|
||||
_acceptableModelsInResponse: (klass, jsons) ->
|
||||
# Filter out models that are locked by pending optimistic changes
|
||||
accepted = jsons.filter (json) =>
|
||||
if @_optimisticChangeTracker.acceptRemoteChangesTo(klass, json.id) is false
|
||||
json._delta?.ignoredBecause = "This model is locked by the optimistic change tracker"
|
||||
return false
|
||||
return true
|
||||
|
||||
# Filter out models that already have newer versions in the local cache
|
||||
ids = _.pluck(accepted, 'id')
|
||||
DatabaseStore = require './stores/database-store'
|
||||
DatabaseStore.findVersions(klass, ids).then (versions) ->
|
||||
accepted = accepted.filter (json) ->
|
||||
if json.version and versions[json.id] >= json.version
|
||||
json._delta?.ignoredBecause = "This version (#{json.version}) is not newer. Already have (#{versions[json.id]})"
|
||||
return false
|
||||
return true
|
||||
Promise.resolve(accepted)
|
||||
|
||||
getThreads: (accountId, params = {}, requestOptions = {}) ->
|
||||
requestSuccess = requestOptions.success
|
||||
requestOptions.success = (json) =>
|
||||
|
|
|
@ -52,10 +52,10 @@ AnalyticsStore = Reflux.createStore
|
|||
composeReply: ({threadId, messageId}) -> {threadId, messageId}
|
||||
composeForward: ({threadId, messageId}) -> {threadId, messageId}
|
||||
composeReplyAll: ({threadId, messageId}) -> {threadId, messageId}
|
||||
composePopoutDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
|
||||
composePopoutDraft: (draftClientId) -> {draftClientId: draftClientId}
|
||||
composeNewBlankDraft: -> {}
|
||||
sendDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
|
||||
destroyDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
|
||||
sendDraft: (draftClientId) -> {draftClientId}
|
||||
destroyDraft: (draftClientId) -> {draftClientId}
|
||||
searchQueryCommitted: (query) -> {}
|
||||
fetchAndOpenFile: -> {}
|
||||
fetchAndSaveFile: -> {}
|
||||
|
@ -65,7 +65,7 @@ AnalyticsStore = Reflux.createStore
|
|||
coreGlobalActions: ->
|
||||
fileAborted: (uploadData={}) -> {fileSize: uploadData.fileSize}
|
||||
fileUploaded: (uploadData={}) -> {fileSize: uploadData.fileSize}
|
||||
sendDraftSuccess: ({draftLocalId}) -> {draftLocalId: draftLocalId}
|
||||
sendDraftSuccess: ({draftClientId}) -> {draftClientId}
|
||||
|
||||
track: (action, data={}) ->
|
||||
_.defer =>
|
||||
|
|
|
@ -4,8 +4,8 @@ async = require 'async'
|
|||
path = require 'path'
|
||||
sqlite3 = require 'sqlite3'
|
||||
Model = require '../models/model'
|
||||
Utils = require '../models/utils'
|
||||
Actions = require '../actions'
|
||||
LocalLink = require '../models/local-link'
|
||||
ModelQuery = require '../models/query'
|
||||
NylasStore = require '../../../exports/nylas-store'
|
||||
DatabaseSetupQueryBuilder = require './database-setup-query-builder'
|
||||
|
@ -14,12 +14,10 @@ PriorityUICoordinator = require '../../priority-ui-coordinator'
|
|||
{AttributeCollection, AttributeJoinedData} = require '../attributes'
|
||||
|
||||
{tableNameForJoin,
|
||||
generateTempId,
|
||||
serializeRegisteredObjects,
|
||||
deserializeRegisteredObjects,
|
||||
isTempId} = require '../models/utils'
|
||||
deserializeRegisteredObjects} = require '../models/utils'
|
||||
|
||||
DatabaseVersion = 59
|
||||
DatabaseVersion = 12
|
||||
|
||||
DatabasePhase =
|
||||
Setup: 'setup'
|
||||
|
@ -81,7 +79,6 @@ class DatabaseStore extends NylasStore
|
|||
|
||||
constructor: ->
|
||||
@_triggerPromise = null
|
||||
@_localIdLookupCache = {}
|
||||
@_inflightTransactions = 0
|
||||
@_open = false
|
||||
@_waiting = []
|
||||
|
@ -221,10 +218,11 @@ class DatabaseStore extends NylasStore
|
|||
str = results.map((row) -> row.detail).join('\n') + " for " + query
|
||||
@_prettyConsoleLog(str) if str.indexOf("SCAN") isnt -1
|
||||
|
||||
# Important: once the user begins a transaction, queries need to run in serial.
|
||||
# This ensures that the subsequent "COMMIT" call actually runs after the other
|
||||
# queries in the transaction, and that no other code can execute "BEGIN TRANS."
|
||||
# until the previously queued BEGIN/COMMIT have been processed.
|
||||
# Important: once the user begins a transaction, queries need to run
|
||||
# in serial. This ensures that the subsequent "COMMIT" call
|
||||
# actually runs after the other queries in the transaction, and that
|
||||
# no other code can execute "BEGIN TRANS." until the previously
|
||||
# queued BEGIN/COMMIT have been processed.
|
||||
|
||||
# We don't exit serial execution mode until the last pending transaction has
|
||||
# finished executing.
|
||||
|
@ -316,8 +314,7 @@ class DatabaseStore extends NylasStore
|
|||
new ModelQuery(klass, @).where(predicates).count()
|
||||
|
||||
# Public: Modelify converts the provided array of IDs or models (or a mix of
|
||||
# IDs and models) into an array of models of the `klass` provided by querying
|
||||
# for the missing items.
|
||||
# IDs and models) into an array of models of the `klass` provided by querying for the missing items.
|
||||
#
|
||||
# Modelify is efficient and uses a single database query. It resolves Immediately
|
||||
# if no query is necessary.
|
||||
|
@ -353,106 +350,6 @@ class DatabaseStore extends NylasStore
|
|||
|
||||
return Promise.resolve(arr)
|
||||
|
||||
###
|
||||
Support for Local IDs
|
||||
###
|
||||
|
||||
# Public: Retrieve a Model given a localId.
|
||||
#
|
||||
# - `class` The class of the {Model} you're trying to retrieve.
|
||||
# - `localId` The {String} localId of the object.
|
||||
#
|
||||
# Returns a {Promise} that:
|
||||
# - resolves with the Model associated with the localId
|
||||
# - rejects if no matching object is found
|
||||
#
|
||||
# Note: When fetching an object by local Id, joined attributes
|
||||
# (like body, stored in a separate table) are always included.
|
||||
#
|
||||
findByLocalId: (klass, localId) =>
|
||||
return Promise.reject(new Error("DatabaseStore::findByLocalId - You must provide a class")) unless klass
|
||||
return Promise.reject(new Error("DatabaseStore::findByLocalId - You must provide a localId")) unless localId
|
||||
|
||||
new Promise (resolve, reject) =>
|
||||
@find(LocalLink, localId).then (link) =>
|
||||
return reject(new Error("DatabaseStore::findByLocalId - no LocalLink found")) unless link
|
||||
query = @find(klass, link.objectId).includeAll().then(resolve)
|
||||
|
||||
# Public: Give a Model a localId.
|
||||
#
|
||||
# - `model` A {Model} object to assign a localId.
|
||||
# - `localId` (optional) The {String} localId. If you don't pass a LocalId, one
|
||||
# will be automatically assigned.
|
||||
#
|
||||
# Returns a {Promise} that:
|
||||
# - resolves with the localId assigned to the model
|
||||
bindToLocalId: (model, localId = null) =>
|
||||
return Promise.reject(new Error("DatabaseStore::bindToLocalId - You must provide a model")) unless model
|
||||
return Promise.reject(new Error("DatabaseStore::bindToLocalId - Recieved a model with no ID")) unless model.id?
|
||||
|
||||
new Promise (resolve, reject) =>
|
||||
unless localId
|
||||
if isTempId(model.id)
|
||||
localId = model.id
|
||||
else
|
||||
localId = generateTempId()
|
||||
|
||||
link = new LocalLink({id: localId, objectId: model.id})
|
||||
@_localIdLookupCache[model.id] = localId
|
||||
|
||||
@persistModel(link).then ->
|
||||
resolve(localId)
|
||||
.catch(reject)
|
||||
|
||||
# Public: Look up the localId assigned to the model. If no localId has been
|
||||
# assigned to the model yet, it assigns a new one and persists it to the database.
|
||||
#
|
||||
# - `model` A {Model} object to assign a localId.
|
||||
#
|
||||
# Returns a {Promise} that:
|
||||
# - resolves with the {String} localId.
|
||||
localIdForModel: (model) =>
|
||||
return Promise.reject(new Error("DatabaseStore::localIdForModel - You must provide a model")) unless model
|
||||
|
||||
new Promise (resolve, reject) =>
|
||||
if @_localIdLookupCache[model.id]
|
||||
return resolve(@_localIdLookupCache[model.id])
|
||||
|
||||
@findBy(LocalLink, {objectId: model.id}).then (link) =>
|
||||
if link
|
||||
@_localIdLookupCache[model.id] = link.id
|
||||
resolve(link.id)
|
||||
else
|
||||
@bindToLocalId(model).then(resolve).catch(reject)
|
||||
|
||||
# Private: Returns an {Object} with id-version key value pairs for models in
|
||||
# the ID set provided. Does not retrieve or inflate object JSON from the database.
|
||||
#
|
||||
# Using this method requires that the klass has declared the version field queryable.
|
||||
#
|
||||
findVersions: (klass, allIds) =>
|
||||
return Promise.reject(new Error("DatabaseStore::findVersions - You must provide a class")) unless klass
|
||||
return Promise.reject(new Error("DatabaseStore::findVersions - version field must be queryable")) unless klass.attributes.version.queryable
|
||||
|
||||
_findVersionsFor = (ids) =>
|
||||
marks = new Array(ids.length)
|
||||
marks[idx] = '?' for m, idx in marks
|
||||
@_query("SELECT id, version FROM `#{klass.name}` WHERE id IN (#{marks.join(",")})", ids).then (results) ->
|
||||
map = {}
|
||||
map[id] = version for {id, version} in results
|
||||
Promise.resolve(map)
|
||||
|
||||
promises = []
|
||||
while allIds.length > 0
|
||||
promises.push(_findVersionsFor(allIds.splice(0, 100)))
|
||||
|
||||
# We can only use WHERE IN for up to ~250 items at a time. Run a query for
|
||||
# every 100 items and then combine the results before returning.
|
||||
Promise.all(promises).then (results) =>
|
||||
all = {}
|
||||
all = _.extend(all, result) for result in results
|
||||
Promise.resolve(all)
|
||||
|
||||
# Public: Executes a {ModelQuery} on the local database.
|
||||
#
|
||||
# - `modelQuery` A {ModelQuery} to execute.
|
||||
|
@ -530,34 +427,6 @@ class DatabaseStore extends NylasStore
|
|||
]).then =>
|
||||
@_triggerSoon({objectClass: model.constructor.name, objects: [model], type: 'unpersist'})
|
||||
|
||||
# Public: Given an `oldModel` with a unique `localId`, it will swap the
|
||||
# item out in the database.
|
||||
#
|
||||
# - `args` An arguments hash with:
|
||||
# - `oldModel` The old model
|
||||
# - `newModel` The new model
|
||||
# - `localId` The localId to reference
|
||||
#
|
||||
# Returns a {Promise} that
|
||||
# - resolves after the database queries are complete and any listening
|
||||
# database callbacks have finished
|
||||
# - rejects if any databse query fails or one of the triggering
|
||||
# callbacks failed
|
||||
swapModel: ({oldModel, newModel, localId}) =>
|
||||
queryPromise = Promise.all([
|
||||
@_query(BEGIN_TRANSACTION)
|
||||
@_deleteModel(oldModel)
|
||||
@_writeModels([newModel])
|
||||
@_writeModels([new LocalLink(id: localId, objectId: newModel.id)]) if localId
|
||||
@_query(COMMIT)
|
||||
]).then =>
|
||||
Actions.didSwapModel({
|
||||
oldModel: oldModel,
|
||||
newModel: newModel,
|
||||
localId: localId
|
||||
})
|
||||
@_triggerSoon({objectClass: newModel.constructor.name, objects: [oldModel, newModel], type: 'swap'})
|
||||
|
||||
persistJSONObject: (key, json) ->
|
||||
jsonString = serializeRegisteredObjects(json)
|
||||
@_query(BEGIN_TRANSACTION)
|
||||
|
|
|
@ -22,7 +22,7 @@ DraftChangeSet associated with the store proxy. The DraftChangeSet does two thin
|
|||
Section: Drafts
|
||||
###
|
||||
class DraftChangeSet
|
||||
constructor: (@localId, @_onChange) ->
|
||||
constructor: (@clientId, @_onChange) ->
|
||||
@_commitChain = Promise.resolve()
|
||||
@_pending = {}
|
||||
@_saving = {}
|
||||
|
@ -51,19 +51,19 @@ class DraftChangeSet
|
|||
return Promise.resolve(true)
|
||||
|
||||
DatabaseStore = require './database-store'
|
||||
return DatabaseStore.findByLocalId(Message, @localId).then (draft) =>
|
||||
DatabaseStore.findBy(Message, clientId: @clientId).then (draft) =>
|
||||
if @_destroyed
|
||||
return Promise.resolve(true)
|
||||
|
||||
if not draft
|
||||
throw new Error("DraftChangeSet.commit: Assertion failure. Draft #{@localId} is not in the database.")
|
||||
throw new Error("DraftChangeSet.commit: Assertion failure. Draft #{@clientId} is not in the database.")
|
||||
|
||||
@_saving = @_pending
|
||||
@_pending = {}
|
||||
draft = @applyToModel(draft)
|
||||
|
||||
return DatabaseStore.persistModel(draft).then =>
|
||||
syncback = new SyncbackDraftTask(@localId)
|
||||
syncback = new SyncbackDraftTask(@clientId)
|
||||
Actions.queueTask(syncback)
|
||||
@_saving = {}
|
||||
|
||||
|
@ -94,17 +94,16 @@ class DraftStoreProxy
|
|||
@include Publisher
|
||||
@include Listener
|
||||
|
||||
constructor: (@draftLocalId, draft = null) ->
|
||||
constructor: (@draftClientId, draft = null) ->
|
||||
DraftStore = require './draft-store'
|
||||
|
||||
@listenTo DraftStore, @_onDraftChanged
|
||||
@listenTo Actions.didSwapModel, @_onDraftSwapped
|
||||
|
||||
@_draft = false
|
||||
@_draftPristineBody = null
|
||||
@_destroyed = false
|
||||
|
||||
@changes = new DraftChangeSet @draftLocalId, =>
|
||||
@changes = new DraftChangeSet @draftClientId, =>
|
||||
return if @_destroyed
|
||||
if !@_draft
|
||||
throw new Error("DraftChangeSet was modified before the draft was prepared.")
|
||||
|
@ -115,7 +114,7 @@ class DraftStoreProxy
|
|||
@_draftPromise = Promise.resolve(@)
|
||||
|
||||
@prepare()
|
||||
|
||||
|
||||
# Public: Returns the draft object with the latest changes applied.
|
||||
#
|
||||
draft: ->
|
||||
|
@ -131,9 +130,9 @@ class DraftStoreProxy
|
|||
|
||||
prepare: ->
|
||||
DatabaseStore = require './database-store'
|
||||
@_draftPromise ?= DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
|
||||
@_draftPromise ?= DatabaseStore.findBy(Message, clientId: @draftClientId).then (draft) =>
|
||||
return Promise.reject(new Error("Draft has been destroyed.")) if @_destroyed
|
||||
return Promise.reject(new Error("Assertion Failure: Draft #{@draftLocalId} not found.")) if not draft
|
||||
return Promise.reject(new Error("Assertion Failure: Draft #{@draftClientId} not found.")) if not draft
|
||||
@_setDraft(draft)
|
||||
Promise.resolve(@)
|
||||
@_draftPromise
|
||||
|
@ -167,12 +166,4 @@ class DraftStoreProxy
|
|||
@_draft = _.extend @_draft, _.last(myDrafts)
|
||||
@trigger()
|
||||
|
||||
_onDraftSwapped: (change) ->
|
||||
# A draft was saved with a new ID. Since we use the draft ID to
|
||||
# watch for changes to our draft, we need to pull again using our
|
||||
# localId.
|
||||
if change.oldModel.id is @_draft.id
|
||||
@_setDraft(change.newModel)
|
||||
|
||||
|
||||
module.exports = DraftStoreProxy
|
||||
|
|
|
@ -18,7 +18,7 @@ Actions = require '../actions'
|
|||
|
||||
TaskQueue = require './task-queue'
|
||||
|
||||
{subjectWithPrefix, generateTempId} = require '../models/utils'
|
||||
{subjectWithPrefix} = require '../models/utils'
|
||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
CoffeeHelpers = require '../coffee-helpers'
|
||||
DOMUtils = require '../../dom-utils'
|
||||
|
@ -46,7 +46,7 @@ class DraftStore
|
|||
@listenTo Actions.composeReply, @_onComposeReply
|
||||
@listenTo Actions.composeForward, @_onComposeForward
|
||||
@listenTo Actions.composeReplyAll, @_onComposeReplyAll
|
||||
@listenTo Actions.composePopoutDraft, @_onPopoutDraftLocalId
|
||||
@listenTo Actions.composePopoutDraft, @_onPopoutDraftClientId
|
||||
@listenTo Actions.composeNewBlankDraft, @_onPopoutBlankDraft
|
||||
|
||||
atom.commands.add 'body',
|
||||
|
@ -89,30 +89,30 @@ class DraftStore
|
|||
######### PUBLIC #######################################################
|
||||
|
||||
# Public: Fetch a {DraftStoreProxy} for displaying and/or editing the
|
||||
# draft with `localId`.
|
||||
# draft with `clientId`.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# ```coffee
|
||||
# session = DraftStore.sessionForLocalId(localId)
|
||||
# session = DraftStore.sessionForClientId(clientId)
|
||||
# session.prepare().then ->
|
||||
# # session.draft() is now ready
|
||||
# ```
|
||||
#
|
||||
# - `localId` The {String} local ID of the draft.
|
||||
# - `clientId` The {String} clientId of the draft.
|
||||
#
|
||||
# Returns a {Promise} that resolves to an {DraftStoreProxy} for the
|
||||
# draft once it has been prepared:
|
||||
sessionForLocalId: (localId) =>
|
||||
if not localId
|
||||
throw new Error("DraftStore::sessionForLocalId requires a localId")
|
||||
@_draftSessions[localId] ?= new DraftStoreProxy(localId)
|
||||
@_draftSessions[localId].prepare()
|
||||
sessionForClientId: (clientId) =>
|
||||
if not clientId
|
||||
throw new Error("DraftStore::sessionForClientId requires a clientId")
|
||||
@_draftSessions[clientId] ?= new DraftStoreProxy(clientId)
|
||||
@_draftSessions[clientId].prepare()
|
||||
|
||||
# Public: Look up the sending state of the given draft Id.
|
||||
# Public: Look up the sending state of the given draftClientId.
|
||||
# In popout windows the existance of the window is the sending state.
|
||||
isSendingDraft: (draftLocalId) ->
|
||||
return @_draftsSending[draftLocalId]?
|
||||
isSendingDraft: (draftClientId) ->
|
||||
return @_draftsSending[draftClientId]?
|
||||
|
||||
###
|
||||
Composer Extensions
|
||||
|
@ -142,7 +142,7 @@ class DraftStore
|
|||
|
||||
_doneWithSession: (session) ->
|
||||
session.teardown()
|
||||
delete @_draftSessions[session.draftLocalId]
|
||||
delete @_draftSessions[session.draftClientId]
|
||||
|
||||
_onBeforeUnload: =>
|
||||
promises = []
|
||||
|
@ -153,7 +153,7 @@ class DraftStore
|
|||
# window.close() within on onbeforeunload could do weird things.
|
||||
for key, session of @_draftSessions
|
||||
if session.draft()?.pristine
|
||||
Actions.queueTask(new DestroyDraftTask(draftLocalId: session.draftLocalId))
|
||||
Actions.queueTask(new DestroyDraftTask(draftClientId: session.draftClientId))
|
||||
else
|
||||
promises.push(session.changes.commit())
|
||||
|
||||
|
@ -204,19 +204,12 @@ class DraftStore
|
|||
continue unless extension.prepareNewDraft
|
||||
extension.prepareNewDraft(draft)
|
||||
|
||||
# Normally we'd allow the DatabaseStore to create a localId, wait for it to
|
||||
# commit a LocalLink and resolve, etc. but it's faster to create one now.
|
||||
draftLocalId = generateTempId()
|
||||
|
||||
# Optimistically create a draft session and hand it the draft so that it
|
||||
# doesn't need to do a query for it a second from now when the composer wants it.
|
||||
@_draftSessions[draftLocalId] = new DraftStoreProxy(draftLocalId, draft)
|
||||
@_draftSessions[draft.clientId] = new DraftStoreProxy(draft.clientId, draft)
|
||||
|
||||
Promise.all([
|
||||
DatabaseStore.bindToLocalId(draft, draftLocalId)
|
||||
DatabaseStore.persistModel(draft)
|
||||
]).then =>
|
||||
return Promise.resolve({draftLocalId})
|
||||
DatabaseStore.persistModel(draft).then =>
|
||||
Promise.resolve(draftClientId: draft.clientId)
|
||||
|
||||
_newMessageWithContext: ({thread, threadId, message, messageId, popout}, attributesCallback) =>
|
||||
return unless AccountStore.current()
|
||||
|
@ -254,34 +247,34 @@ class DraftStore
|
|||
DOMUtils.escapeHTMLCharacters(_.invoke(cs, "toString").join(", "))
|
||||
|
||||
if attributes.replyToMessage
|
||||
msg = attributes.replyToMessage
|
||||
replyToMessage = attributes.replyToMessage
|
||||
|
||||
attributes.subject = subjectWithPrefix(msg.subject, 'Re:')
|
||||
attributes.replyToMessageId = msg.id
|
||||
attributes.subject = subjectWithPrefix(replyToMessage.subject, 'Re:')
|
||||
attributes.replyToMessageId = replyToMessage.id
|
||||
attributes.body = """
|
||||
<br><br><blockquote class="gmail_quote"
|
||||
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||
#{DOMUtils.escapeHTMLCharacters(msg.replyAttributionLine())}
|
||||
#{DOMUtils.escapeHTMLCharacters(replyToMessage.replyAttributionLine())}
|
||||
<br>
|
||||
#{@_formatBodyForQuoting(msg.body)}
|
||||
#{@_formatBodyForQuoting(replyToMessage.body)}
|
||||
</blockquote>"""
|
||||
delete attributes.quotedMessage
|
||||
|
||||
if attributes.forwardMessage
|
||||
msg = attributes.forwardMessage
|
||||
forwardMessage = attributes.forwardMessage
|
||||
fields = []
|
||||
fields.push("From: #{contactsAsHtml(msg.from)}") if msg.from.length > 0
|
||||
fields.push("Subject: #{msg.subject}")
|
||||
fields.push("Date: #{msg.formattedDate()}")
|
||||
fields.push("To: #{contactsAsHtml(msg.to)}") if msg.to.length > 0
|
||||
fields.push("CC: #{contactsAsHtml(msg.cc)}") if msg.cc.length > 0
|
||||
fields.push("BCC: #{contactsAsHtml(msg.bcc)}") if msg.bcc.length > 0
|
||||
fields.push("From: #{contactsAsHtml(forwardMessage.from)}") if forwardMessage.from.length > 0
|
||||
fields.push("Subject: #{forwardMessage.subject}")
|
||||
fields.push("Date: #{forwardMessage.formattedDate()}")
|
||||
fields.push("To: #{contactsAsHtml(forwardMessage.to)}") if forwardMessage.to.length > 0
|
||||
fields.push("CC: #{contactsAsHtml(forwardMessage.cc)}") if forwardMessage.cc.length > 0
|
||||
fields.push("BCC: #{contactsAsHtml(forwardMessage.bcc)}") if forwardMessage.bcc.length > 0
|
||||
|
||||
if msg.files?.length > 0
|
||||
if forwardMessage.files?.length > 0
|
||||
attributes.files ?= []
|
||||
attributes.files = attributes.files.concat(msg.files)
|
||||
attributes.files = attributes.files.concat(forwardMessage.files)
|
||||
|
||||
attributes.subject = subjectWithPrefix(msg.subject, 'Fwd:')
|
||||
attributes.subject = subjectWithPrefix(forwardMessage.subject, 'Fwd:')
|
||||
attributes.body = """
|
||||
<br><br><blockquote class="gmail_quote"
|
||||
style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
|
||||
|
@ -289,7 +282,7 @@ class DraftStore
|
|||
<br><br>
|
||||
#{fields.join('<br>')}
|
||||
<br><br>
|
||||
#{@_formatBodyForQuoting(msg.body)}
|
||||
#{@_formatBodyForQuoting(forwardMessage.body)}
|
||||
</blockquote>"""
|
||||
delete attributes.forwardedMessage
|
||||
|
||||
|
@ -301,8 +294,8 @@ class DraftStore
|
|||
threadId: thread.id
|
||||
accountId: thread.accountId
|
||||
|
||||
@_finalizeAndPersistNewMessage(draft).then ({draftLocalId}) =>
|
||||
Actions.composePopoutDraft(draftLocalId) if popout
|
||||
@_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
|
||||
Actions.composePopoutDraft(draftClientId) if popout
|
||||
|
||||
|
||||
# Eventually we'll want a nicer solution for inline attachments
|
||||
|
@ -325,18 +318,18 @@ class DraftStore
|
|||
pristine: true
|
||||
accountId: account.id
|
||||
|
||||
@_finalizeAndPersistNewMessage(draft).then ({draftLocalId}) =>
|
||||
@_onPopoutDraftLocalId(draftLocalId, {newDraft: true})
|
||||
@_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
|
||||
@_onPopoutDraftClientId(draftClientId, {newDraft: true})
|
||||
|
||||
_onPopoutDraftLocalId: (draftLocalId, options = {}) =>
|
||||
_onPopoutDraftClientId: (draftClientId, options = {}) =>
|
||||
return unless AccountStore.current()
|
||||
|
||||
if not draftLocalId?
|
||||
throw new Error("DraftStore::onPopoutDraftLocalId - You must provide a draftLocalId")
|
||||
if not draftClientId?
|
||||
throw new Error("DraftStore::onPopoutDraftId - You must provide a draftClientId")
|
||||
|
||||
save = Promise.resolve()
|
||||
if @_draftSessions[draftLocalId]
|
||||
save = @_draftSessions[draftLocalId].changes.commit()
|
||||
if @_draftSessions[draftClientId]
|
||||
save = @_draftSessions[draftClientId].changes.commit()
|
||||
|
||||
title = if options.newDraft then "New Message" else "Message"
|
||||
|
||||
|
@ -344,7 +337,7 @@ class DraftStore
|
|||
atom.newWindow
|
||||
title: title
|
||||
windowType: "composer"
|
||||
windowProps: _.extend(options, {draftLocalId})
|
||||
windowProps: _.extend(options, {draftClientId})
|
||||
|
||||
_onHandleMailtoLink: (urlString) =>
|
||||
account = AccountStore.current()
|
||||
|
@ -374,32 +367,32 @@ class DraftStore
|
|||
if query[attr]
|
||||
draft[attr] = ContactStore.parseContactsInString(query[attr])
|
||||
|
||||
@_finalizeAndPersistNewMessage(draft).then ({draftLocalId}) =>
|
||||
@_onPopoutDraftLocalId(draftLocalId)
|
||||
@_finalizeAndPersistNewMessage(draft).then ({draftClientId}) =>
|
||||
@_onPopoutDraftClientId({draftClientId})
|
||||
|
||||
_onDestroyDraft: (draftLocalId) =>
|
||||
session = @_draftSessions[draftLocalId]
|
||||
_onDestroyDraft: (draftClientId) =>
|
||||
session = @_draftSessions[draftClientId]
|
||||
|
||||
# Immediately reset any pending changes so no saves occur
|
||||
if session
|
||||
@_doneWithSession(session)
|
||||
|
||||
# Queue the task to destroy the draft
|
||||
Actions.queueTask(new DestroyDraftTask(draftLocalId: draftLocalId))
|
||||
Actions.queueTask(new DestroyDraftTask(draftClientId: draftClientId))
|
||||
|
||||
atom.close() if @_isPopout()
|
||||
|
||||
# The user request to send the draft
|
||||
_onSendDraft: (draftLocalId) =>
|
||||
@_draftsSending[draftLocalId] = true
|
||||
@trigger(draftLocalId)
|
||||
_onSendDraft: (draftClientId) =>
|
||||
@_draftsSending[draftClientId] = true
|
||||
@trigger(draftClientId)
|
||||
|
||||
@sessionForLocalId(draftLocalId).then (session) =>
|
||||
@sessionForClientId(draftClientId).then (session) =>
|
||||
@_runExtensionsBeforeSend(session)
|
||||
|
||||
# Immediately save any pending changes so we don't save after sending
|
||||
session.changes.commit().then =>
|
||||
task = new SendDraftTask(draftLocalId, {fromPopout: @_isPopout()})
|
||||
task = new SendDraftTask(draftClientId, {fromPopout: @_isPopout()})
|
||||
Actions.queueTask(task)
|
||||
@_doneWithSession(session)
|
||||
atom.close() if @_isPopout()
|
||||
|
@ -413,8 +406,8 @@ class DraftStore
|
|||
continue unless extension.finalizeSessionBeforeSending
|
||||
extension.finalizeSessionBeforeSending(session)
|
||||
|
||||
_onRemoveFile: ({file, messageLocalId}) =>
|
||||
@sessionForLocalId(messageLocalId).then (session) ->
|
||||
_onRemoveFile: ({file, messageClientId}) =>
|
||||
@sessionForClientId(messageClientId).then (session) ->
|
||||
files = _.clone(session.draft().files) ? []
|
||||
files = _.reject files, (f) -> f.id is file.id
|
||||
session.changes.add({files}, immediate: true)
|
||||
|
|
|
@ -59,7 +59,7 @@ class Download
|
|||
return @promise if @promise
|
||||
|
||||
@promise = new Promise (resolve, reject) =>
|
||||
account = AccountStore.current()?.id
|
||||
accountId = AccountStore.current()?.id
|
||||
stream = fs.createWriteStream(@targetPath)
|
||||
finished = false
|
||||
finishedAction = null
|
||||
|
@ -82,7 +82,7 @@ class Download
|
|||
NylasAPI.makeRequest
|
||||
json: false
|
||||
path: "/files/#{@fileId}/download"
|
||||
accountId: account
|
||||
accountId: accountId
|
||||
encoding: null # Tell `request` not to parse the response data
|
||||
started: (req) =>
|
||||
@request = req
|
||||
|
|
|
@ -20,7 +20,7 @@ FileUploadStore = Reflux.createStore
|
|||
@listenTo Actions.fileAborted, @_onFileAborted
|
||||
|
||||
# We don't save uploads to the DB, we keep it in memory in the store.
|
||||
# The key is the messageLocalId. The value is a hash of paths and
|
||||
# The key is the messageClientId. The value is a hash of paths and
|
||||
# corresponding upload data.
|
||||
@_fileUploads = {}
|
||||
@_linkedFiles = {}
|
||||
|
@ -28,18 +28,18 @@ FileUploadStore = Reflux.createStore
|
|||
|
||||
######### PUBLIC #######################################################
|
||||
|
||||
uploadsForMessage: (messageLocalId) ->
|
||||
if not messageLocalId? then return []
|
||||
uploadsForMessage: (messageClientId) ->
|
||||
if not messageClientId? then return []
|
||||
_.filter @_fileUploads, (uploadData, uploadKey) ->
|
||||
uploadData.messageLocalId is messageLocalId
|
||||
uploadData.messageClientId is messageClientId
|
||||
|
||||
linkedUpload: (file) -> @_linkedFiles[file.id]
|
||||
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
_onAttachFile: ({messageLocalId}) ->
|
||||
@_verifyId(messageLocalId)
|
||||
_onAttachFile: ({messageClientId}) ->
|
||||
@_verifyId(messageClientId)
|
||||
|
||||
# When the dialog closes, it triggers `Actions.pathsToOpen`
|
||||
atom.showOpenDialog {properties: ['openFile', 'multiSelections']}, (pathsToOpen) ->
|
||||
|
@ -47,7 +47,7 @@ FileUploadStore = Reflux.createStore
|
|||
pathsToOpen = [pathsToOpen] if _.isString(pathsToOpen)
|
||||
|
||||
pathsToOpen.forEach (path) ->
|
||||
Actions.attachFilePath({messageLocalId, path})
|
||||
Actions.attachFilePath({messageClientId, path})
|
||||
|
||||
_onAttachFileError: (message) ->
|
||||
remote = require('remote')
|
||||
|
@ -58,8 +58,8 @@ FileUploadStore = Reflux.createStore
|
|||
message: 'Cannot Attach File',
|
||||
detail: message
|
||||
|
||||
_onAttachFilePath: ({messageLocalId, path}) ->
|
||||
@_verifyId(messageLocalId)
|
||||
_onAttachFilePath: ({messageClientId, path}) ->
|
||||
@_verifyId(messageClientId)
|
||||
fs.stat path, (err, stats) =>
|
||||
filename = require('path').basename(path)
|
||||
if err
|
||||
|
@ -67,12 +67,12 @@ FileUploadStore = Reflux.createStore
|
|||
else if stats.isDirectory()
|
||||
@_onAttachFileError("#{filename} is a directory. Try compressing it and attaching it again.")
|
||||
else
|
||||
Actions.queueTask(new FileUploadTask(path, messageLocalId))
|
||||
Actions.queueTask(new FileUploadTask(path, messageClientId))
|
||||
|
||||
# Receives:
|
||||
# uploadData:
|
||||
# uploadTaskId - A unique id
|
||||
# messageLocalId - The localId of the message (draft) we're uploading to
|
||||
# messageClientId - The clientId of the message (draft) we're uploading to
|
||||
# filePath - The full absolute local system file path
|
||||
# fileSize - The size in bytes
|
||||
# fileName - The basename of the file
|
||||
|
@ -107,6 +107,6 @@ FileUploadStore = Reflux.createStore
|
|||
delete @_fileUploads[uploadData.uploadTaskId]
|
||||
@trigger()
|
||||
|
||||
_verifyId: (messageLocalId) ->
|
||||
if messageLocalId.blank?
|
||||
_verifyId: (messageClientId) ->
|
||||
if messageClientId.blank?
|
||||
throw new Error "You need to pass the ID of the message (draft) this Action refers to"
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
_ = require 'underscore'
|
||||
Reflux = require 'reflux'
|
||||
|
||||
Utils = require '../models/utils'
|
||||
Actions = require '../actions'
|
||||
NylasStore = require 'nylas-store'
|
||||
MessageStore = require './message-store'
|
||||
AccountStore = require './account-store'
|
||||
FocusedContentStore = require './focused-content-store'
|
||||
|
||||
# A store that handles the focuses collections of and individual contacts
|
||||
module.exports =
|
||||
FocusedContactsStore = Reflux.createStore
|
||||
init: ->
|
||||
class FocusedContactsStore extends NylasStore
|
||||
constructor: ->
|
||||
@listenTo Actions.focusContact, @_focusContact
|
||||
@listenTo MessageStore, => @_onMessageStoreChanged()
|
||||
@listenTo AccountStore, => @_onAccountChanged()
|
||||
@listenTo MessageStore, @_onMessageStoreChanged
|
||||
@listenTo AccountStore, @_onAccountChanged
|
||||
@listenTo FocusedContentStore, @_onFocusChanged
|
||||
|
||||
@_currentThread = null
|
||||
|
@ -31,7 +30,7 @@ FocusedContactsStore = Reflux.createStore
|
|||
@_currentFocusedContact = null
|
||||
@trigger() unless silent
|
||||
|
||||
_onFocusChanged: (change) ->
|
||||
_onFocusChanged: (change) =>
|
||||
return unless change.impactsCollection('thread')
|
||||
item = FocusedContentStore.focused('thread')
|
||||
return if @_currentThread?.id is item?.id
|
||||
|
@ -42,13 +41,13 @@ FocusedContactsStore = Reflux.createStore
|
|||
# We need to wait now for the MessageStore to grab all of the
|
||||
# appropriate messages for the given thread.
|
||||
|
||||
_onMessageStoreChanged: -> _.defer =>
|
||||
_onMessageStoreChanged: =>
|
||||
if MessageStore.threadId() is @_currentThread?.id
|
||||
@_setCurrentParticipants()
|
||||
else
|
||||
@_clearCurrentParticipants()
|
||||
|
||||
_onAccountChanged: ->
|
||||
_onAccountChanged: =>
|
||||
@_myEmail = (AccountStore.current()?.me().email ? "").toLowerCase().trim()
|
||||
|
||||
# For now we take the last message
|
||||
|
@ -59,7 +58,7 @@ FocusedContactsStore = Reflux.createStore
|
|||
@_focusContact(@_currentContacts[0], silent: true)
|
||||
@trigger()
|
||||
|
||||
_focusContact: (contact, {silent}={}) ->
|
||||
_focusContact: (contact, {silent}={}) =>
|
||||
return unless contact
|
||||
@_currentFocusedContact = contact
|
||||
@trigger() unless silent
|
||||
|
@ -113,3 +112,4 @@ FocusedContactsStore = Reflux.createStore
|
|||
theirDomain = _.last(email.split("@"))
|
||||
return myDomain.length > 0 and theirDomain.length > 0 and myDomain is theirDomain
|
||||
|
||||
module.exports = new FocusedContactsStore
|
||||
|
|
|
@ -31,8 +31,8 @@ class MessageStore extends NylasStore
|
|||
# this.state == nextState is always true if we modify objects in place.
|
||||
_.clone @_itemsExpanded
|
||||
|
||||
itemLocalIds: =>
|
||||
_.clone @_itemsLocalIds
|
||||
itemClientIds: ->
|
||||
_.pluck(@_items, "clientId")
|
||||
|
||||
itemsLoading: ->
|
||||
@_itemsLoading
|
||||
|
@ -71,7 +71,6 @@ class MessageStore extends NylasStore
|
|||
_setStoreDefaults: =>
|
||||
@_items = []
|
||||
@_itemsExpanded = {}
|
||||
@_itemsLocalIds = {}
|
||||
@_itemsLoading = false
|
||||
@_thread = null
|
||||
@_extensions = []
|
||||
|
@ -89,20 +88,13 @@ class MessageStore extends NylasStore
|
|||
inDisplayedThread = _.some change.objects, (obj) => obj.threadId is @_thread.id
|
||||
if inDisplayedThread
|
||||
|
||||
# Are we most likely adding a new draft? If the item is a draft and we don't
|
||||
# have it's local Id, optimistically add it to the set, resort, and trigger.
|
||||
# Note: this can avoid 100msec+ of delay from "Reply" => composer onscreen,
|
||||
item = change.objects[0]
|
||||
itemAlreadyExists = _.some @_items, (msg) -> msg.id is item.id
|
||||
if change.objects.length is 1 and item.draft is true and not itemAlreadyExists
|
||||
DatabaseStore.localIdForModel(item).then (localId) =>
|
||||
@_itemsLocalIds[item.id] = localId
|
||||
# We need to create a new copy of the items array so that the message-list
|
||||
# can compare new state to previous state.
|
||||
@_items = [].concat(@_items, [item])
|
||||
@_items = @_sortItemsForDisplay(@_items)
|
||||
@_expandItemsToDefault()
|
||||
@trigger()
|
||||
@_items = [].concat(@_items, [item])
|
||||
@_items = @_sortItemsForDisplay(@_items)
|
||||
@_expandItemsToDefault()
|
||||
@trigger()
|
||||
else
|
||||
@_fetchFromCache()
|
||||
|
||||
|
@ -144,61 +136,53 @@ class MessageStore extends NylasStore
|
|||
query.where(threadId: loadedThreadId, accountId: @_thread.accountId)
|
||||
query.include(Message.attributes.body)
|
||||
query.then (items) =>
|
||||
localIds = {}
|
||||
async.each items, (item, callback) ->
|
||||
return callback() unless item.draft
|
||||
DatabaseStore.localIdForModel(item).then (localId) ->
|
||||
localIds[item.id] = localId
|
||||
callback()
|
||||
, =>
|
||||
# Check to make sure that our thread is still the thread we were
|
||||
# loading items for. Necessary because this takes a while.
|
||||
return unless loadedThreadId is @_thread?.id
|
||||
# Check to make sure that our thread is still the thread we were
|
||||
# loading items for. Necessary because this takes a while.
|
||||
return unless loadedThreadId is @_thread?.id
|
||||
|
||||
loaded = true
|
||||
loaded = true
|
||||
|
||||
@_items = @_sortItemsForDisplay(items)
|
||||
@_itemsLocalIds = localIds
|
||||
@_items = @_sortItemsForDisplay(items)
|
||||
|
||||
# If no items were returned, attempt to load messages via the API. If items
|
||||
# are returned, this will trigger a refresh here.
|
||||
if @_items.length is 0
|
||||
@_fetchMessages()
|
||||
loaded = false
|
||||
# If no items were returned, attempt to load messages via the API. If items
|
||||
# are returned, this will trigger a refresh here.
|
||||
if @_items.length is 0
|
||||
@_fetchMessages()
|
||||
loaded = false
|
||||
|
||||
@_expandItemsToDefault()
|
||||
@_expandItemsToDefault()
|
||||
|
||||
# Download the attachments on expanded messages.
|
||||
@_fetchExpandedAttachments(@_items)
|
||||
# Download the attachments on expanded messages.
|
||||
@_fetchExpandedAttachments(@_items)
|
||||
|
||||
# Check that expanded messages have bodies. We won't mark ourselves
|
||||
# as loaded until they're all available. Note that items can be manually
|
||||
# expanded so this logic must be separate from above.
|
||||
if @_fetchExpandedBodies(@_items)
|
||||
loaded = false
|
||||
# Check that expanded messages have bodies. We won't mark ourselves
|
||||
# as loaded until they're all available. Note that items can be manually
|
||||
# expanded so this logic must be separate from above.
|
||||
if @_fetchExpandedBodies(@_items)
|
||||
loaded = false
|
||||
|
||||
# Normally, we would trigger often and let the view's
|
||||
# shouldComponentUpdate decide whether to re-render, but if we
|
||||
# know we're not ready, don't even bother. Trigger once at start
|
||||
# and once when ready. Many third-party stores will observe
|
||||
# MessageStore and they'll be stupid and re-render constantly.
|
||||
if loaded
|
||||
# Mark the thread as read if necessary. Make sure it's still the
|
||||
# current thread after the timeout.
|
||||
# Normally, we would trigger often and let the view's
|
||||
# shouldComponentUpdate decide whether to re-render, but if we
|
||||
# know we're not ready, don't even bother. Trigger once at start
|
||||
# and once when ready. Many third-party stores will observe
|
||||
# MessageStore and they'll be stupid and re-render constantly.
|
||||
if loaded
|
||||
# Mark the thread as read if necessary. Make sure it's still the
|
||||
# current thread after the timeout.
|
||||
|
||||
# Override canBeUndone to return false so that we don't see undo prompts
|
||||
# (since this is a passive action vs. a user-triggered action.)
|
||||
if @_thread.unread
|
||||
markAsReadDelay = atom.config.get('core.reading.markAsReadDelay')
|
||||
setTimeout =>
|
||||
return unless loadedThreadId is @_thread?.id
|
||||
t = new ChangeUnreadTask(thread: @_thread, unread: false)
|
||||
t.canBeUndone = => false
|
||||
Actions.queueTask(t)
|
||||
, markAsReadDelay
|
||||
# Override canBeUndone to return false so that we don't see undo prompts
|
||||
# (since this is a passive action vs. a user-triggered action.)
|
||||
if @_thread.unread
|
||||
markAsReadDelay = atom.config.get('core.reading.markAsReadDelay')
|
||||
setTimeout =>
|
||||
return unless loadedThreadId is @_thread?.id
|
||||
t = new ChangeUnreadTask(thread: @_thread, unread: false)
|
||||
t.canBeUndone = => false
|
||||
Actions.queueTask(t)
|
||||
, markAsReadDelay
|
||||
|
||||
@_itemsLoading = false
|
||||
@trigger(@)
|
||||
@_itemsLoading = false
|
||||
@trigger(@)
|
||||
|
||||
_fetchExpandedBodies: (items) ->
|
||||
startedAFetch = false
|
||||
|
|
|
@ -92,7 +92,7 @@ class ModelView
|
|||
# "Total Refresh" - in a subclass, do something smarter
|
||||
@invalidateRetainedRange()
|
||||
|
||||
invalidateMetadataFor: (ids = []) ->
|
||||
invalidateMetadataFor: ->
|
||||
# "Total Refresh" - in a subclass, do something smarter
|
||||
@invalidateRetainedRange()
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
_ = require 'underscore'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{generateTempId} = require '../models/utils'
|
||||
|
||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
CoffeeHelpers = require '../coffee-helpers'
|
||||
|
@ -100,7 +99,7 @@ class TaskQueue
|
|||
{SaveDraftTask} or 'SaveDraftTask')
|
||||
|
||||
- `matching`: Optional An {Object} with criteria to pass to _.isMatch. For a
|
||||
SaveDraftTask, this could be {draftLocalId: "123123"}
|
||||
SaveDraftTask, this could be {draftClientId: "123123"}
|
||||
|
||||
Returns a matching {Task}, or null.
|
||||
###
|
||||
|
|
|
@ -9,7 +9,7 @@ ChangeMailTask = require './change-mail-task'
|
|||
# Public: Create a new task to apply labels to a message or thread.
|
||||
#
|
||||
# Takes an options array of the form:
|
||||
# - `folder` The {Folder} or {Folder} id to move to
|
||||
# - `folder` The {Folder} or {Folder} IDs to move to
|
||||
# - `threads` An array of {Thread}s or {Thread} IDs
|
||||
# - `threads` An array of {Message}s or {Message} IDs
|
||||
# - `undoData` Since changing the folder is a destructive action,
|
||||
|
|
|
@ -60,7 +60,7 @@ class ChangeMailTask extends Task
|
|||
# prepared the data they need and verified that requirements are met.
|
||||
#
|
||||
# Note: Currently, *ALL* subclasses must use `DatabaseStore.modelify`
|
||||
# to convert `threads` and `messages` from models/ids to models.
|
||||
# to convert `threads` and `messages` from models or ids to models.
|
||||
#
|
||||
performLocal: ->
|
||||
if @_isUndoTask and not @_restoreValues
|
||||
|
|
|
@ -11,29 +11,29 @@ FileUploadTask = require './file-upload-task'
|
|||
|
||||
module.exports =
|
||||
class DestroyDraftTask extends Task
|
||||
constructor: ({@draftLocalId, @draftId} = {}) -> super
|
||||
constructor: ({@draftClientId, @draftId} = {}) -> super
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
if @draftLocalId
|
||||
(other instanceof DestroyDraftTask and other.draftLocalId is @draftLocalId) or
|
||||
(other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId) or
|
||||
(other instanceof SendDraftTask and other.draftLocalId is @draftLocalId) or
|
||||
(other instanceof FileUploadTask and other.messageLocalId is @draftLocalId)
|
||||
if @draftClientId
|
||||
(other instanceof DestroyDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof SendDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof FileUploadTask and other.messageClientId is @draftClientId)
|
||||
else if @draftId
|
||||
(other instanceof DestroyDraftTask and other.draftLocalId is @draftLocalId)
|
||||
(other instanceof DestroyDraftTask and other.draftClientId is @draftClientId)
|
||||
else
|
||||
false
|
||||
|
||||
shouldWaitForTask: (other) ->
|
||||
(other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId)
|
||||
(other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId)
|
||||
|
||||
performLocal: ->
|
||||
if @draftLocalId
|
||||
find = DatabaseStore.findByLocalId(Message, @draftLocalId)
|
||||
if @draftClientId
|
||||
find = DatabaseStore.findBy(Message, clientId: @draftClientId)
|
||||
else if @draftId
|
||||
find = DatabaseStore.find(Message, @draftId)
|
||||
else
|
||||
return Promise.reject(new Error("Attempt to call DestroyDraftTask.performLocal without draftLocalId or draftId"))
|
||||
return Promise.reject(new Error("Attempt to call DestroyDraftTask.performLocal without draftClientId"))
|
||||
|
||||
find.then (draft) =>
|
||||
return Promise.resolve() unless draft
|
||||
|
@ -45,10 +45,10 @@ class DestroyDraftTask extends Task
|
|||
# when we performed locally, or if the draft has never been synced to
|
||||
# the server (id is still self-assigned)
|
||||
return Promise.resolve(Task.Status.Finished) unless @draft
|
||||
return Promise.resolve(Task.Status.Finished) unless @draft.isSaved() and @draft.version?
|
||||
return Promise.resolve(Task.Status.Finished) unless @draft.serverId and @draft.version?
|
||||
|
||||
NylasAPI.makeRequest
|
||||
path: "/drafts/#{@draft.id}"
|
||||
path: "/drafts/#{@draft.serverId}"
|
||||
accountId: @draft.accountId
|
||||
method: "DELETE"
|
||||
body:
|
||||
|
|
|
@ -17,7 +17,7 @@ UploadCounter = 0
|
|||
|
||||
class FileUploadTask extends Task
|
||||
|
||||
constructor: (@filePath, @messageLocalId) ->
|
||||
constructor: (@filePath, @messageClientId) ->
|
||||
super
|
||||
@_startDate = Date.now()
|
||||
@_startId = UploadCounter
|
||||
|
@ -27,7 +27,7 @@ class FileUploadTask extends Task
|
|||
|
||||
performLocal: ->
|
||||
return Promise.reject(new Error("Must pass an absolute path to upload")) unless @filePath?.length
|
||||
return Promise.reject(new Error("Must be attached to a messageLocalId")) unless isTempId(@messageLocalId)
|
||||
return Promise.reject(new Error("Must be attached to a messageClientId")) unless isTempId(@messageClientId)
|
||||
Actions.uploadStateChanged @_uploadData("pending")
|
||||
Promise.resolve()
|
||||
|
||||
|
@ -97,7 +97,7 @@ class FileUploadTask extends Task
|
|||
Actions.linkFileToUpload(file: file, uploadData: @_uploadData("completed"))
|
||||
|
||||
DraftStore = require '../stores/draft-store'
|
||||
DraftStore.sessionForLocalId(@messageLocalId).then (session) =>
|
||||
DraftStore.sessionForClientId(@messageClientId).then (session) =>
|
||||
files = _.clone(session.draft().files) ? []
|
||||
files.push(file)
|
||||
session.changes.add({files})
|
||||
|
@ -121,7 +121,7 @@ class FileUploadTask extends Task
|
|||
filename: @_uploadData().fileName
|
||||
|
||||
# returns:
|
||||
# messageLocalId - The localId of the message (draft) we're uploading to
|
||||
# messageClientId - The clientId of the message (draft) we're uploading to
|
||||
# filePath - The full absolute local system file path
|
||||
# fileSize - The size in bytes
|
||||
# fileName - The basename of the file
|
||||
|
@ -132,7 +132,7 @@ class FileUploadTask extends Task
|
|||
uploadTaskId: @id
|
||||
startDate: @_startDate
|
||||
startId: @_startId
|
||||
messageLocalId: @messageLocalId
|
||||
messageClientId: @messageClientId
|
||||
filePath: @filePath
|
||||
fileSize: @_getFileSize(@filePath)
|
||||
fileName: pathUtils.basename(@filePath)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
{isTempId} = require '../models/utils'
|
||||
|
||||
Actions = require '../actions'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
Message = require '../models/message'
|
||||
|
@ -13,40 +11,39 @@ NylasAPI = require '../nylas-api'
|
|||
module.exports =
|
||||
class SendDraftTask extends Task
|
||||
|
||||
constructor: (@draftLocalId, {@fromPopout}={}) ->
|
||||
constructor: (@draftClientId, {@fromPopout}={}) ->
|
||||
super
|
||||
|
||||
label: ->
|
||||
"Sending draft..."
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
other instanceof SendDraftTask and other.draftLocalId is @draftLocalId
|
||||
other instanceof SendDraftTask and other.draftClientId is @draftClientId
|
||||
|
||||
shouldWaitForTask: (other) ->
|
||||
(other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId) or
|
||||
(other instanceof FileUploadTask and other.messageLocalId is @draftLocalId)
|
||||
(other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId) or
|
||||
(other instanceof FileUploadTask and other.messageClientId is @draftClientId)
|
||||
|
||||
performLocal: ->
|
||||
# When we send drafts, we don't update anything in the app until
|
||||
# it actually succeeds. We don't want users to think messages have
|
||||
# already sent when they haven't!
|
||||
if not @draftLocalId
|
||||
return Promise.reject(new Error("Attempt to call SendDraftTask.performLocal without @draftLocalId."))
|
||||
if not @draftClientId
|
||||
return Promise.reject(new Error("Attempt to call SendDraftTask.performLocal without @draftClientId."))
|
||||
Promise.resolve()
|
||||
|
||||
performRemote: ->
|
||||
# Fetch the latest draft data to make sure we make the request with the most
|
||||
# recent draft version
|
||||
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
|
||||
DatabaseStore.findBy(Message, clientId: @draftClientId).then (draft) =>
|
||||
# The draft may have been deleted by another task. Nothing we can do.
|
||||
NylasAPI.incrementOptimisticChangeCount(Message, draft.id)
|
||||
@draft = draft
|
||||
if not draft
|
||||
return Promise.reject(new Error("We couldn't find the saved draft."))
|
||||
|
||||
if draft.isSaved()
|
||||
if draft.serverId
|
||||
body =
|
||||
draft_id: draft.id
|
||||
draft_id: draft.serverId
|
||||
version: draft.version
|
||||
else
|
||||
body = draft.toJSON()
|
||||
|
@ -68,13 +65,14 @@ class SendDraftTask extends Task
|
|||
message = (new Message).fromJSON(json)
|
||||
atom.playSound('mail_sent.ogg')
|
||||
Actions.sendDraftSuccess
|
||||
draftLocalId: @draftLocalId
|
||||
draftClientId: @draftClientId
|
||||
newMessage: message
|
||||
DatabaseStore.unpersistModel(@draft).then =>
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
DestroyDraftTask = require './destroy-draft'
|
||||
task = new DestroyDraftTask(draftClientId: @draftClientId)
|
||||
Actions.queueTask(task)
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
|
||||
.catch APIError, (err) =>
|
||||
NylasAPI.decrementOptimisticChangeCount(Message, @draft.id)
|
||||
if err.message?.indexOf('Invalid message public id') is 0
|
||||
body.reply_to_message_id = null
|
||||
return @_send(body)
|
||||
|
@ -84,7 +82,7 @@ class SendDraftTask extends Task
|
|||
return @_send(body)
|
||||
else if err.statusCode in NylasAPI.PermanentErrorCodes
|
||||
msg = err.message ? "Your draft could not be sent."
|
||||
Actions.composePopoutDraft(@draftLocalId, {errorMessage: msg})
|
||||
Actions.composePopoutDraft(@draftClientId, {errorMessage: msg})
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
else
|
||||
return Promise.resolve(Task.Status.Retry)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
_ = require 'underscore'
|
||||
{isTempId, generateTempId} = require '../models/utils'
|
||||
|
||||
Actions = require '../actions'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
|
@ -18,21 +17,21 @@ FileUploadTask = require './file-upload-task'
|
|||
module.exports =
|
||||
class SyncbackDraftTask extends Task
|
||||
|
||||
constructor: (@draftLocalId) ->
|
||||
constructor: (@draftClientId) ->
|
||||
super
|
||||
|
||||
shouldDequeueOtherTask: (other) ->
|
||||
other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId and other.creationDate < @creationDate
|
||||
other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId and other.creationDate < @creationDate
|
||||
|
||||
shouldWaitForTask: (other) ->
|
||||
other instanceof SyncbackDraftTask and other.draftLocalId is @draftLocalId and other.creationDate < @creationDate
|
||||
other instanceof SyncbackDraftTask and other.draftClientId is @draftClientId and other.creationDate < @creationDate
|
||||
|
||||
performLocal: ->
|
||||
# SyncbackDraftTask does not do anything locally. You should persist your changes
|
||||
# to the local database directly or using a DraftStoreProxy, and then queue a
|
||||
# SyncbackDraftTask to send those changes to the server.
|
||||
if not @draftLocalId
|
||||
errMsg = "Attempt to call SyncbackDraftTask.performLocal without @draftLocalId"
|
||||
if not @draftClientId
|
||||
errMsg = "Attempt to call SyncbackDraftTask.performLocal without @draftClientId"
|
||||
return Promise.reject(new Error(errMsg))
|
||||
Promise.resolve()
|
||||
|
||||
|
@ -42,8 +41,8 @@ class SyncbackDraftTask extends Task
|
|||
return Promise.resolve() unless draft
|
||||
@checkDraftFromMatchesAccount(draft).then (draft) =>
|
||||
|
||||
if draft.isSaved()
|
||||
path = "/drafts/#{draft.id}"
|
||||
if draft.serverId
|
||||
path = "/drafts/#{draft.serverId}"
|
||||
method = 'PUT'
|
||||
else
|
||||
path = "/drafts"
|
||||
|
@ -71,38 +70,31 @@ class SyncbackDraftTask extends Task
|
|||
# below. We currently have no way of locking between processes. Maybe a
|
||||
# log-style data structure would be better suited for drafts.
|
||||
#
|
||||
@getLatestLocalDraft().then (draft) =>
|
||||
updatedDraft = draft.clone()
|
||||
updatedDraft.version = json.version
|
||||
updatedDraft.id = json.id
|
||||
|
||||
if updatedDraft.id != draft.id
|
||||
DatabaseStore.swapModel(oldModel: draft, newModel: updatedDraft, localId: @draftLocalId)
|
||||
else
|
||||
DatabaseStore.persistModel(updatedDraft)
|
||||
@getLatestLocalDraft().then (draft) ->
|
||||
draft.version = json.version
|
||||
draft.serverId = json.id
|
||||
DatabaseStore.persistModel(draft)
|
||||
|
||||
.then =>
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
|
||||
.catch APIError, (err) =>
|
||||
if err.statusCode in [400, 404, 409] and err.requestOptions.method is 'PUT'
|
||||
return @getLatestLocalDraft().then (draft) =>
|
||||
@detatchFromRemoteID(draft).then =>
|
||||
Promise.resolve(Task.Status.Retry)
|
||||
return Promise.resolve(Task.Status.Retry)
|
||||
|
||||
if err.statusCode in NylasAPI.PermanentErrorCodes
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
|
||||
return Promise.resolve(Task.Status.Retry)
|
||||
|
||||
getLatestLocalDraft: ->
|
||||
DatabaseStore.findByLocalId(Message, @draftLocalId)
|
||||
getLatestLocalDraft: =>
|
||||
DatabaseStore.findBy(Message, clientId: @draftClientId)
|
||||
|
||||
checkDraftFromMatchesAccount: (existingAccountDraft) ->
|
||||
DatabaseStore.findBy(Account, [Account.attributes.emailAddress.equal(existingAccountDraft.from[0].email)]).then (acct) =>
|
||||
promise = Promise.resolve(existingAccountDraft)
|
||||
checkDraftFromMatchesAccount: (draft) ->
|
||||
DatabaseStore.findBy(Account, [Account.attributes.emailAddress.equal(draft.from[0].email)]).then (account) =>
|
||||
promise = Promise.resolve(draft)
|
||||
|
||||
if existingAccountDraft.accountId isnt acct.id
|
||||
if draft.accountId isnt account.id
|
||||
DestroyDraftTask = require './destroy-draft'
|
||||
destroy = new DestroyDraftTask(draftId: existingAccountDraft.id)
|
||||
promise = TaskQueueStatusStore.waitForPerformLocal(destroy).then =>
|
||||
|
@ -115,11 +107,11 @@ class SyncbackDraftTask extends Task
|
|||
detatchFromRemoteID: (draft, newAccountId = null) ->
|
||||
return Promise.resolve() unless draft
|
||||
newDraft = new Message(draft)
|
||||
newDraft.id = generateTempId()
|
||||
newDraft.accountId = newAccountId if newAccountId
|
||||
|
||||
delete newDraft.serverId
|
||||
delete newDraft.version
|
||||
delete newDraft.threadId
|
||||
delete newDraft.replyToMessageId
|
||||
|
||||
DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then =>
|
||||
Promise.resolve(newDraft)
|
||||
DatabaseStore.persistModel(newDraft)
|
||||
|
|
|
@ -11,7 +11,7 @@ class MyComposerButton extends React.Component
|
|||
# reference to the draft, and you can look it up to perform
|
||||
# actions and retrieve data.
|
||||
@propTypes:
|
||||
draftLocalId: React.PropTypes.string.isRequired
|
||||
draftClientId: React.PropTypes.string.isRequired
|
||||
|
||||
render: =>
|
||||
<div className="my-package">
|
||||
|
@ -24,7 +24,7 @@ class MyComposerButton extends React.Component
|
|||
# To retrieve information about the draft, we fetch the current editing
|
||||
# session from the draft store. We can access attributes of the draft
|
||||
# and add changes to the session which will be appear immediately.
|
||||
DraftStore.sessionForLocalId(@props.draftLocalId).then (session) =>
|
||||
DraftStore.sessionForClientId(@props.draftClientId).then (session) =>
|
||||
newSubject = "#{session.draft().subject} - It Worked!"
|
||||
|
||||
dialog = @_getDialog()
|
||||
|
@ -40,4 +40,4 @@ class MyComposerButton extends React.Component
|
|||
require('remote').require('dialog')
|
||||
|
||||
|
||||
module.exports = MyComposerButton
|
||||
module.exports = MyComposerButton
|
||||
|
|
|
@ -9,7 +9,7 @@ dialogStub =
|
|||
describe "MyComposerButton", ->
|
||||
beforeEach ->
|
||||
@component = ReactTestUtils.renderIntoDocument(
|
||||
<MyComposerButton draftLocalId="test" />
|
||||
<MyComposerButton draftClientId="test" />
|
||||
)
|
||||
|
||||
it "should render into the page", ->
|
||||
|
@ -22,4 +22,4 @@ describe "MyComposerButton", ->
|
|||
spyOn(@component, '_onClick')
|
||||
buttonNode = React.findDOMNode(@component.refs.button)
|
||||
ReactTestUtils.Simulate.click(buttonNode)
|
||||
expect(@component._onClick).toHaveBeenCalled()
|
||||
expect(@component._onClick).toHaveBeenCalled()
|
||||
|
|
Loading…
Add table
Reference in a new issue