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