diff --git a/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx b/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx
index 2ca49a3cc..0d4e12769 100644
--- a/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx
+++ b/packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx
@@ -7,7 +7,7 @@ class TemplatePopover extends React.Component {
static displayName = 'TemplatePopover';
static propTypes = {
- draftClientId: React.PropTypes.string,
+ headerMessageId: React.PropTypes.string,
};
constructor() {
@@ -45,7 +45,7 @@ class TemplatePopover extends React.Component {
};
_onChooseTemplate = (template) => {
- Actions.insertTemplateId({templateId: template.id, draftClientId: this.props.draftClientId});
+ Actions.insertTemplateId({templateId: template.id, headerMessageId: this.props.headerMessageId});
Actions.closePopover();
}
@@ -54,7 +54,7 @@ class TemplatePopover extends React.Component {
};
_onNewTemplate = () => {
- Actions.createTemplate({draftClientId: this.props.draftClientId});
+ Actions.createTemplate({headerMessageId: this.props.headerMessageId});
};
_onClickButton = () => {
@@ -103,13 +103,13 @@ class TemplatePicker extends React.Component {
static displayName = 'TemplatePicker';
static propTypes = {
- draftClientId: React.PropTypes.string,
+ headerMessageId: React.PropTypes.string,
};
_onClickButton = () => {
const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()
Actions.openPopover(
- ,
+ ,
{originRect: buttonRect, direction: 'up'}
)
};
diff --git a/packages/client-app/internal_packages/composer-templates/lib/template-store.es6 b/packages/client-app/internal_packages/composer-templates/lib/template-store.es6
index 8baa4c5db..8642f685c 100644
--- a/packages/client-app/internal_packages/composer-templates/lib/template-store.es6
+++ b/packages/client-app/internal_packages/composer-templates/lib/template-store.es6
@@ -112,9 +112,9 @@ class TemplateStore extends NylasStore {
});
}
- _onCreateTemplate({draftClientId, name, contents} = {}) {
- if (draftClientId) {
- DraftStore.sessionForClientId(draftClientId).then((session) => {
+ _onCreateTemplate({headerMessageId, name, contents} = {}) {
+ if (headerMessageId) {
+ DraftStore.sessionForClientId(headerMessageId).then((session) => {
const draft = session.draft();
const draftName = name || draft.subject.replace(TemplateStore.INVALID_TEMPLATE_NAME_REGEX, '');
let draftContents = contents || QuotedHTMLTransformer.removeQuotedHTML(draft.body);
@@ -255,9 +255,9 @@ class TemplateStore extends NylasStore {
});
}
- _onInsertTemplateId({templateId, draftClientId} = {}) {
+ _onInsertTemplateId({templateId, headerMessageId} = {}) {
this.getTemplateContents(templateId, (templateBody) => {
- DraftStore.sessionForClientId(draftClientId).then((session) => {
+ DraftStore.sessionForClientId(headerMessageId).then((session) => {
let proceed = true;
if (!session.draft().pristine && !session.draft().hasEmptyBody()) {
proceed = this._displayDialog(
diff --git a/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6 b/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6
index 65a7ffba3..c7e411d35 100644
--- a/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6
+++ b/packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6
@@ -83,7 +83,7 @@ xdescribe('TemplateStore', function templateStore() {
runs(() => {
TemplateStore._onInsertTemplateId({
templateId: 'template1.html',
- draftClientId: 'localid-draft',
+ headerMessageId: 'localid-draft',
});
});
waitsFor(() => add.calls.length > 0);
@@ -98,8 +98,8 @@ xdescribe('TemplateStore', function templateStore() {
describe('onCreateTemplate', () => {
beforeEach(() => {
let d;
- spyOn(DraftStore, 'sessionForClientId').andCallFake((draftClientId) => {
- if (draftClientId === 'localid-nosubject') {
+ spyOn(DraftStore, 'sessionForClientId').andCallFake((headerMessageId) => {
+ if (headerMessageId === 'localid-nosubject') {
d = new Message({subject: '', body: '
Body
'});
} else {
d = new Message({subject: 'Subject', body: 'Body
'});
@@ -149,7 +149,7 @@ xdescribe('TemplateStore', function templateStore() {
spyOn(TemplateStore, 'trigger');
spyOn(TemplateStore, '_populate');
runs(() => {
- TemplateStore._onCreateTemplate({draftClientId: 'localid-b'});
+ TemplateStore._onCreateTemplate({headerMessageId: 'localid-b'});
});
waitsFor(() => TemplateStore.trigger.callCount > 0);
runs(() => {
@@ -161,7 +161,7 @@ xdescribe('TemplateStore', function templateStore() {
spyOn(TemplateStore, '_displayError');
spyOn(fs, 'watch');
runs(() => {
- TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'});
+ TemplateStore._onCreateTemplate({headerMessageId: 'localid-nosubject'});
});
waitsFor(() => TemplateStore._displayError.callCount > 0);
runs(() => {
diff --git a/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx b/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx
index 28a7982ea..f7baea48e 100644
--- a/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx
+++ b/packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx
@@ -7,14 +7,14 @@ export default class ComposerHeaderActions extends React.Component {
static displayName = 'ComposerHeaderActions';
static propTypes = {
- draftClientId: React.PropTypes.string.isRequired,
+ headerMessageId: React.PropTypes.string.isRequired,
enabledFields: React.PropTypes.array.isRequired,
participantsFocused: React.PropTypes.bool,
onShowAndFocusField: React.PropTypes.func.isRequired,
}
_onPopoutComposer = () => {
- Actions.composePopoutDraft(this.props.draftClientId);
+ Actions.composePopoutDraft(this.props.headerMessageId);
}
render() {
diff --git a/packages/client-app/internal_packages/composer/lib/composer-header.jsx b/packages/client-app/internal_packages/composer/lib/composer-header.jsx
index a9c373cc9..06d40458a 100644
--- a/packages/client-app/internal_packages/composer/lib/composer-header.jsx
+++ b/packages/client-app/internal_packages/composer/lib/composer-header.jsx
@@ -326,7 +326,7 @@ export default class ComposerHeader extends React.Component {
return (
)
}
@@ -365,7 +365,7 @@ export default class ComposerView extends React.Component {
exposedProps={{
draft: this.props.draft,
threadId: this.props.draft.threadId,
- draftClientId: this.props.draft.clientId,
+ headerMessageId: this.props.draft.headerMessageId,
session: this.props.session,
}}
/>
@@ -415,7 +415,7 @@ export default class ComposerView extends React.Component {
]}
exposedProps={{
draft: this.props.draft,
- draftClientId: this.props.draft.clientId,
+ headerMessageId: this.props.draft.headerMessageId,
session: this.props.session,
isValidDraft: this._isValidDraft,
}}
@@ -510,7 +510,7 @@ export default class ComposerView extends React.Component {
// called from onDrop and onFilePaste - assume images should be inline
Actions.addAttachment({
filePath: filePath,
- messageClientId: this.props.draft.clientId,
+ headerMessageId: this.props.draft.headerMessageId,
onUploadCreated: (upload) => {
if (Utils.shouldDisplayAsImage(upload)) {
const {draft, session} = this.props;
@@ -522,7 +522,7 @@ export default class ComposerView extends React.Component {
session.changes.add({uploads})
Actions.insertAttachmentIntoDraft({
- draftClientId: draft.clientId,
+ headerMessageId: draft.headerMessageId,
uploadId: matchingUpload.id,
});
}
@@ -541,7 +541,7 @@ export default 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.
- if (DraftStore.isSendingDraft(this.props.draft.clientId)) {
+ if (DraftStore.isSendingDraft(this.props.draft.headerMessageId)) {
return false;
}
@@ -579,11 +579,11 @@ export default class ComposerView extends React.Component {
}
_onDestroyDraft = () => {
- Actions.destroyDraft(this.props.draft.clientId);
+ Actions.destroyDraft(this.props.draft.headerMessageId);
}
_onSelectAttachment = () => {
- Actions.selectAttachment({messageClientId: this.props.draft.clientId});
+ Actions.selectAttachment({headerMessageId: this.props.draft.headerMessageId});
}
render() {
diff --git a/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6 b/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6
index a3179ae07..99de16728 100644
--- a/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6
+++ b/packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6
@@ -24,7 +24,7 @@ export default class ImageUploadComposerExtension extends ComposerExtension {
}
static _onInsertAttachmentIntoDraft({editor, actionArg}) {
- if (editor.draftClientId === actionArg.draftClientId) { return }
+ if (editor.headerMessageId === actionArg.headerMessageId) { return }
editor.insertCustomComponent("InlineImageUploadContainer", {
className: `inline-container-${actionArg.uploadId}`,
diff --git a/packages/client-app/internal_packages/composer/lib/main.jsx b/packages/client-app/internal_packages/composer/lib/main.jsx
index 5865fd7c6..db4b18ec2 100644
--- a/packages/client-app/internal_packages/composer/lib/main.jsx
+++ b/packages/client-app/internal_packages/composer/lib/main.jsx
@@ -29,12 +29,12 @@ class ComposerWithWindowProps extends React.Component {
// We'll now always have windowProps by the time we construct this.
const windowProps = NylasEnv.getWindowProps();
- const {draftJSON, draftClientId} = windowProps;
+ const {draftJSON, headerMessageId} = windowProps;
if (!draftJSON) {
throw new Error("Initialize popout composer windows with valid draftJSON")
}
const draft = new Message().fromJSON(draftJSON);
- DraftStore._createSession(draftClientId, draft);
+ DraftStore._createSession(headerMessageId, draft);
this.state = windowProps
}
@@ -74,7 +74,7 @@ class ComposerWithWindowProps extends React.Component {
);
diff --git a/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx b/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx
index 1cc97ef13..41a6961c7 100644
--- a/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx
+++ b/packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx
@@ -10,7 +10,7 @@ describe "ComposerHeaderActions", ->
@onShowAndFocusField = jasmine.createSpy("onShowAndFocusField")
props.onShowAndFocusField = @onShowAndFocusField
props.enabledFields ?= []
- props.draftClientId = 'a'
+ props.headerMessageId = 'a'
@component = ReactTestUtils.renderIntoDocument(
)
diff --git a/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee b/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee
index bd26108d9..883e84330 100644
--- a/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee
+++ b/packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee
@@ -58,7 +58,7 @@ class DraftListStore extends NylasStore
mailboxPerspective.accountIds.forEach (aid) =>
OutboxStore.itemsForAccount(aid).forEach (task) =>
- draft = resultSet.modelWithId(task.draftClientId)
+ draft = resultSet.modelWithId(task.headerMessageId)
if draft
draft = draft.clone()
draft.uploadTaskId = task.id
diff --git a/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx b/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx
index ad02f0f1c..ddf37fd52 100644
--- a/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx
+++ b/packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx
@@ -41,12 +41,12 @@ class DraftList extends React.Component
_onDoubleClick: (draft) =>
unless draft.uploadTaskId
- Actions.composePopoutDraft(draft.id)
+ Actions.composePopoutDraft(draft.headerMessageId)
# Additional Commands
_onRemoveFromView: =>
drafts = DraftListStore.dataSource().selection.items()
- Actions.destroyDraft(draft.id) for draft in drafts
+ Actions.destroyDraft(draft.headerMessageId) for draft in drafts
module.exports = DraftList
diff --git a/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx b/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx
index be98010ae..c6fc6ceef 100644
--- a/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx
+++ b/packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx
@@ -19,7 +19,7 @@ class DraftDeleteButton extends React.Component
_destroySelected: =>
for item in @props.selection.items()
- Actions.destroyDraft(item.id)
+ Actions.destroyDraft(item.headerMessageId)
@props.selection.clear()
return
diff --git a/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx b/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx
index 15ef0a7f6..099f00265 100644
--- a/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx
+++ b/packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx
@@ -64,11 +64,11 @@ class MessageItemContainer extends React.Component
_renderComposer: =>
Composer = ComponentRegistry.findComponentsMatching(role: 'Composer')[0]
if (!Composer)
- return
+ return
No Composer Component Present
- if draftId is @props.message.id
+ _onSendingStateChanged: (headerMessageId) =>
+ if headerMessageId is @props.message.headerMessageId
@setState(@_getStateFromStores())
_getStateFromStores: (props = @props) ->
diff --git a/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx b/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx
index 8badf1da5..68908ffc9 100644
--- a/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx
+++ b/packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx
@@ -37,7 +37,7 @@ xdescribe 'MessageItemContainer', ->
ReactTestUtils.renderIntoDocument(
+ headerMessageId={testClientId} />
)
it "shows composer if it's a draft", ->
diff --git a/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx b/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx
index bd97b6f64..c6834b225 100644
--- a/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx
+++ b/packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx
@@ -50,7 +50,7 @@ m1 = (new Message).fromJSON({
"draft" : false
"files" : [],
"unread" : false,
- "object" : "message",
+ "__constructorName": "Message",
"snippet" : "snippet one...",
"subject" : "Subject One",
"thread_id" : "thread_12345",
@@ -67,7 +67,7 @@ m2 = (new Message).fromJSON({
"draft" : false
"files" : [],
"unread" : false,
- "object" : "message",
+ "__constructorName": "Message",
"snippet" : "snippet Two...",
"subject" : "Subject Two",
"thread_id" : "thread_12345",
@@ -84,7 +84,7 @@ m3 = (new Message).fromJSON({
"draft" : false
"files" : [],
"unread" : false,
- "object" : "message",
+ "__constructorName": "Message",
"snippet" : "snippet Three...",
"subject" : "Subject Three",
"thread_id" : "thread_12345",
@@ -101,7 +101,7 @@ m4 = (new Message).fromJSON({
"draft" : false
"files" : [],
"unread" : false,
- "object" : "message",
+ "__constructorName": "Message",
"snippet" : "snippet Four...",
"subject" : "Subject Four",
"thread_id" : "thread_12345",
@@ -118,7 +118,7 @@ m5 = (new Message).fromJSON({
"draft" : false
"files" : [],
"unread" : false,
- "object" : "message",
+ "__constructorName": "Message",
"snippet" : "snippet Five...",
"subject" : "Subject Five",
"thread_id" : "thread_12345",
@@ -137,7 +137,7 @@ draftMessages = [
"draft" : true
"files" : [],
"unread" : false,
- "object" : "draft",
+ "__constructorName": "Message",
"snippet" : "draft snippet one...",
"subject" : "Draft One",
"thread_id" : "thread_12345",
diff --git a/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx b/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx
index 05c479d11..d90b670b2 100644
--- a/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx
+++ b/packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx
@@ -90,7 +90,7 @@ class SnoozeStore {
// Get messages for those threads and metadata for those.
DatabaseStore.findAll(Message, {threadId: update.threads.map(t => t.id)}).then((messages) => {
for (const message of messages) {
- const header = message.messageIdHeader;
+ const header = message.headerMessageId;
const stableId = message.id;
Actions.setMetadata(message, this.pluginId,
{expiration: snoozeDate, header, stableId, snoozeCategoryId, returnCategoryId})
diff --git a/packages/client-app/script/grunt b/packages/client-app/script/grunt
index ddf19bd51..ab78dcb67 100755
--- a/packages/client-app/script/grunt
+++ b/packages/client-app/script/grunt
@@ -4,7 +4,7 @@ var fs = require('fs');
var path = require('path');
// node build/node_modules/.bin/grunt "$@"
-var gruntPath = path.resolve(__dirname, '..', 'build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : '');
+var gruntPath = path.resolve(__dirname, '..', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : '');
if (!fs.existsSync(gruntPath)) {
console.error('Grunt command does not exist at: ' + gruntPath);
diff --git a/packages/client-app/spec/action-bridge-spec.coffee b/packages/client-app/spec/action-bridge-spec.coffee
index c962efa8f..20e2df9ff 100644
--- a/packages/client-app/spec/action-bridge-spec.coffee
+++ b/packages/client-app/spec/action-bridge-spec.coffee
@@ -12,14 +12,14 @@ ipc =
describe "ActionBridge", ->
- describe "in the work window", ->
+ describe "in the womainrk window", ->
beforeEach ->
spyOn(NylasEnv, "getWindowType").andReturn "default"
spyOn(NylasEnv, "isWorkWindow").andReturn true
@bridge = new ActionBridge(ipc)
- it "should have the role Role.WORK", ->
- expect(@bridge.role).toBe(ActionBridge.Role.WORK)
+ it "should have the role Role.MAIN", ->
+ expect(@bridge.role).toBe(ActionBridge.Role.MAIN)
it "should rebroadcast global actions", ->
spyOn(@bridge, 'onRebroadcast')
@@ -87,12 +87,12 @@ describe "ActionBridge", ->
@bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}])
expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'onNewMailDeltas', '[{"oldModel":"1","newModel":2}]')
- describe "when called with TargetWindows.WORK", ->
+ describe "when called with TargetWindows.MAIN", ->
it "should broadcast the action over IPC to the main window only", ->
spyOn(ipc, 'send')
Actions.onNewMailDeltas.firing = false
- @bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}])
- expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'onNewMailDeltas', '[{"oldModel":"1","newModel":2}]')
+ @bridge.onRebroadcast(ActionBridge.TargetWindows.MAIN, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}])
+ expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-default', 'popout', 'onNewMailDeltas', '[{"oldModel":"1","newModel":2}]')
it "should not do anything if the current invocation of the Action was triggered by itself", ->
spyOn(ipc, 'send')
diff --git a/packages/client-app/spec/models/event-spec.coffee b/packages/client-app/spec/models/event-spec.coffee
index 40656d3ba..b6f9d991d 100644
--- a/packages/client-app/spec/models/event-spec.coffee
+++ b/packages/client-app/spec/models/event-spec.coffee
@@ -3,7 +3,7 @@ AccountStore = require("../../src/flux/stores/account-store").default
json_event =
{
- "object": "event",
+ "__constructorName": "Event",
"id": "4ee4xbnx7pxdb9g7c2f8ncyto",
"calendar_id": "ci0k1wfyv533ccgox4t7uri4h",
"account_id": "14e5bn96uizyuhidhcw5rfrb0",
diff --git a/packages/client-app/spec/models/thread-spec.coffee b/packages/client-app/spec/models/thread-spec.coffee
index 50f1d7590..b1641e746 100644
--- a/packages/client-app/spec/models/thread-spec.coffee
+++ b/packages/client-app/spec/models/thread-spec.coffee
@@ -9,7 +9,7 @@ describe 'Thread', ->
describe 'serialization performance', ->
xit '1,000,000 iterations', ->
iterations = 0
- json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","object":"thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","display_name":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]'
+ json = '[{"client_id":"local-76c370af-65de","server_id":"f0vkowp7zxt7djue7ifylb940","__constructorName":"Thread","account_id":"1r6w6qiq3sb0o9fiwin6v87dd","snippet":"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat","subject":"iTunes Terms And Conditions as you\'ve never seen them before","unread":true,"starred":false,"version":1,"folders":[],"labels":[{"server_id":"8cf4fn20k9pjjhjawrv3xrxo0","name":"all","display_name":"All Mail","id":"8cf4fn20k9pjjhjawrv3xrxo0"},{"server_id":"f1lq8faw8vv06m67y8f3xdf84","name":"inbox","display_name":"Inbox","id":"f1lq8faw8vv06m67y8f3xdf84"}],"participants":[{"name":"Andrew Stadler","email":"stadler@gmail.com","thirdPartyData":{}},{"name":"Smart Friends™ Chat","email":"smartfriends-chat@macgroup.com","thirdPartyData":{}}],"has_attachments":false,"last_message_received_timestamp":1446600615,"id":"f0vkowp7zxt7djue7ifylb940"}]'
start = Date.now()
while iterations < 1000000
if _.isString(json)
diff --git a/packages/client-app/spec/stores/account-store-spec.coffee b/packages/client-app/spec/stores/account-store-spec.coffee
index e6ec4358a..24eb93c65 100644
--- a/packages/client-app/spec/stores/account-store-spec.coffee
+++ b/packages/client-app/spec/stores/account-store-spec.coffee
@@ -26,7 +26,7 @@ describe "AccountStore", ->
"client_id" : 'local-4f9d476a-c173',
"server_id" : 'A',
"email_address":"bengotow@gmail.com",
- "object":"account"
+ "__constructorName":"Account"
"organization_unit": "label"
"aliases": ["Alias "]
},{
@@ -34,7 +34,7 @@ describe "AccountStore", ->
"client_id" : 'local-4f9d476a-c175',
"server_id" : 'B',
"email_address":"ben@nylas.com",
- "object":"account"
+ "__constructorName":"Account"
"organization_unit": "label"
}]
@@ -95,7 +95,7 @@ describe "AccountStore", ->
"server_id" : 'B',
"email_address":"ben@nylas.com",
"provider":"gmail",
- "object":"account",
+ "__constructorName":"Account",
"organization_unit": "label",
@instance = new @constructor
spyOn(NylasEnv.config, "set")
@@ -127,7 +127,7 @@ describe "AccountStore", ->
"server_id" : 'B',
"email_address":"ben@nylas.com",
"provider":"gmail",
- "object":"account"
+ "__constructorName":"Account"
"organization_unit": "label"
@spyOnConfig()
@instance = new @constructor
@@ -144,7 +144,7 @@ describe "AccountStore", ->
"server_id" : 'NEVER SEEN BEFORE',
"email_address":"ben@nylas.com",
"provider":"gmail",
- "object":"account"
+ "__constructorName":"Account"
"organization_unit": "label"
@spyOnConfig()
@instance = new @constructor
diff --git a/packages/client-app/spec/stores/file-upload-store-spec.coffee b/packages/client-app/spec/stores/file-upload-store-spec.coffee
index 4bbb233c3..4ae8af9c9 100644
--- a/packages/client-app/spec/stores/file-upload-store-spec.coffee
+++ b/packages/client-app/spec/stores/file-upload-store-spec.coffee
@@ -132,9 +132,9 @@ xdescribe 'FileUploadStore', ->
describe "when a draft is sent", ->
it "should delete its uploads directory", ->
- spyOn(FileUploadStore, '_deleteUploadsForClientId')
+ spyOn(FileUploadStore, '_deleteUploadsForId')
Actions.ensureMessageInSentSuccess({messageClientId: '123'})
- expect(FileUploadStore._deleteUploadsForClientId).toHaveBeenCalledWith('123')
+ expect(FileUploadStore._deleteUploadsForId).toHaveBeenCalledWith('123')
describe '_getFileStats', ->
it 'returns the correct stats', ->
diff --git a/packages/client-app/spec/stores/undo-redo-store-spec.es6 b/packages/client-app/spec/stores/undo-redo-store-spec.es6
index 032b47fd7..b2d381689 100644
--- a/packages/client-app/spec/stores/undo-redo-store-spec.es6
+++ b/packages/client-app/spec/stores/undo-redo-store-spec.es6
@@ -23,7 +23,6 @@ describe("UndoRedoStore", function undoRedoStoreSpec() {
UndoRedoStore._undo = []
UndoRedoStore._redo = []
spyOn(UndoRedoStore, "trigger")
- spyOn(Actions, "undoTaskId")
spyOn(Actions, "queueTask").andCallFake((...args) => {
UndoRedoStore._onQueue(...args)
});
@@ -83,9 +82,9 @@ describe("UndoRedoStore", function undoRedoStoreSpec() {
it("runs undoTask on each group of undo tasks", () => {
UndoRedoStore._undo = [[this.t3], [this.t1, this.t2]]
UndoRedoStore.undo()
- expect(Actions.undoTaskId.calls.length).toBe(2)
- expect(Actions.undoTaskId.calls[0].args[0]).toBe("t1")
- expect(Actions.undoTaskId.calls[1].args[0]).toBe("t2")
+ expect(Actions.queueTask.calls.length).toBe(2)
+ expect(Actions.queueTask.calls[0].args[0]).toBe("t1")
+ expect(Actions.queueTask.calls[1].args[0]).toBe("t2")
expect(UndoRedoStore._undo).toEqual([[this.t3]])
});
diff --git a/packages/client-app/spec/tasks/base-draft-task-spec.es6 b/packages/client-app/spec/tasks/base-draft-task-spec.es6
index 7c3b6b2ff..3ad7780e0 100644
--- a/packages/client-app/spec/tasks/base-draft-task-spec.es6
+++ b/packages/client-app/spec/tasks/base-draft-task-spec.es6
@@ -68,7 +68,7 @@ xdescribe('BaseDraftTask', function baseDraftTask() {
badTask.performLocal().then(() => {
throw new Error("Shouldn't succeed")
}).catch((err) => {
- expect(err.message).toBe("Attempt to call BaseDraftTask.performLocal without a draftClientId")
+ expect(err.message).toBe("Attempt to call BaseDraftTask.performLocal without a headerMessageId")
});
});
});
diff --git a/packages/client-app/src/browser/application.es6 b/packages/client-app/src/browser/application.es6
index 116d7b299..8ba9a4375 100644
--- a/packages/client-app/src/browser/application.es6
+++ b/packages/client-app/src/browser/application.es6
@@ -456,7 +456,7 @@ export default class Application extends EventEmitter {
ipcMain.on('ensure-worker-window', () => {
// TODO BG
- // this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
+ // this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW)
})
ipcMain.on('inline-style-parse', (event, {html, key}) => {
@@ -523,16 +523,15 @@ export default class Application extends EventEmitter {
this.windowManager.sendToAllWindows('action-bridge-message', {except: win}, ...args)
});
- ipcMain.on('action-bridge-rebroadcast-to-work', (event, ...args) => {
- // TODO BG
- // const workWindow = this.windowManager.get(WindowManager.WORK_WINDOW)
- // if (!workWindow || !workWindow.browserWindow.webContents) {
- // return;
- // }
- // if (BrowserWindow.fromWebContents(event.sender) === workWindow) {
- // return;
- // }
- // workWindow.browserWindow.webContents.send('action-bridge-message', ...args);
+ ipcMain.on('action-bridge-rebroadcast-to-default', (event, ...args) => {
+ const mainWindow = this.windowManager.get(WindowManager.MAIN_WINDOW)
+ if (!mainWindow || !mainWindow.browserWindow.webContents) {
+ return;
+ }
+ if (BrowserWindow.fromWebContents(event.sender) === mainWindow) {
+ return;
+ }
+ mainWindow.browserWindow.webContents.send('action-bridge-message', ...args);
});
ipcMain.on('write-text-to-selection-clipboard', (event, selectedText) => {
@@ -542,7 +541,6 @@ export default class Application extends EventEmitter {
ipcMain.on('account-setup-successful', () => {
this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW);
- this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);
const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW);
if (onboarding) {
onboarding.close();
diff --git a/packages/client-app/src/components/injected-component.cjsx b/packages/client-app/src/components/injected-component.cjsx
index 176177ec3..f6d3af1c2 100644
--- a/packages/client-app/src/components/injected-component.cjsx
+++ b/packages/client-app/src/components/injected-component.cjsx
@@ -14,11 +14,11 @@ components inside of your React render method. Rather than explicitly render
a component, such as a ``, you can use InjectedComponent:
```coffee
-
+
```
InjectedComponent will look up the component registered with that role in the
-{ComponentRegistry} and render it, passing the exposedProps (`draftClientId={123}`) along.
+{ComponentRegistry} and render it, passing the exposedProps (`headerMessageId={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/packages/client-app/src/decorators/inflates-draft-client-id.jsx b/packages/client-app/src/decorators/inflates-draft-client-id.jsx
index a2b9e923f..5e02f3715 100644
--- a/packages/client-app/src/decorators/inflates-draft-client-id.jsx
+++ b/packages/client-app/src/decorators/inflates-draft-client-id.jsx
@@ -8,7 +8,7 @@ function InflatesDraftClientId(ComposedComponent) {
static displayName = ComposedComponent.displayName;
static propTypes = {
- draftClientId: React.PropTypes.string,
+ headerMessageId: React.PropTypes.string,
onDraftReady: React.PropTypes.func,
}
@@ -28,7 +28,7 @@ function InflatesDraftClientId(ComposedComponent) {
componentDidMount() {
this._mounted = true;
- this._prepareForDraft(this.props.draftClientId);
+ this._prepareForDraft(this.props.headerMessageId);
}
componentWillUnmount() {
@@ -38,19 +38,19 @@ function InflatesDraftClientId(ComposedComponent) {
}
componentWillReceiveProps(newProps) {
- if (newProps.draftClientId !== this.props.draftClientId) {
+ if (newProps.headerMessageId !== this.props.headerMessageId) {
this._teardownForDraft();
- this._prepareForDraft(newProps.draftClientId);
+ this._prepareForDraft(newProps.headerMessageId);
}
}
- _prepareForDraft(draftClientId) {
- if (!draftClientId) {
+ _prepareForDraft(headerMessageId) {
+ if (!headerMessageId) {
return;
}
- DraftStore.sessionForClientId(draftClientId).then((session) => {
+ DraftStore.sessionForClientId(headerMessageId).then((session) => {
const shouldSetState = () =>
- this._mounted && session.draftClientId === this.props.draftClientId
+ this._mounted && session.headerMessageId === this.props.headerMessageId
if (!shouldSetState()) { return; }
this._sessionUnlisten = session.listen(() => {
@@ -80,7 +80,7 @@ function InflatesDraftClientId(ComposedComponent) {
return;
}
if (this.state.draft.pristine) {
- Actions.destroyDraft(this.props.draftClientId);
+ Actions.destroyDraft(this.props.headerMessageId);
}
}
diff --git a/packages/client-app/src/flux/action-bridge-cpp.es6 b/packages/client-app/src/flux/action-bridge-cpp.es6
index 60be9563e..45ffa9206 100644
--- a/packages/client-app/src/flux/action-bridge-cpp.es6
+++ b/packages/client-app/src/flux/action-bridge-cpp.es6
@@ -2,7 +2,8 @@ import net from 'net';
import fs from 'fs';
import DatabaseStore from './stores/database-store';
import DatabaseChangeRecord from './stores/database-change-record';
-
+import DatabaseObjectRegistry from '../registries/database-object-registry';
+import Actions from './actions';
import Utils from './models/utils';
class ActionBridgeCPP {
@@ -13,6 +14,13 @@ class ActionBridgeCPP {
return;
}
+ Actions.queueTask.listen(this.onQueueTask, this);
+ Actions.queueTasks.listen((tasks) => {
+ if (!tasks || !tasks.length) { return; }
+ for (const task of tasks) { this.onQueueTask(task); }
+ });
+ Actions.dequeueTask.listen(this.onDequeueTask, this);
+
try {
fs.unlinkSync('/tmp/cmail.sock');
} catch (err) {
@@ -41,7 +49,7 @@ class ActionBridgeCPP {
});
});
- unixServer.listen('/tmp/cmail.sock', () => {
+ unixServer.listen('/tmp/cmail.sock', () => {
console.log('server bound');
});
@@ -54,6 +62,29 @@ class ActionBridgeCPP {
process.on('SIGINT', shutdown);
}
+ onQueueTask(task) {
+ // if (!(task instanceof Task)) {
+ // console.log(task);
+ // throw new Error("You must queue a `Task` instance. Be sure you have the task registered with the DatabaseObjectRegistry. If this is a task for a custom plugin, you must export a `taskConstructors` array with your `Task` constructors in it. You must all subclass the base Nylas `Task`.");
+ // }
+ if (!DatabaseObjectRegistry.isInRegistry(task.constructor.name)) {
+ console.log(task);
+ throw new Error("You must queue a `Task` instance which is registred with the DatabaseObjectRegistry")
+ }
+ if (!task.id) {
+ console.log(task);
+ throw new Error("Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`");
+ }
+ task.sequentialId = ++this._currentSequentialId;
+ task.status = 'local';
+
+ this.onTellClients({type: 'task-queued', task: task});
+ }
+
+ onDequeueTask() { // task
+ throw new Error("Unimplemented");
+ }
+
onIncomingMessage(message) {
this._readBuffer += message;
const msgs = this._readBuffer.split('\n');
diff --git a/packages/client-app/src/flux/action-bridge.es6 b/packages/client-app/src/flux/action-bridge.es6
index 5d7047a46..690bd4c78 100644
--- a/packages/client-app/src/flux/action-bridge.es6
+++ b/packages/client-app/src/flux/action-bridge.es6
@@ -6,13 +6,13 @@ import DatabaseChangeRecord from './stores/database-change-record';
import Utils from './models/utils';
const Role = {
- WORK: 'work',
+ MAIN: 'default',
SECONDARY: 'secondary',
};
const TargetWindows = {
ALL: 'all',
- WORK: 'work',
+ MAIN: 'default',
};
const Message = {
@@ -49,7 +49,7 @@ class ActionBridge {
this.ipc = ipc;
this.ipcLastSendTime = null;
this.initiatorId = NylasEnv.getWindowType();
- this.role = NylasEnv.isWorkWindow() ? Role.WORK : Role.SECONDARY;
+ this.role = NylasEnv.isMainWindow() ? Role.MAIN : Role.SECONDARY;
NylasEnv.onBeforeUnload(this.onBeforeUnload);
@@ -76,11 +76,11 @@ class ActionBridge {
};
DatabaseStore.listen(databaseCallback, this);
- if (this.role !== Role.WORK) {
+ if (this.role !== Role.MAIN) {
// Observe all mainWindow actions fired in this window and re-broadcast
// them to other windows so the central application stores can take action
- Actions.workWindowActions.forEach(name => {
- const callback = (...args) => this.onRebroadcast(TargetWindows.WORK, name, args);
+ Actions.mainWindowActions.forEach(name => {
+ const callback = (...args) => this.onRebroadcast(TargetWindows.MAIN, name, args);
return Actions[name].listen(callback, this);
});
}
@@ -157,7 +157,7 @@ class ActionBridge {
const params = [];
args.forEach((arg) => {
if (arg instanceof Function) {
- throw new Error("ActionBridge cannot forward action argument of type `function` to work window.");
+ throw new Error("ActionBridge cannot forward action argument of type `function` to another window.");
}
return params.push(arg);
});
diff --git a/packages/client-app/src/flux/actions.es6 b/packages/client-app/src/flux/actions.es6
index 164866bd9..cee9d8bf1 100644
--- a/packages/client-app/src/flux/actions.es6
+++ b/packages/client-app/src/flux/actions.es6
@@ -2,7 +2,7 @@ import Reflux from 'reflux';
const ActionScopeWindow = 'window';
const ActionScopeGlobal = 'global';
-const ActionScopeWorkWindow = 'work';
+const ActionScopeMainWindow = 'main';
/*
Public: In the Flux {Architecture.md}, almost every user action
@@ -81,36 +81,33 @@ class Actions {
/*
Public: Queue a {Task} object to the {TaskQueue}.
- *Scope: Work Window*
+ *Scope: Main Window*
*/
- static queueTask = ActionScopeWorkWindow;
+ static queueTask = ActionScopeMainWindow;
/*
Public: Queue multiple {Task} objects to the {TaskQueue}, which should be
undone as a single user action.
- *Scope: Work Window*
+ *Scope: Main Window*
*/
- static queueTasks = ActionScopeWorkWindow;
-
- static undoTaskId = ActionScopeWorkWindow;
-
+ static queueTasks = ActionScopeMainWindow;
/*
Public: Dequeue all {Task}s from the {TaskQueue}. Use with care.
- *Scope: Work Window*
+ *Scope: Main Window*
*/
- static dequeueTask = ActionScopeWorkWindow;
+ static dequeueTask = ActionScopeMainWindow;
/*
Public: Dequeue a {Task} matching the description provided.
- *Scope: Work Window*
+ *Scope: Main Window*
*/
- static longPollReceivedRawDeltas = ActionScopeWorkWindow;
- static longPollProcessedDeltas = ActionScopeWorkWindow;
- static willMakeAPIRequest = ActionScopeWorkWindow;
- static didMakeAPIRequest = ActionScopeWorkWindow;
+ static longPollReceivedRawDeltas = ActionScopeMainWindow;
+ static longPollProcessedDeltas = ActionScopeMainWindow;
+ static willMakeAPIRequest = ActionScopeMainWindow;
+ static didMakeAPIRequest = ActionScopeMainWindow;
static checkOnlineStatus = ActionScopeWindow;
@@ -456,7 +453,7 @@ class Actions {
```
Actions.removeFile
file: fileObject
- messageClientId: draftClientId
+ headerMessageId: headerMessageId
```
*/
static removeFile = ActionScopeWindow;
@@ -510,7 +507,7 @@ class Actions {
/*
Public: Publish a user event to any analytics services linked to N1.
*/
- static recordUserEvent = ActionScopeWorkWindow;
+ static recordUserEvent = ActionScopeMainWindow;
static addMailRule = ActionScopeWindow;
static reorderMailRule = ActionScopeWindow;
@@ -570,14 +567,14 @@ const create = (obj, name, scope) => {
const scopes = {
window: [],
global: [],
- work: [],
+ main: [],
};
for (const name of Object.getOwnPropertyNames(Actions)) {
if (name === 'length' || name === 'name' || name === 'arguments' || name === 'caller' || name === 'prototype') {
continue;
}
- if (Actions[name] !== 'window' && Actions[name] !== 'global' && Actions[name] !== 'work') {
+ if (Actions[name] !== 'window' && Actions[name] !== 'global' && Actions[name] !== 'main') {
continue;
}
const scope = Actions[name];
@@ -586,7 +583,7 @@ for (const name of Object.getOwnPropertyNames(Actions)) {
}
Actions.windowActions = scopes.window;
-Actions.workWindowActions = scopes.work;
+Actions.mainWindowActions = scopes.main;
Actions.globalActions = scopes.global;
export default Actions;
diff --git a/packages/client-app/src/flux/models/message.es6 b/packages/client-app/src/flux/models/message.es6
index b1503eca1..d5cdf4c7f 100644
--- a/packages/client-app/src/flux/models/message.es6
+++ b/packages/client-app/src/flux/models/message.es6
@@ -139,8 +139,9 @@ export default class Message extends ModelWithMetadata {
modelKey: 'threadId',
}),
- messageIdHeader: Attributes.String({
- modelKey: 'messageIdHeader',
+ headerMessageId: Attributes.String({
+ queryable: true,
+ modelKey: 'headerMessageId',
}),
subject: Attributes.String({
diff --git a/packages/client-app/src/flux/models/model.coffee b/packages/client-app/src/flux/models/model.coffee
index bb2bf2f0a..16c63fa0c 100644
--- a/packages/client-app/src/flux/models/model.coffee
+++ b/packages/client-app/src/flux/models/model.coffee
@@ -27,9 +27,6 @@ class Model
queryable: true
modelKey: 'id'
- 'object': Attributes.String
- modelKey: 'object'
-
'accountId': Attributes.String
queryable: true
modelKey: 'accountId'
@@ -96,6 +93,7 @@ class Model
continue if attr instanceof Attributes.AttributeJoinedData and options.joined is false
json[attr.jsonKey] = attr.toJSON(attrValue)
json["id"] = @id
+ json["__constructorName"] = this.constructor.name
json
toString: ->
diff --git a/packages/client-app/src/flux/nylas-api.es6 b/packages/client-app/src/flux/nylas-api.es6
index e8cb3d816..41707f520 100644
--- a/packages/client-app/src/flux/nylas-api.es6
+++ b/packages/client-app/src/flux/nylas-api.es6
@@ -1,4 +1,4 @@
-import {AccountStore} from 'nylas-exports'
+import AccountStore from './stores/account-store'
import NylasLongConnection from './nylas-long-connection'
// A 0 code is when an error returns without a status code, like "ESOCKETTIMEDOUT"
diff --git a/packages/client-app/src/flux/stores/database-store.es6 b/packages/client-app/src/flux/stores/database-store.es6
index 31869700e..7d1e2f63c 100644
--- a/packages/client-app/src/flux/stores/database-store.es6
+++ b/packages/client-app/src/flux/stores/database-store.es6
@@ -3,7 +3,6 @@ import path from 'path';
import createDebug from 'debug';
import childProcess from 'child_process';
import PromiseQueue from 'promise-queue';
-import {remote} from 'electron';
import LRU from "lru-cache";
import {ExponentialBackoffScheduler} from '../../backoff-schedulers';
@@ -111,21 +110,6 @@ class DatabaseStore extends NylasStore {
this._emitter.emit('ready')
}
- // When 3rd party components register new models, we need to refresh the
- // database schema to prepare those tables. This method may be called
- // extremely frequently as new models are added when packages load.
- refreshDatabaseSchema() {
- if (!NylasEnv.isMainWindow()) {
- return Promise.resolve();
- }
- const app = remote.getGlobal('application');
- const phase = app.databasePhase();
- if (phase !== DatabasePhase.Setup) {
- app.setDatabasePhase(DatabasePhase.Setup);
- }
- return this._asyncWaitForReady()
- }
-
_prettyConsoleLog(qa) {
let q = qa.replace(/%/g, '%%');
q = `color:black |||%c ${q}`;
@@ -200,7 +184,7 @@ class DatabaseStore extends NylasStore {
const msec = Date.now() - start;
if (debugVerbose.enabled) {
const q = `🔶 (${msec}ms) Background: ${query}`;
- debugVerbose(StringUtils.trimTo(q))
+ debugVerbose(trimTo(q))
}
if (msec > 100) {
@@ -253,7 +237,7 @@ class DatabaseStore extends NylasStore {
const msec = Date.now() - start;
if (debugVerbose.enabled) {
const q = `(${msec}ms) ${query}`;
- debugVerbose(StringUtils.trimTo(q))
+ debugVerbose(trimTo(q))
}
if (msec > 100) {
diff --git a/packages/client-app/src/flux/stores/draft-editing-session.coffee b/packages/client-app/src/flux/stores/draft-editing-session.coffee
index d6d309f30..f7c64c94e 100644
--- a/packages/client-app/src/flux/stores/draft-editing-session.coffee
+++ b/packages/client-app/src/flux/stores/draft-editing-session.coffee
@@ -102,7 +102,7 @@ class DraftEditingSession
@include Publisher
@include Listener
- constructor: (@draftId, draft = null) ->
+ constructor: (@headerMessageId, draft = null) ->
DraftStore ?= require('./draft-store').default
@listenTo DraftStore, @_onDraftChanged
@@ -137,9 +137,9 @@ class DraftEditingSession
@_draftPristineBody
prepare: ->
- @_draftPromise ?= DatabaseStore.findBy(Message, id: @draftId).include(Message.attributes.body).then (draft) =>
+ @_draftPromise ?= DatabaseStore.findBy(Message, headerMessageId: @headerMessageId).include(Message.attributes.body).then (draft) =>
return Promise.reject(new Error("Draft has been destroyed.")) if @_destroyed
- return Promise.reject(new Error("Assertion Failure: Draft #{@draftId} not found.")) if not draft
+ return Promise.reject(new Error("Assertion Failure: Draft #{@headerMessageId} not found.")) if not draft
return @_setDraft(draft)
teardown: ->
@@ -261,31 +261,18 @@ class DraftEditingSession
# underneath us
inMemoryDraft = @_draft
- DatabaseStore.inTransaction (t) =>
- t.findBy(Message, id: inMemoryDraft.id).include(Message.attributes.body).then (draft) =>
- # This can happen if we get a "delete" delta, or something else
- # strange happens. In this case, we'll use the @_draft we have in
- # memory to apply the changes to. On the `persistModel` in the
- # next line it will save the correct changes. The
- # `SyncbackDraftTask` may then fail due to differing Ids not
- # existing, but if this happens it'll 404 and recover gracefully
- # by creating a new draft
- draft ?= inMemoryDraft
- updatedDraft = @changes.applyToModel(draft)
- return t.persistModel(updatedDraft)
-
- .then =>
- return if noSyncback
- # We have temporarily disabled the syncback of most drafts to user's mail
- # providers, due to a number of issues in the sync-engine that we're still
- # firefighting.
- #
- # For now, drafts are only synced when you choose "Send Later", and then
- # once they have a serverId we sync them periodically here.
- #
- return unless @_draft.serverId
- Actions.ensureDraftSynced(@draftId)
-
+ DatabaseStore.findBy(Message, id: inMemoryDraft.id).include(Message.attributes.body).then (draft) =>
+ # This can happen if we get a "delete" delta, or something else
+ # strange happens. In this case, we'll use the @_draft we have in
+ # memory to apply the changes to. On the `persistModel` in the
+ # next line it will save the correct changes. The
+ # `SyncbackDraftTask` may then fail due to differing Ids not
+ # existing, but if this happens it'll 404 and recover gracefully
+ # by creating a new draft
+ draft ?= inMemoryDraft
+ updatedDraft = @changes.applyToModel(draft)
+ console.log("Queueing SyncbackDraftTask")
+ Actions.queueTask(new SyncbackDraftTask(updatedDraft))
# Undo / Redo
diff --git a/packages/client-app/src/flux/stores/draft-factory.coffee b/packages/client-app/src/flux/stores/draft-factory.coffee
index 521e75308..ffc3de4b7 100644
--- a/packages/client-app/src/flux/stores/draft-factory.coffee
+++ b/packages/client-app/src/flux/stores/draft-factory.coffee
@@ -27,7 +27,11 @@ class DraftFactory
Promise.resolve(new Message(_.extend({
body: ''
subject: ''
- id: Utils.generateTempId()
+ version: 0
+ unread: false
+ starred: false
+ folderImapUID: 0
+ headerMessageId: Utils.generateTempId()
from: [account.defaultMe()]
date: (new Date)
draft: true
diff --git a/packages/client-app/src/flux/stores/draft-store.es6 b/packages/client-app/src/flux/stores/draft-store.es6
index 0deb0d458..10c471f57 100644
--- a/packages/client-app/src/flux/stores/draft-store.es6
+++ b/packages/client-app/src/flux/stores/draft-store.es6
@@ -91,26 +91,26 @@ class DraftStore extends NylasStore {
@param {String} clientId - The clientId of the draft.
@returns {Promise} - Resolves to an {DraftEditingSession} for the draft once it has been prepared
*/
- sessionForClientId(clientId) {
- if (!clientId) {
+ sessionForClientId(headerMessageId) {
+ if (!headerMessageId) {
throw new Error("DraftStore::sessionForClientId requires a clientId");
}
- if (this._draftSessions[clientId] == null) {
- this._draftSessions[clientId] = this._createSession(clientId);
+ if (this._draftSessions[headerMessageId] == null) {
+ this._draftSessions[headerMessageId] = this._createSession(headerMessageId);
}
- return this._draftSessions[clientId].prepare();
+ return this._draftSessions[headerMessageId].prepare();
}
- // Public: Look up the sending state of the given draftClientId.
+ // Public: Look up the sending state of the given draft headerMessageId.
// In popout windows the existance of the window is the sending state.
- isSendingDraft(draftClientId) {
- return this._draftsSending[draftClientId] || false;
+ isSendingDraft(headerMessageId) {
+ return this._draftsSending[headerMessageId] || false;
}
_doneWithSession(session) {
session.teardown();
- delete this._draftSessions[session.draftClientId];
+ delete this._draftSessions[session.headerMessageId];
}
_cleanupAllSessions() {
@@ -129,7 +129,7 @@ class DraftStore extends NylasStore {
_.each(this._draftSessions, (session) => {
const draft = session.draft()
if (draft && draft.pristine) {
- Actions.queueTask(new DestroyDraftTask(session.draftClientId));
+ Actions.queueTask(new DestroyDraftTask(session.headerMessageId));
} else {
promises.push(session.changes.commit());
}
@@ -172,10 +172,10 @@ class DraftStore extends NylasStore {
.then((draft) => {
draft.body = `${body}\n\n${draft.body}`
draft.pristine = false;
- return DatabaseStore.inTransaction((t) => {
- return t.persistModel(draft);
- })
- .then(() => {
+
+ const t = new SyncbackDraftTask(draft)
+ Actions.queueTask(t)
+ TaskQueue.waitForPerformLocal(t).then(() => {
Actions.sendDraft(draft.clientId);
});
});
@@ -249,17 +249,17 @@ class DraftStore extends NylasStore {
// doesn't need to do a query for it a second from now when the composer wants it.
this._createSession(draft.clientId, draft);
- return DatabaseStore.inTransaction((t) => {
- return t.persistModel(draft);
- })
- .then(() => {
+ const task = new SyncbackDraftTask(draft);
+ Actions.queueTask(task)
+
+ return TaskQueue.waitForPerformLocal(task).then(() => {
if (popout) {
- this._onPopoutDraftClientId(draft.clientId);
+ this._onPopoutDraftClientId(draft.headerMessageId);
} else {
- Actions.focusDraft({draftClientId: draft.clientId});
+ Actions.focusDraft({headerMessageId: draft.headerMessageId});
}
+ return {headerMessageId: draft.headerMessageId, draft};
})
- .thenReturn({draftClientId: draft.clientId, draft});
}
_createSession(clientId, draft) {
@@ -276,18 +276,18 @@ class DraftStore extends NylasStore {
_onPopoutBlankDraft = () => {
Actions.recordUserEvent("Draft Created", {type: "new"});
return DraftFactory.createDraft().then((draft) => {
- return this._finalizeAndPersistNewMessage(draft).then(({draftClientId}) => {
- return this._onPopoutDraftClientId(draftClientId, {newDraft: true});
+ return this._finalizeAndPersistNewMessage(draft).then(({headerMessageId}) => {
+ return this._onPopoutDraftClientId(headerMessageId, {newDraft: true});
});
});
}
- _onPopoutDraftClientId = (draftClientId, options = {}) => {
- if (draftClientId == null) {
- throw new Error("DraftStore::onPopoutDraftId - You must provide a draftClientId");
+ _onPopoutDraftClientId = (headerMessageId, options = {}) => {
+ if (headerMessageId == null) {
+ throw new Error("DraftStore::onPopoutDraftId - You must provide a headerMessageId");
}
const title = options.newDraft ? "New Message" : "Message";
- return this.sessionForClientId(draftClientId).then((session) => {
+ return this.sessionForClientId(headerMessageId).then((session) => {
return session.changes.commit().then(() => {
const draftJSON = session.draft().toJSON();
// Since we pass a windowKey, if the popout composer draft already
@@ -296,9 +296,9 @@ class DraftStore extends NylasStore {
NylasEnv.newWindow({
title,
hidden: true, // We manually show in ComposerWithWindowProps::onDraftReady
- windowKey: `composer-${draftClientId}`,
+ windowKey: `composer-${headerMessageId}`,
windowType: 'composer-preload',
- windowProps: _.extend(options, {draftClientId, draftJSON}),
+ windowProps: _.extend(options, {headerMessageId, draftJSON}),
});
});
});
@@ -318,27 +318,27 @@ class DraftStore extends NylasStore {
return DraftFactory.createDraft().then((draft) => {
return this._finalizeAndPersistNewMessage(draft);
})
- .then(({draftClientId}) => {
+ .then(({headerMessageId}) => {
let remaining = paths.length;
const callback = () => {
remaining -= 1;
if (remaining === 0) {
- this._onPopoutDraftClientId(draftClientId);
+ this._onPopoutDraftClientId(headerMessageId);
}
};
paths.forEach((path) => {
Actions.addAttachment({
filePath: path,
- messageClientId: draftClientId,
+ messageClientId: headerMessageId,
onUploadCreated: callback,
});
})
});
}
- _onDestroyDraft = (draftClientId) => {
- const session = this._draftSessions[draftClientId];
+ _onDestroyDraft = (headerMessageId) => {
+ const session = this._draftSessions[headerMessageId];
// Immediately reset any pending changes so no saves occur
if (session) {
@@ -347,34 +347,34 @@ class DraftStore extends NylasStore {
// Stop any pending tasks related ot the draft
TaskQueue.queue().forEach((task) => {
- if (task instanceof BaseDraftTask && task.draftClientId === draftClientId) {
+ if (task instanceof BaseDraftTask && task.headerMessageId === headerMessageId) {
Actions.dequeueTask(task.id);
}
})
// Queue the task to destroy the draft
- Actions.queueTask(new DestroyDraftTask(draftClientId));
+ Actions.queueTask(new DestroyDraftTask(headerMessageId));
if (NylasEnv.isComposerWindow()) {
NylasEnv.close();
}
}
- _onEnsureDraftSynced = (draftClientId) => {
- return this.sessionForClientId(draftClientId).then((session) => {
+ _onEnsureDraftSynced = (headerMessageId) => {
+ return this.sessionForClientId(headerMessageId).then((session) => {
return DraftHelpers.prepareDraftForSyncback(session)
.then(() => {
- Actions.queueTask(new SyncbackDraftTask(draftClientId));
+ Actions.queueTask(new SyncbackDraftTask(headerMessageId));
});
});
}
- _onSendDraft = (draftClientId, sendActionKey = DefaultSendActionKey) => {
- this._draftsSending[draftClientId] = true;
- return this.sessionForClientId(draftClientId).then((session) => {
+ _onSendDraft = (headerMessageId, sendActionKey = DefaultSendActionKey) => {
+ this._draftsSending[headerMessageId] = true;
+ return this.sessionForClientId(headerMessageId).then((session) => {
return DraftHelpers.prepareDraftForSyncback(session)
.then(() => {
- Actions.queueTask(new PerformSendActionTask(draftClientId, sendActionKey));
+ Actions.queueTask(new PerformSendActionTask(headerMessageId, sendActionKey));
this._doneWithSession(session);
if (NylasEnv.config.get("core.sending.sounds")) {
SoundRegistry.playSound('hit-send');
@@ -387,7 +387,7 @@ class DraftStore extends NylasStore {
}
__testExtensionTransforms() {
- const clientId = NylasEnv.getWindowProps().draftClientId;
+ const clientId = NylasEnv.getWindowProps().headerMessageId;
return this.sessionForClientId(clientId).then((session) => {
return this._prepareForSyncback(session).then(() => {
window.__draft = session.draft();
@@ -405,19 +405,19 @@ class DraftStore extends NylasStore {
});
}
- _onDidCancelSendAction = ({draftClientId}) => {
- delete this._draftsSending[draftClientId];
- this.trigger(draftClientId);
+ _onDidCancelSendAction = ({headerMessageId}) => {
+ delete this._draftsSending[headerMessageId];
+ this.trigger(headerMessageId);
}
- _onSendDraftSuccess = ({draftClientId}) => {
- delete this._draftsSending[draftClientId];
- this.trigger(draftClientId);
+ _onSendDraftSuccess = ({headerMessageId}) => {
+ delete this._draftsSending[headerMessageId];
+ this.trigger(headerMessageId);
}
- _onSendDraftFailed = ({draftClientId, threadId, errorMessage, errorDetail}) => {
- this._draftsSending[draftClientId] = false;
- this.trigger(draftClientId);
+ _onSendDraftFailed = ({headerMessageId, threadId, errorMessage, errorDetail}) => {
+ this._draftsSending[headerMessageId] = false;
+ this.trigger(headerMessageId);
if (NylasEnv.isMainWindow()) {
// We delay so the view has time to update the restored draft. If we
// don't delay the modal may come up in a state where the draft looks
@@ -426,17 +426,17 @@ class DraftStore extends NylasStore {
// We also need to delay because the old draft window needs to fully
// close. It takes windows currently (June 2016) 100ms to close by
setTimeout(() => {
- this._notifyUserOfError({draftClientId, threadId, errorMessage, errorDetail});
+ this._notifyUserOfError({headerMessageId, threadId, errorMessage, errorDetail});
}, 300);
}
}
- _notifyUserOfError({draftClientId, threadId, errorMessage, errorDetail}) {
+ _notifyUserOfError({headerMessageId, threadId, errorMessage, errorDetail}) {
const focusedThread = FocusedContentStore.focused('thread');
if (threadId && focusedThread && focusedThread.id === threadId) {
NylasEnv.showErrorDialog(errorMessage, {detail: errorDetail});
} else {
- Actions.composePopoutDraft(draftClientId, {errorMessage, errorDetail});
+ Actions.composePopoutDraft(headerMessageId, {errorMessage, errorDetail});
}
}
}
diff --git a/packages/client-app/src/flux/stores/file-upload-store.es6 b/packages/client-app/src/flux/stores/file-upload-store.es6
index 4ec50e36f..a9484bc0d 100644
--- a/packages/client-app/src/flux/stores/file-upload-store.es6
+++ b/packages/client-app/src/flux/stores/file-upload-store.es6
@@ -50,7 +50,7 @@ class FileUploadStore extends NylasStore {
mkdirp.sync(UPLOAD_DIR);
if (NylasEnv.isMainWindow() || NylasEnv.inSpecMode()) {
this.listenTo(Actions.ensureMessageInSentSuccess, ({messageClientId}) => {
- this._deleteUploadsForClientId(messageClientId);
+ this._deleteUploadsForId(messageClientId);
});
}
}
@@ -107,7 +107,7 @@ class FileUploadStore extends NylasStore {
.catch((err) => Promise.reject(new Error(`Error deleting file upload ${upload.filename}:\n\n${err.message}`)));
}
- _deleteUploadsForClientId(messageClientId) {
+ _deleteUploadsForId(messageClientId) {
rimraf(path.join(UPLOAD_DIR, messageClientId), {disableGlob: true}, (err) => {
if (err) {
console.warn(err);
diff --git a/packages/client-app/src/flux/stores/task-queue.es6 b/packages/client-app/src/flux/stores/task-queue.es6
index fbbcaebef..9fefd76be 100644
--- a/packages/client-app/src/flux/stores/task-queue.es6
+++ b/packages/client-app/src/flux/stores/task-queue.es6
@@ -2,8 +2,6 @@ import _ from 'underscore';
import NylasStore from 'nylas-store';
import {Rx} from 'nylas-exports';
import Task from "../tasks/task";
-import DatabaseObjectRegistry from '../../registries/database-object-registry';
-import Actions from '../actions';
import DatabaseStore from './database-store';
/**
@@ -52,19 +50,28 @@ class TaskQueue extends NylasStore {
this._waitingForRemote = [];
Rx.Observable.fromQuery(DatabaseStore.findAll(Task)).subscribe((tasks => {
- this._queue = tasks.filter(t => t.complete === false);
- this._completed = tasks.filter(t => t.complete === true);
- this.trigger();
- // TODO : this._waitingForLocal!
- }))
+ this._queue = tasks.filter(t => t.status !== 'complete');
+ this._completed = tasks.filter(t => t.status === 'complete');
+ const all = [].concat(this._queue, this._completed);
- this.listenTo(Actions.queueTask, this.enqueue)
- this.listenTo(Actions.queueTasks, (tasks) => {
- if (!tasks || !tasks.length) { return; }
- for (const task of tasks) { this.enqueue(task); }
- });
- this.listenTo(Actions.undoTaskId, this.enqueueUndoOfTaskId);
- this.listenTo(Actions.dequeueTask, this.dequeue);
+ this._waitingForLocal.filter(({task, resolve}) => {
+ if (all.find(t => task.id === t.id)) {
+ resolve();
+ return false;
+ }
+ return true;
+ });
+
+ this._waitingForRemote.filter(({task, resolve}) => {
+ if (this._completed.find(t => task.id === t.id)) {
+ resolve();
+ return false;
+ }
+ return true;
+ });
+
+ this.trigger();
+ }));
}
queue() {
@@ -88,7 +95,7 @@ class TaskQueue extends NylasStore {
{SaveDraftTask} or 'SaveDraftTask')
- `matching`: Optional An {Object} with criteria to pass to _.isMatch. For a
- SaveDraftTask, this could be {draftClientId: "123123"}
+ SaveDraftTask, this could be {headerMessageId: "123123"}
Returns a matching {Task}, or null.
*/
@@ -111,32 +118,6 @@ class TaskQueue extends NylasStore {
return matches;
}
- enqueue = (task) => {
- if (!(task instanceof Task)) {
- console.log(task);
- throw new Error("You must queue a `Task` instance. Be sure you have the task registered with the DatabaseObjectRegistry. If this is a task for a custom plugin, you must export a `taskConstructors` array with your `Task` constructors in it. You must all subclass the base Nylas `Task`.");
- }
- if (!DatabaseObjectRegistry.isInRegistry(task.constructor.name)) {
- console.log(task);
- throw new Error("You must queue a `Task` instance which is registred with the DatabaseObjectRegistry")
- }
- if (!task.id) {
- console.log(task);
- throw new Error("Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`");
- }
- task.sequentialId = ++this._currentSequentialId;
- task.status = 'local';
-
- NylasEnv.actionBridgeCpp.onTellClients({type: 'task-queued', task: task});
- }
-
- enqueueUndoOfTaskId = (taskId) => {
- const task = this._queue.find(t => t.id === taskId) || this._completed.find(t => t.id === taskId);
- if (task) {
- this.enqueue(task.createUndoTask());
- }
- }
-
dequeue = (taskOrId) => {
const task = this._resolveTaskArgument(taskOrId);
if (!task) {
diff --git a/packages/client-app/src/flux/stores/undo-redo-store.es6 b/packages/client-app/src/flux/stores/undo-redo-store.es6
index 501350b00..6be6a35d5 100644
--- a/packages/client-app/src/flux/stores/undo-redo-store.es6
+++ b/packages/client-app/src/flux/stores/undo-redo-store.es6
@@ -43,7 +43,7 @@ class UndoRedoStore extends NylasStore {
this.trigger();
for (const task of topTasks) {
- Actions.undoTaskId(task.id);
+ Actions.queueTask(task.createUndoTask());
}
const redoTasks = topTasks.map((t) => {
diff --git a/packages/client-app/src/flux/tasks/base-draft-task.es6 b/packages/client-app/src/flux/tasks/base-draft-task.es6
index d4c857bdc..607b80fda 100644
--- a/packages/client-app/src/flux/tasks/base-draft-task.es6
+++ b/packages/client-app/src/flux/tasks/base-draft-task.es6
@@ -3,47 +3,8 @@ import DraftHelpers from '../stores/draft-helpers';
export default class BaseDraftTask extends Task {
- static DraftNotFoundError = DraftHelpers.DraftNotFoundError;
-
- constructor(draftClientId) {
+ constructor(draft) {
super();
- this.draftClientId = draftClientId;
- this.draft = null;
- }
-
- shouldDequeueOtherTask(other) {
- const isSameDraft = (other.draftClientId === this.draftClientId);
- const isOlderTask = (other.sequentialId < this.sequentialId);
- const isExactClass = (other.constructor.name === this.constructor.name);
- return (isSameDraft && isOlderTask && isExactClass);
- }
-
- isDependentOnTask(other) {
- // Set this task to be dependent on any SyncbackDraftTasks and
- // SendDraftTasks for the same draft that were created first.
- // This, in conjunction with this method on SendDraftTask, ensures
- // that a send and a syncback never run at the same time for a draft.
-
- // Require here rather than on top to avoid a circular dependency
- const isSameDraft = (other.draftClientId === this.draftClientId);
- const isOlderTask = (other.sequentialId < this.sequentialId);
- const isSaveOrSend = (other instanceof BaseDraftTask);
- return (isSameDraft && isOlderTask && isSaveOrSend);
- }
-
- performLocal() {
- // SyncbackDraftTask does not do anything locally. You should persist your changes
- // to the local database directly or using a DraftEditingSession, and then queue a
- // SyncbackDraftTask to send those changes to the server.
- if (!this.draftClientId) {
- const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a draftClientId`;
- return Promise.reject(new Error(errMsg));
- }
- return Promise.resolve();
- }
-
- async refreshDraftReference() {
- this.draft = await DraftHelpers.refreshDraftReference(this.draftClientId)
- return this.draft;
+ this.draft = draft;
}
}
diff --git a/packages/client-app/src/flux/tasks/destroy-draft-task.es6 b/packages/client-app/src/flux/tasks/destroy-draft-task.es6
index b8bc8d81f..f5761ecda 100644
--- a/packages/client-app/src/flux/tasks/destroy-draft-task.es6
+++ b/packages/client-app/src/flux/tasks/destroy-draft-task.es6
@@ -1,21 +1,8 @@
-import Task from './task';
-import {APIError} from '../errors';
-import Message from '../models/message';
-import DatabaseStore from '../stores/database-store';
-import NylasAPI from '../nylas-api';
-import NylasAPIRequest from '../nylas-api-request';
import BaseDraftTask from './base-draft-task';
export default class DestroyDraftTask extends BaseDraftTask {
-
- shouldDequeueOtherTask(other) {
- return (other instanceof BaseDraftTask && other.draftClientId === this.draftClientId);
- }
-
- performLocal() {
- super.performLocal();
- return this.refreshDraftReference()
- .then(() => DatabaseStore.inTransaction((t) => t.unpersistModel(this.draft)))
- .catch(BaseDraftTask.DraftNotFoundError, () => Promise.resolve());
+ constructor(headerMessageId) {
+ super();
+ this.headerMessageId = headerMessageId;
}
}
diff --git a/packages/client-app/src/flux/tasks/perform-send-action-task.es6 b/packages/client-app/src/flux/tasks/perform-send-action-task.es6
index 2d2e242b6..5395c5d6a 100644
--- a/packages/client-app/src/flux/tasks/perform-send-action-task.es6
+++ b/packages/client-app/src/flux/tasks/perform-send-action-task.es6
@@ -7,8 +7,8 @@ import SendActionsStore from '../stores/send-actions-store';
class PerformSendActionTask extends BaseDraftTask {
- constructor(draftClientId, sendActionKey) {
- super(draftClientId)
+ constructor(headerMessageId, sendActionKey) {
+ super(headerMessageId)
this._sendActionKey = sendActionKey
this._sendTimer = null
this._taskResolve = () => {}
@@ -21,22 +21,22 @@ class PerformSendActionTask extends BaseDraftTask {
shouldDequeueOtherTask(otherTask) {
return (
otherTask instanceof PerformSendActionTask &&
- this.draftClientId === otherTask.draftClientId
+ this.headerMessageId === otherTask.headerMessageId
)
}
performLocal() {
- if (!this.draftClientId) {
- const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a draftClientId`;
+ if (!this.headerMessageId) {
+ const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a headerMessageId`;
return Promise.reject(new Error(errMsg));
}
return Promise.resolve()
}
cancel() {
- const {id: taskId, draftClientId} = this
+ const {id: taskId, headerMessageId} = this
clearTimeout(this._sendTimer)
- Actions.didCancelSendAction({taskId, draftClientId})
+ Actions.didCancelSendAction({taskId, headerMessageId})
this._taskResolve(Task.Status.Continue)
}
diff --git a/packages/client-app/src/nylas-env.es6 b/packages/client-app/src/nylas-env.es6
index e877ee720..8c0e814d7 100644
--- a/packages/client-app/src/nylas-env.es6
+++ b/packages/client-app/src/nylas-env.es6
@@ -171,9 +171,9 @@ export default class NylasEnvConstructor {
const PackageManager = require('./package-manager');
const ThemeManager = require('./theme-manager');
const StyleManager = require('./style-manager');
+ const MenuManager = require('./menu-manager').default;
const ActionBridge = require('./flux/action-bridge').default;
const ActionBridgeCPP = require('./flux/action-bridge-cpp').default;
- const MenuManager = require('./menu-manager').default;
const {devMode, benchmarkMode, safeMode, resourcePath, configDirPath, windowType} = this.getLoadSettings();
@@ -371,8 +371,13 @@ export default class NylasEnvConstructor {
if (event.defaultPrevented) { return; }
this.lastUncaughtError = error;
-
- extra.pluginIds = this._findPluginsFromError(error);
+
+ try {
+ extra.pluginIds = this._findPluginsFromError(error);
+ } catch (err) {
+ // can happen when an error is thrown very early
+ extra.pluginIds = [];
+ }
if (this.inSpecMode()) {
jasmine.getEnv().currentSpec.fail(error);
diff --git a/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx b/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx
index 1aca3968d..5ee2e991c 100644
--- a/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx
+++ b/packages/client-app/static/package-template/spec/my-composer-button-spec.jsx
@@ -6,7 +6,7 @@ import MyComposerButton from '../lib/my-composer-button';
describe("MyComposerButton", () => {
beforeEach(() => {
this.component = ReactTestUtils.renderIntoDocument(
-
+
);
});