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