mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-19 19:14:59 +08:00
fix(mail-merge): Refactor mass sending procedure
Summary: This diff introduces several updates to mail merge to improve the procedure for sending a list of drafts. Specifically, sending mass email will now: - Clear mail merge metadata on the drafts that will actually be sent - Upload attached files only /once/, and reuse those files on the drafts that will actually be sent - Minimize database writes for new drafts being created - Will queue a SendManyDraftsTask that will subsequently queue the necessary SendDraftTasks and keep track of them, and notify of any failed tasks TODO: - Add state to MailMerge plugin for failed sends and ability to attempt to re send them Test Plan: - TODO Reviewers: evan, bengotow, jackie Reviewed By: bengotow, jackie Subscribers: jackie Differential Revision: https://phab.nylas.com/D2973
This commit is contained in:
parent
c9ea5b6483
commit
a4ee61eadc
26 changed files with 397 additions and 266 deletions
|
@ -2,7 +2,7 @@ import _ from 'underscore';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import AccountContactField from './account-contact-field';
|
||||
import {Utils, Actions, AccountStore} from 'nylas-exports';
|
||||
import {Utils, DraftHelpers, Actions, AccountStore} from 'nylas-exports';
|
||||
import {InjectedComponent, KeyCommandsRegion, ParticipantsTextField, ListensToFluxStore} from 'nylas-component-kit';
|
||||
|
||||
import CollapsedParticipants from './collapsed-participants';
|
||||
|
@ -105,7 +105,7 @@ export default class ComposerHeader extends React.Component {
|
|||
if (_.isEmpty(this.props.draft.subject)) {
|
||||
return true;
|
||||
}
|
||||
if (Utils.isForwardedMessage(this.props.draft)) {
|
||||
if (DraftHelpers.isForwardedMessage(this.props.draft)) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.draft.replyToMessageId) {
|
||||
|
@ -153,7 +153,7 @@ export default class ComposerHeader extends React.Component {
|
|||
if (isDropping) {
|
||||
this.setState({
|
||||
participantsFocused: true,
|
||||
enabledFields: [...Fields.ParticipantFields, Fields.Subject],
|
||||
enabledFields: [...Fields.ParticipantFields, Fields.From, Fields.Subject],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import {
|
|||
Utils,
|
||||
Actions,
|
||||
DraftStore,
|
||||
ContactStore,
|
||||
QuotedHTMLTransformer,
|
||||
UndoManager,
|
||||
DraftHelpers,
|
||||
FileDownloadStore,
|
||||
ExtensionRegistry,
|
||||
QuotedHTMLTransformer,
|
||||
} from 'nylas-exports';
|
||||
|
||||
import {
|
||||
|
@ -55,7 +55,7 @@ export default class ComposerView extends React.Component {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
showQuotedText: Utils.isForwardedMessage(props.draft),
|
||||
showQuotedText: DraftHelpers.isForwardedMessage(props.draft),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,14 +65,14 @@ export default class ComposerView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.session !== this.props.session) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.session !== this.props.session) {
|
||||
this._teardownForProps();
|
||||
this._setupForProps(newProps);
|
||||
this._setupForProps(nextProps);
|
||||
}
|
||||
if (Utils.isForwardedMessage(this.props.draft) !== Utils.isForwardedMessage(newProps.draft)) {
|
||||
if (DraftHelpers.isForwardedMessage(this.props.draft) !== DraftHelpers.isForwardedMessage(nextProps.draft)) {
|
||||
this.setState({
|
||||
showQuotedText: Utils.isForwardedMessage(newProps.draft),
|
||||
showQuotedText: DraftHelpers.isForwardedMessage(nextProps.draft),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -507,56 +507,19 @@ export default class ComposerView extends React.Component {
|
|||
}
|
||||
|
||||
const dialog = remote.dialog;
|
||||
const {session} = this.props
|
||||
const {errors, warnings} = session.validateDraftForSending()
|
||||
|
||||
const {to, cc, bcc, body, files, uploads} = this.props.draft;
|
||||
const allRecipients = [].concat(to, cc, bcc);
|
||||
let dealbreaker = null;
|
||||
|
||||
for (const contact of allRecipients) {
|
||||
if (!ContactStore.isValidContact(contact)) {
|
||||
dealbreaker = `${contact.email} is not a valid email address - please remove or edit it before sending.`
|
||||
}
|
||||
}
|
||||
if (allRecipients.length === 0) {
|
||||
dealbreaker = 'You need to provide one or more recipients before sending the message.';
|
||||
}
|
||||
|
||||
if (dealbreaker) {
|
||||
if (errors.length > 0) {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
buttons: ['Edit Message', 'Cancel'],
|
||||
message: 'Cannot Send',
|
||||
detail: dealbreaker,
|
||||
detail: errors[0],
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const bodyIsEmpty = body === this.props.session.draftPristineBody();
|
||||
const forwarded = Utils.isForwardedMessage(this.props.draft);
|
||||
const hasAttachment = (files || []).length > 0 || (uploads || []).length > 0;
|
||||
|
||||
let warnings = [];
|
||||
|
||||
if (this.props.draft.subject.length === 0) {
|
||||
warnings.push('without a subject line');
|
||||
}
|
||||
|
||||
if (this._mentionsAttachment(this.props.draft.body) && !hasAttachment) {
|
||||
warnings.push('without an attachment');
|
||||
}
|
||||
|
||||
if (bodyIsEmpty && !forwarded && !hasAttachment) {
|
||||
warnings.push('without a body');
|
||||
}
|
||||
|
||||
// Check third party warnings added via Composer extensions
|
||||
for (const extension of ExtensionRegistry.Composer.extensions()) {
|
||||
if (!extension.warningsForSending) {
|
||||
continue;
|
||||
}
|
||||
warnings = warnings.concat(extension.warningsForSending({draft: this.props.draft}));
|
||||
}
|
||||
|
||||
if ((warnings.length > 0) && (!options.force)) {
|
||||
const response = dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'warning',
|
||||
|
@ -569,7 +532,6 @@ export default class ComposerView extends React.Component {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -585,15 +547,62 @@ export default class ComposerView extends React.Component {
|
|||
Actions.selectAttachment({messageClientId: this.props.draft.clientId});
|
||||
}
|
||||
|
||||
_mentionsAttachment = (body) => {
|
||||
let cleaned = QuotedHTMLTransformer.removeQuotedHTML(body.toLowerCase().trim());
|
||||
const signatureIndex = cleaned.indexOf('<signature>');
|
||||
if (signatureIndex !== -1) {
|
||||
cleaned = cleaned.substr(0, signatureIndex - 1);
|
||||
undo = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const historyItem = this.undoManager.undo() || {};
|
||||
if (!historyItem.state) {
|
||||
return;
|
||||
}
|
||||
return (cleaned.indexOf("attach") >= 0);
|
||||
|
||||
this._recoveredSelection = historyItem.currentSelection;
|
||||
this._applyChanges(historyItem.state, {fromUndoManager: true});
|
||||
this._recoveredSelection = null;
|
||||
}
|
||||
|
||||
redo = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const historyItem = this.undoManager.redo() || {}
|
||||
if (!historyItem.state) {
|
||||
return;
|
||||
}
|
||||
this._recoveredSelection = historyItem.currentSelection;
|
||||
this._applyChanges(historyItem.state, {fromUndoManager: true});
|
||||
this._recoveredSelection = null;
|
||||
}
|
||||
|
||||
_getSelections = () => {
|
||||
const bodyComponent = this.refs[Fields.Body];
|
||||
return {
|
||||
currentSelection: bodyComponent.getCurrentSelection ? bodyComponent.getCurrentSelection() : null,
|
||||
previousSelection: bodyComponent.getPreviousSelection ? bodyComponent.getPreviousSelection() : null,
|
||||
}
|
||||
}
|
||||
|
||||
_saveToHistory = (selections) => {
|
||||
const {previousSelection, currentSelection} = selections || this._getSelections();
|
||||
|
||||
const historyItem = {
|
||||
previousSelection,
|
||||
currentSelection,
|
||||
state: {
|
||||
body: _.clone(this.props.draft.body),
|
||||
subject: _.clone(this.props.draft.subject),
|
||||
to: _.clone(this.props.draft.to),
|
||||
cc: _.clone(this.props.draft.cc),
|
||||
bcc: _.clone(this.props.draft.bcc),
|
||||
},
|
||||
}
|
||||
|
||||
const lastState = this.undoManager.current()
|
||||
if (lastState) {
|
||||
lastState.currentSelection = historyItem.previousSelection;
|
||||
}
|
||||
|
||||
this.undoManager.saveToHistory(historyItem);
|
||||
}
|
||||
|
||||
render() {
|
||||
const dropCoverDisplay = this.state.isDropping ? 'block' : 'none';
|
||||
|
|
|
@ -77,8 +77,8 @@ describe "ComposerView", ->
|
|||
@draft = draft
|
||||
spyOn(ContactStore, "searchContacts").andCallFake (email) =>
|
||||
return _.filter(users, (u) u.email.toLowerCase() is email.toLowerCase())
|
||||
spyOn(ContactStore, "isValidContact").andCallFake (contact) =>
|
||||
return contact.email.indexOf('@') > 0
|
||||
spyOn(Contact.prototype, "isValid").andCallFake (contact) ->
|
||||
return @email.indexOf('@') > 0
|
||||
|
||||
afterEach ->
|
||||
ComposerEditor.containerRequired = undefined
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
EmailFrame = require('./email-frame').default
|
||||
{Utils,
|
||||
{DraftHelpers,
|
||||
CanvasUtils,
|
||||
NylasAPI,
|
||||
MessageUtils,
|
||||
|
@ -21,7 +21,7 @@ class MessageItemBody extends React.Component
|
|||
constructor: (@props) ->
|
||||
@_unmounted = false
|
||||
@state =
|
||||
showQuotedText: Utils.isForwardedMessage(@props.message)
|
||||
showQuotedText: DraftHelpers.isForwardedMessage(@props.message)
|
||||
processedBody: null
|
||||
error: null
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
{Actions,
|
||||
Utils,
|
||||
{DraftHelpers,
|
||||
Actions,
|
||||
Thread,
|
||||
ChangeStarredTask,
|
||||
AccountStore} = require 'nylas-exports'
|
||||
|
@ -25,7 +25,7 @@ class ThreadListIcon extends React.Component
|
|||
last = msgs[msgs.length - 1]
|
||||
|
||||
if msgs.length > 1 and last.from[0]?.isMe()
|
||||
if Utils.isForwardedMessage(last)
|
||||
if DraftHelpers.isForwardedMessage(last)
|
||||
return 'thread-icon-forwarded thread-icon-star-on-hover'
|
||||
else
|
||||
return 'thread-icon-replied thread-icon-star-on-hover'
|
||||
|
|
|
@ -154,3 +154,24 @@ describe "Contact", ->
|
|||
spyOn(AccountStore, 'accountForEmail').andReturn(acct)
|
||||
expect(c1.isMe()).toBe(true)
|
||||
expect(AccountStore.accountForEmail).toHaveBeenCalled()
|
||||
|
||||
describe 'isValid', ->
|
||||
it "should return true for a variety of valid contacts", ->
|
||||
expect((new Contact(name: 'Ben', email: 'ben@nylas.com')).isValid()).toBe(true)
|
||||
expect((new Contact(email: 'ben@nylas.com')).isValid()).toBe(true)
|
||||
expect((new Contact(email: 'ben+123@nylas.com')).isValid()).toBe(true)
|
||||
|
||||
it "should return false if the contact has no email", ->
|
||||
expect((new Contact(name: 'Ben')).isValid()).toBe(false)
|
||||
|
||||
it "should return false if the contact has an email that is not valid", ->
|
||||
expect((new Contact(name: 'Ben', email:'Ben <ben@nylas.com>')).isValid()).toBe(false)
|
||||
expect((new Contact(name: 'Ben', email:'<ben@nylas.com>')).isValid()).toBe(false)
|
||||
expect((new Contact(name: 'Ben', email:'"ben@nylas.com"')).isValid()).toBe(false)
|
||||
|
||||
it "returns false if the email doesn't satisfy the regex", ->
|
||||
expect((new Contact(name: "test", email: "foo")).isValid()).toBe false
|
||||
|
||||
it "returns false if the email doesn't match", ->
|
||||
expect((new Contact(name: "test", email: "foo@")).isValid()).toBe false
|
||||
|
||||
|
|
|
@ -117,34 +117,17 @@ describe "ContactStore", ->
|
|||
expect(results.length).toBe 6
|
||||
|
||||
describe 'isValidContact', ->
|
||||
it "should return true for a variety of valid contacts", ->
|
||||
expect(ContactStore.isValidContact(new Contact(name: 'Ben', email: 'ben@nylas.com'))).toBe(true)
|
||||
expect(ContactStore.isValidContact(new Contact(email: 'ben@nylas.com'))).toBe(true)
|
||||
expect(ContactStore.isValidContact(new Contact(email: 'ben+123@nylas.com'))).toBe(true)
|
||||
it "should call contact.isValid", ->
|
||||
contact = new Contact()
|
||||
spyOn(contact, 'isValid').andReturn(true)
|
||||
expect(ContactStore.isValidContact(contact)).toBe(true)
|
||||
|
||||
it "should return false for non-Contact objects", ->
|
||||
expect(ContactStore.isValidContact({name: 'Ben', email: 'ben@nylas.com'})).toBe(false)
|
||||
|
||||
it "should return false if the contact has no email", ->
|
||||
expect(ContactStore.isValidContact(new Contact(name: 'Ben'))).toBe(false)
|
||||
|
||||
it "should return false if the contact has an email that is not valid", ->
|
||||
expect(ContactStore.isValidContact(new Contact(name: 'Ben', email:'Ben <ben@nylas.com>'))).toBe(false)
|
||||
expect(ContactStore.isValidContact(new Contact(name: 'Ben', email:'<ben@nylas.com>'))).toBe(false)
|
||||
expect(ContactStore.isValidContact(new Contact(name: 'Ben', email:'"ben@nylas.com"'))).toBe(false)
|
||||
|
||||
it "returns false if we're not passed a contact", ->
|
||||
expect(ContactStore.isValidContact()).toBe false
|
||||
|
||||
it "returns false if the contact doesn't have an email", ->
|
||||
expect(ContactStore.isValidContact(new Contact(name: "test"))).toBe false
|
||||
|
||||
it "returns false if the email doesn't satisfy the regex", ->
|
||||
expect(ContactStore.isValidContact(new Contact(name: "test", email: "foo"))).toBe false
|
||||
|
||||
it "returns false if the email doesn't match", ->
|
||||
expect(ContactStore.isValidContact(new Contact(name: "test", email: "foo@"))).toBe false
|
||||
|
||||
describe 'parseContactsInString', ->
|
||||
testCases =
|
||||
# Single contact test cases
|
||||
|
|
37
spec/stores/draft-helpers-spec.es6
Normal file
37
spec/stores/draft-helpers-spec.es6
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
Actions,
|
||||
Message,
|
||||
DraftHelpers,
|
||||
SyncbackDraftFilesTask,
|
||||
} from 'nylas-exports';
|
||||
|
||||
describe('DraftHelpers', function describeBlock() {
|
||||
describe('prepareForSyncback', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(DraftHelpers, 'applyExtensionTransformsToDraft').andCallFake((draft) => Promise.resolve(draft))
|
||||
spyOn(Actions, 'queueTask')
|
||||
});
|
||||
|
||||
it('queues tasks to upload files and send the draft', () => {
|
||||
const draft = new Message({
|
||||
clientId: "local-123",
|
||||
threadId: "thread-123",
|
||||
replyToMessageId: "message-123",
|
||||
uploads: ['stub'],
|
||||
});
|
||||
const session = {
|
||||
ensureCorrectAccount() { return Promise.resolve() },
|
||||
draft() { return draft },
|
||||
}
|
||||
runs(() => {
|
||||
DraftHelpers.prepareDraftForSyncback(session);
|
||||
});
|
||||
waitsFor(() => Actions.queueTask.calls.length > 0);
|
||||
runs(() => {
|
||||
const saveAttachments = Actions.queueTask.calls[0].args[0];
|
||||
expect(saveAttachments instanceof SyncbackDraftFilesTask).toBe(true);
|
||||
expect(saveAttachments.draftClientId).toBe(draft.clientId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,15 +5,14 @@ import {
|
|||
Message,
|
||||
Account,
|
||||
DraftStore,
|
||||
DraftHelpers,
|
||||
DatabaseStore,
|
||||
SoundRegistry,
|
||||
SendDraftTask,
|
||||
DestroyDraftTask,
|
||||
ComposerExtension,
|
||||
ExtensionRegistry,
|
||||
FocusedContentStore,
|
||||
DatabaseTransaction,
|
||||
SyncbackDraftFilesTask,
|
||||
} from 'nylas-exports';
|
||||
|
||||
import {remote} from 'electron';
|
||||
|
@ -323,7 +322,7 @@ describe('DraftStore', function draftStore() {
|
|||
|
||||
DraftStore._draftSessions[this.draft.clientId] = session;
|
||||
spyOn(DraftStore, "_doneWithSession").andCallThrough();
|
||||
spyOn(DraftStore, "_prepareForSyncback").andReturn(Promise.resolve());
|
||||
spyOn(DraftHelpers, "prepareDraftForSyncback").andReturn(Promise.resolve());
|
||||
spyOn(DraftStore, "trigger");
|
||||
spyOn(SoundRegistry, "playSound");
|
||||
spyOn(Actions, "queueTask");
|
||||
|
@ -408,25 +407,10 @@ describe('DraftStore', function draftStore() {
|
|||
});
|
||||
});
|
||||
|
||||
it("queues tasks to upload files and send the draft", () => {
|
||||
runs(() => {
|
||||
DraftStore._onSendDraft(this.draft.clientId);
|
||||
});
|
||||
waitsFor(() => Actions.queueTask.calls.length > 0);
|
||||
runs(() => {
|
||||
const saveAttachments = Actions.queueTask.calls[0].args[0];
|
||||
expect(saveAttachments instanceof SyncbackDraftFilesTask).toBe(true);
|
||||
expect(saveAttachments.draftClientId).toBe(this.draft.clientId);
|
||||
const sendDraft = Actions.queueTask.calls[1].args[0];
|
||||
expect(sendDraft instanceof SendDraftTask).toBe(true);
|
||||
expect(sendDraft.draftClientId).toBe(this.draft.clientId);
|
||||
});
|
||||
});
|
||||
|
||||
it("resets the sending state if there's an error", () => {
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn(false);
|
||||
DraftStore._draftsSending[this.draft.clientId] = true;
|
||||
Actions.draftSendingFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
Actions.sendDraftFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false);
|
||||
expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.clientId);
|
||||
});
|
||||
|
@ -437,7 +421,7 @@ describe('DraftStore', function draftStore() {
|
|||
spyOn(remote.dialog, "showMessageBox");
|
||||
spyOn(Actions, "composePopoutDraft");
|
||||
DraftStore._draftsSending[this.draft.clientId] = true;
|
||||
Actions.draftSendingFailed({threadId: 't1', errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
Actions.sendDraftFailed({threadId: 't1', errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
advanceClock(200);
|
||||
expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false);
|
||||
expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.clientId);
|
||||
|
@ -452,7 +436,7 @@ describe('DraftStore', function draftStore() {
|
|||
spyOn(FocusedContentStore, "focused").andReturn({id: "t1"});
|
||||
spyOn(Actions, "composePopoutDraft");
|
||||
DraftStore._draftsSending[this.draft.clientId] = true;
|
||||
Actions.draftSendingFailed({threadId: 't2', errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
Actions.sendDraftFailed({threadId: 't2', errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
advanceClock(200);
|
||||
expect(Actions.composePopoutDraft).toHaveBeenCalled();
|
||||
const call = Actions.composePopoutDraft.calls[0];
|
||||
|
@ -465,7 +449,7 @@ describe('DraftStore', function draftStore() {
|
|||
spyOn(Actions, "composePopoutDraft");
|
||||
DraftStore._draftsSending[this.draft.clientId] = true;
|
||||
spyOn(FocusedContentStore, "focused").andReturn(null);
|
||||
Actions.draftSendingFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
Actions.sendDraftFailed({errorMessage: "boohoo", draftClientId: this.draft.clientId});
|
||||
advanceClock(200);
|
||||
expect(Actions.composePopoutDraft).toHaveBeenCalled();
|
||||
const call = Actions.composePopoutDraft.calls[0];
|
||||
|
|
|
@ -222,7 +222,7 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
|
||||
describe("when there are errors", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Actions, 'draftSendingFailed');
|
||||
spyOn(Actions, 'sendDraftFailed');
|
||||
jasmine.unspy(NylasAPI, "makeRequest");
|
||||
});
|
||||
|
||||
|
@ -238,7 +238,7 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
waitsForPromise(() => this.task.performRemote().then((status) => {
|
||||
expect(status[0]).toBe(Task.Status.Failed);
|
||||
expect(status[1]).toBe(thrownError);
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled();
|
||||
expect(Actions.sendDraftFailed).toHaveBeenCalled();
|
||||
expect(NylasEnv.reportError).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
@ -300,7 +300,7 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
waitsForPromise(() => this.task.performRemote().then((status) => {
|
||||
expect(status[0]).toBe(Task.Status.Failed);
|
||||
expect(status[1]).toBe(thrownError);
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled();
|
||||
expect(Actions.sendDraftFailed).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -312,7 +312,7 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
waitsForPromise(() => this.task.performRemote().then((status) => {
|
||||
expect(status[0]).toBe(Task.Status.Failed);
|
||||
expect(status[1]).toBe(thrownError);
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled();
|
||||
expect(Actions.sendDraftFailed).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -341,9 +341,9 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
waitsForPromise(() => this.task.performRemote().then((status) => {
|
||||
expect(status[0]).toBe(Task.Status.Failed);
|
||||
expect(status[1]).toBe(thrownError);
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled();
|
||||
expect(Actions.sendDraftFailed).toHaveBeenCalled();
|
||||
|
||||
const msg = Actions.draftSendingFailed.calls[0].args[0].errorMessage;
|
||||
const msg = Actions.sendDraftFailed.calls[0].args[0].errorMessage;
|
||||
expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage));
|
||||
}));
|
||||
});
|
||||
|
@ -365,9 +365,9 @@ describe('SendDraftTask', function sendDraftTask() {
|
|||
waitsForPromise(() => this.task.performRemote().then((status) => {
|
||||
expect(status[0]).toBe(Task.Status.Failed);
|
||||
expect(status[1]).toBe(thrownError);
|
||||
expect(Actions.draftSendingFailed).toHaveBeenCalled();
|
||||
expect(Actions.sendDraftFailed).toHaveBeenCalled();
|
||||
|
||||
const msg = Actions.draftSendingFailed.calls[0].args[0].errorMessage;
|
||||
const msg = Actions.sendDraftFailed.calls[0].args[0].errorMessage;
|
||||
expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage));
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import {findDOMNode} from 'react-dom'
|
|||
|
||||
const MIN_RANGE_SIZE = 2
|
||||
|
||||
function getRange({total, itemHeight, containerHeight, scrollTop}) {
|
||||
function getRange({total, itemHeight, containerHeight, scrollTop = 0} = {}) {
|
||||
const itemsPerBody = Math.floor((containerHeight) / itemHeight);
|
||||
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - (itemsPerBody * 2));
|
||||
const end = Math.max(MIN_RANGE_SIZE, Math.min(start + (4 * itemsPerBody), total));
|
||||
|
@ -22,6 +22,7 @@ class LazyRenderedList extends Component {
|
|||
}
|
||||
|
||||
static defaultProps = {
|
||||
items: [],
|
||||
itemHeight: 30,
|
||||
containerHeight: 150,
|
||||
BufferTag: 'div',
|
||||
|
@ -29,7 +30,7 @@ class LazyRenderedList extends Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {start: 0, end: MIN_RANGE_SIZE}
|
||||
this.state = this.getRangeState(props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -40,9 +41,13 @@ class LazyRenderedList extends Component {
|
|||
this.updateRangeState(this.props)
|
||||
}
|
||||
|
||||
updateRangeState({itemHeight, items, containerHeight}) {
|
||||
getRangeState({items, itemHeight, containerHeight, scrollTop}) {
|
||||
return getRange({total: items.length, itemHeight, containerHeight, scrollTop})
|
||||
}
|
||||
|
||||
updateRangeState(props) {
|
||||
const {scrollTop} = findDOMNode(this)
|
||||
this.setState(getRange({total: items.length, itemHeight, containerHeight, scrollTop}))
|
||||
this.setState(this.getRangeState({...props, scrollTop}))
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
|
|
|
@ -121,6 +121,15 @@ export default class TableDataSource {
|
|||
return {...this._tableData}
|
||||
}
|
||||
|
||||
filterRows(filterFn) {
|
||||
const rows = this.rows()
|
||||
const nextRows = rows.filter(filterFn)
|
||||
return new TableDataSource({
|
||||
...this._tableData,
|
||||
rows: nextRows,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds column
|
||||
*
|
||||
|
|
|
@ -83,8 +83,8 @@ class Actions
|
|||
Recieves the clientId of the message that was sent
|
||||
###
|
||||
@sendDraftSuccess: ActionScopeGlobal
|
||||
@sendDraftFailed: ActionScopeGlobal
|
||||
@sendToAllWindows: ActionScopeGlobal
|
||||
@draftSendingFailed: ActionScopeGlobal
|
||||
|
||||
###
|
||||
Public: Queue a {Task} object to the {TaskQueue}.
|
||||
|
@ -367,7 +367,7 @@ class Actions
|
|||
```
|
||||
###
|
||||
@sendDraft: ActionScopeWindow
|
||||
@sendDrafts: ActionScopeWindow
|
||||
@sendManyDrafts: ActionScopeWindow
|
||||
@ensureDraftSynced: ActionScopeWindow
|
||||
|
||||
###
|
||||
|
|
|
@ -91,7 +91,17 @@ class Contact extends Model
|
|||
json['name'] ||= json['email']
|
||||
json
|
||||
|
||||
# Public: Returns true if the contact provided is a {Contact} instance and
|
||||
# contains a properly formatted email address.
|
||||
#
|
||||
isValid: ->
|
||||
return false unless @email
|
||||
|
||||
# The email regexp must match the /entire/ email address
|
||||
result = RegExpUtils.emailRegex().exec(@email)
|
||||
if result and result instanceof Array
|
||||
return result[0] is @email
|
||||
else return false
|
||||
@email.match(RegExpUtils.emailRegex()) != null
|
||||
|
||||
# Public: Returns true if the contact is the current user, false otherwise.
|
||||
|
|
|
@ -4,11 +4,12 @@ import moment from 'moment'
|
|||
import File from './file'
|
||||
import Utils from './utils'
|
||||
import Event from './event'
|
||||
import Category from './category'
|
||||
import Contact from './contact'
|
||||
import Category from './category'
|
||||
import Attributes from '../attributes'
|
||||
import ModelWithMetadata from './model-with-metadata'
|
||||
|
||||
|
||||
/**
|
||||
Public: The Message model represents a Message object served by the Nylas Platform API.
|
||||
For more information about Messages on the Nylas Platform, read the
|
||||
|
|
|
@ -223,25 +223,6 @@ Utils =
|
|||
else
|
||||
return "#{prefix} #{subject}"
|
||||
|
||||
# Returns true if the message contains "Forwarded" or "Fwd" in the first
|
||||
# 250 characters. A strong indicator that the quoted text should be
|
||||
# shown. Needs to be limited to first 250 to prevent replies to
|
||||
# forwarded messages from also being expanded.
|
||||
isForwardedMessage: ({body, subject} = {}) ->
|
||||
bodyForwarded = false
|
||||
bodyFwd = false
|
||||
subjectFwd = false
|
||||
|
||||
if body
|
||||
indexForwarded = body.search(/forwarded/i)
|
||||
bodyForwarded = indexForwarded >= 0 and indexForwarded < 250
|
||||
indexFwd = body.search(/fwd/i)
|
||||
bodyFwd = indexFwd >= 0 and indexFwd < 250
|
||||
if subject
|
||||
subjectFwd = subject[0...3].toLowerCase() is "fwd"
|
||||
|
||||
return bodyForwarded or bodyFwd or subjectFwd
|
||||
|
||||
# True of all arguments have the same domains
|
||||
emailsHaveSameDomain: (args...) ->
|
||||
return false if args.length < 2
|
||||
|
|
|
@ -80,18 +80,9 @@ class ContactStore extends NylasStore
|
|||
|
||||
return Promise.resolve(results)
|
||||
|
||||
# Public: Returns true if the contact provided is a {Contact} instance and
|
||||
# contains a properly formatted email address.
|
||||
#
|
||||
isValidContact: (contact) =>
|
||||
return false unless contact instanceof Contact
|
||||
return false unless contact.email
|
||||
|
||||
# The email regexp must match the /entire/ email address
|
||||
result = RegExpUtils.emailRegex().exec(contact.email)
|
||||
if result and result instanceof Array
|
||||
return result[0] is contact.email
|
||||
else return false
|
||||
return contact.isValid()
|
||||
|
||||
parseContactsInString: (contactString, options={}) =>
|
||||
{skipNameLookup} = options
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
Message = require('../models/message').default
|
||||
Actions = require '../actions'
|
||||
NylasAPI = require '../nylas-api'
|
||||
AccountStore = require './account-store'
|
||||
ContactStore = require './contact-store'
|
||||
DatabaseStore = require './database-store'
|
||||
UndoStack = require '../../undo-stack'
|
||||
ExtensionRegistry = require('../../extension-registry')
|
||||
DraftHelpers = require '../stores/draft-helpers'
|
||||
ExtensionRegistry = require '../../extension-registry'
|
||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
SyncbackDraftTask = require('../tasks/syncback-draft-task').default
|
||||
CoffeeHelpers = require '../coffee-helpers'
|
||||
DraftStore = null
|
||||
|
||||
_ = require 'underscore'
|
||||
|
||||
MetadataChangePrefix = 'metadata.'
|
||||
|
@ -143,6 +146,64 @@ class DraftEditingSession
|
|||
@changes.teardown()
|
||||
@_destroyed = true
|
||||
|
||||
validateDraftForSending: =>
|
||||
warnings = []
|
||||
errors = []
|
||||
allRecipients = [].concat(@_draft.to, @_draft.cc, @_draft.bcc)
|
||||
bodyIsEmpty = @_draft.body is @draftPristineBody()
|
||||
forwarded = DraftHelpers.isForwardedMessage(@_draft)
|
||||
hasAttachment = @_draft.files?.length > 0 or @_draft.uploads?.length > 0
|
||||
|
||||
for contact in allRecipients
|
||||
if not ContactStore.isValidContact(contact)
|
||||
errors.push("#{contact.email} is not a valid email address - please remove or edit it before sending.")
|
||||
|
||||
if allRecipients.length is 0
|
||||
errors.push('You need to provide one or more recipients before sending the message.')
|
||||
|
||||
if errors.length > 0
|
||||
return {errors, warnings}
|
||||
|
||||
if @_draft.subject.length is 0
|
||||
warnings.push('without a subject line')
|
||||
|
||||
if DraftHelpers.messageMentionsAttachment(@_draft) and not hasAttachment
|
||||
warnings.push('without an attachment')
|
||||
|
||||
if bodyIsEmpty and not forwarded and not hasAttachment
|
||||
warnings.push('without a body')
|
||||
|
||||
## Check third party warnings added via Composer extensions
|
||||
for extension in ExtensionRegistry.Composer.extensions()
|
||||
continue if not extension.warningsForSending
|
||||
warnings = warnings.concat(extension.warningsForSending({draft: @_draft}))
|
||||
|
||||
return {errors, warnings}
|
||||
|
||||
# This function makes sure the draft is attached to a valid account, and changes
|
||||
# it's accountId if the from address does not match the account for the from
|
||||
# address
|
||||
#
|
||||
# If the account is updated it makes a request to delete the draft with the
|
||||
# old accountId
|
||||
ensureCorrectAccount: ({noSyncback} = {}) =>
|
||||
account = AccountStore.accountForEmail(@_draft.from[0].email)
|
||||
if !account
|
||||
return Promise.reject(new Error("DraftEditingSession::ensureCorrectAccount - you can only send drafts from a configured account."))
|
||||
|
||||
if account.id isnt @_draft.accountId
|
||||
NylasAPI.makeDraftDeletionRequest(@_draft)
|
||||
@changes.add({
|
||||
accountId: account.id,
|
||||
version: null,
|
||||
serverId: null,
|
||||
threadId: null,
|
||||
replyToMessageId: null,
|
||||
})
|
||||
return @changes.commit({noSyncback})
|
||||
.thenReturn(@)
|
||||
return Promise.resolve(@)
|
||||
|
||||
_setDraft: (draft) ->
|
||||
if !draft.body?
|
||||
throw new Error("DraftEditingSession._setDraft - new draft has no body!")
|
||||
|
|
96
src/flux/stores/draft-helpers.es6
Normal file
96
src/flux/stores/draft-helpers.es6
Normal file
|
@ -0,0 +1,96 @@
|
|||
import _ from 'underscore'
|
||||
import Actions from '../actions'
|
||||
import DatabaseStore from './database-store'
|
||||
import * as ExtensionRegistry from '../../extension-registry'
|
||||
import SyncbackDraftFilesTask from '../tasks/syncback-draft-files-task'
|
||||
import QuotedHTMLTransformer from '../../services/quoted-html-transformer'
|
||||
|
||||
|
||||
export const AllowedTransformFields = ['to', 'from', 'cc', 'bcc', 'subject', 'body']
|
||||
|
||||
/**
|
||||
* Returns true if the message contains "Forwarded" or "Fwd" in the first
|
||||
* 250 characters. A strong indicator that the quoted text should be
|
||||
* shown. Needs to be limited to first 250 to prevent replies to
|
||||
* forwarded messages from also being expanded.
|
||||
*/
|
||||
export function isForwardedMessage({body, subject} = {}) {
|
||||
let bodyFwd = false
|
||||
let bodyForwarded = false
|
||||
let subjectFwd = false
|
||||
|
||||
if (body) {
|
||||
const indexFwd = body.search(/fwd/i)
|
||||
const indexForwarded = body.search(/forwarded/i)
|
||||
bodyForwarded = indexForwarded >= 0 && indexForwarded < 250
|
||||
bodyFwd = indexFwd >= 0 && indexFwd < 250
|
||||
}
|
||||
if (subject) {
|
||||
subjectFwd = subject.slice(0, 3).toLowerCase() === "fwd"
|
||||
}
|
||||
|
||||
return bodyForwarded || bodyFwd || subjectFwd
|
||||
}
|
||||
|
||||
export function messageMentionsAttachment({body} = {}) {
|
||||
if (body == null) { throw new Error('DraftHelpers::messageMentionsAttachment - Message has no body loaded') }
|
||||
let cleaned = QuotedHTMLTransformer.removeQuotedHTML(body.toLowerCase().trim());
|
||||
const signatureIndex = cleaned.indexOf('<signature>');
|
||||
if (signatureIndex !== -1) {
|
||||
cleaned = cleaned.substr(0, signatureIndex - 1);
|
||||
}
|
||||
return (cleaned.indexOf("attach") >= 0);
|
||||
}
|
||||
|
||||
export function queueDraftFileUploads(draft) {
|
||||
if (draft.files.length > 0 || draft.uploads.length > 0) {
|
||||
Actions.queueTask(new SyncbackDraftFilesTask(draft.clientId))
|
||||
}
|
||||
}
|
||||
|
||||
export function applyExtensionTransformsToDraft(draft) {
|
||||
let latestTransformed = draft
|
||||
const extensions = ExtensionRegistry.Composer.extensions()
|
||||
const transformPromise = (
|
||||
Promise.each(extensions, (ext) => {
|
||||
const extApply = ext.applyTransformsToDraft
|
||||
const extUnapply = ext.unapplyTransformsToDraft
|
||||
|
||||
if (!extApply || !extUnapply) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return Promise.resolve(extUnapply({draft: latestTransformed})).then((cleaned) => {
|
||||
const base = cleaned === 'unnecessary' ? latestTransformed : cleaned;
|
||||
return Promise.resolve(extApply({draft: base})).then((transformed) => (
|
||||
Promise.resolve(extUnapply({draft: transformed.clone()})).then((reverted) => {
|
||||
const untransformed = reverted === 'unnecessary' ? base : reverted;
|
||||
if (!_.isEqual(_.pick(untransformed, AllowedTransformFields), _.pick(base, AllowedTransformFields))) {
|
||||
console.log("-- BEFORE --")
|
||||
console.log(base.body)
|
||||
console.log("-- TRANSFORMED --")
|
||||
console.log(transformed.body)
|
||||
console.log("-- UNTRANSFORMED (should match BEFORE) --")
|
||||
console.log(untransformed.body)
|
||||
NylasEnv.reportError(new Error(`Extension ${ext.name} applied a transform to the draft that it could not reverse.`))
|
||||
}
|
||||
latestTransformed = transformed
|
||||
return Promise.resolve()
|
||||
})
|
||||
))
|
||||
})
|
||||
})
|
||||
)
|
||||
return transformPromise
|
||||
.thenReturn(latestTransformed)
|
||||
}
|
||||
|
||||
export function prepareDraftForSyncback(session) {
|
||||
return session.ensureCorrectAccount({noSyncback: true})
|
||||
.then(() => applyExtensionTransformsToDraft(session.draft()))
|
||||
.then((transformed) => (
|
||||
DatabaseStore.inTransaction((t) => t.persistModel(transformed))
|
||||
.then(() => Promise.resolve(queueDraftFileUploads(transformed)))
|
||||
.thenReturn(transformed)
|
||||
))
|
||||
}
|
|
@ -4,6 +4,7 @@ _ = require 'underscore'
|
|||
|
||||
NylasAPI = require '../nylas-api'
|
||||
DraftEditingSession = require './draft-editing-session'
|
||||
DraftHelpers = require './draft-helpers'
|
||||
DraftFactory = require './draft-factory'
|
||||
DatabaseStore = require './database-store'
|
||||
AccountStore = require './account-store'
|
||||
|
@ -17,7 +18,6 @@ SyncbackDraftTask = require('../tasks/syncback-draft-task').default
|
|||
DestroyDraftTask = require('../tasks/destroy-draft-task').default
|
||||
|
||||
Thread = require('../models/thread').default
|
||||
Contact = require '../models/contact'
|
||||
Message = require('../models/message').default
|
||||
Actions = require '../actions'
|
||||
|
||||
|
@ -55,7 +55,7 @@ class DraftStore
|
|||
@listenTo Actions.sendDraftSuccess, => @trigger()
|
||||
@listenTo Actions.composePopoutDraft, @_onPopoutDraftClientId
|
||||
@listenTo Actions.composeNewBlankDraft, @_onPopoutBlankDraft
|
||||
@listenTo Actions.draftSendingFailed, @_onDraftSendingFailed
|
||||
@listenTo Actions.sendDraftFailed, @_onSendDraftFailed
|
||||
@listenTo Actions.sendQuickReply, @_onSendQuickReply
|
||||
|
||||
if NylasEnv.isMainWindow()
|
||||
|
@ -66,7 +66,6 @@ class DraftStore
|
|||
# window.
|
||||
@listenTo Actions.ensureDraftSynced, @_onEnsureDraftSynced
|
||||
@listenTo Actions.sendDraft, @_onSendDraft
|
||||
@listenTo Actions.sendDrafts, @_onSendDrafts
|
||||
@listenTo Actions.destroyDraft, @_onDestroyDraft
|
||||
|
||||
@listenTo Actions.removeFile, @_onRemoveFile
|
||||
|
@ -335,92 +334,21 @@ class DraftStore
|
|||
|
||||
_onEnsureDraftSynced: (draftClientId) =>
|
||||
@sessionForClientId(draftClientId).then (session) =>
|
||||
@_prepareForSyncback(session).then =>
|
||||
@_queueDraftAssetTasks(session.draft())
|
||||
DraftHelpers.prepareDraftForSyncback(session)
|
||||
.then (preparedDraft) =>
|
||||
Actions.queueTask(new SyncbackDraftTask(draftClientId))
|
||||
|
||||
_onSendDraft: (draftClientId) =>
|
||||
@_sendDraft(draftClientId)
|
||||
.then =>
|
||||
if @_isPopout()
|
||||
NylasEnv.close()
|
||||
|
||||
_onSendDrafts: (draftClientIds) =>
|
||||
Promise.each(draftClientIds, (draftClientId) =>
|
||||
@_sendDraft(draftClientId)
|
||||
).then =>
|
||||
if @_isPopout()
|
||||
NylasEnv.close()
|
||||
|
||||
_sendDraft: (draftClientId) =>
|
||||
@_draftsSending[draftClientId] = true
|
||||
|
||||
@sessionForClientId(draftClientId).then (session) =>
|
||||
@_prepareForSyncback(session).then =>
|
||||
if NylasEnv.config.get("core.sending.sounds")
|
||||
SoundRegistry.playSound('hit-send')
|
||||
@_queueDraftAssetTasks(session.draft())
|
||||
DraftHelpers.prepareDraftForSyncback(session)
|
||||
.then (preparedDraft) =>
|
||||
Actions.queueTask(new SendDraftTask(draftClientId))
|
||||
@_doneWithSession(session)
|
||||
Promise.resolve()
|
||||
|
||||
_queueDraftAssetTasks: (draft) =>
|
||||
if draft.files.length > 0 or draft.uploads.length > 0
|
||||
Actions.queueTask(new SyncbackDraftFilesTask(draft.clientId))
|
||||
|
||||
_isPopout: ->
|
||||
NylasEnv.getWindowType() is "composer"
|
||||
|
||||
_prepareForSyncback: (session) =>
|
||||
draft = session.draft()
|
||||
|
||||
# Make sure the draft is attached to a valid account, and change it's
|
||||
# accountId if the from address does not match the current account.
|
||||
account = AccountStore.accountForEmail(draft.from[0].email)
|
||||
unless account
|
||||
return Promise.reject(new Error("DraftStore::_prepareForSyncback - you can only send drafts from a configured account."))
|
||||
|
||||
if account.id isnt draft.accountId
|
||||
NylasAPI.makeDraftDeletionRequest(draft)
|
||||
session.changes.add({
|
||||
accountId: account.id
|
||||
version: null
|
||||
serverId: null
|
||||
threadId: null
|
||||
replyToMessageId: null
|
||||
})
|
||||
|
||||
# Run draft transformations registered by third-party plugins
|
||||
allowedFields = ['to', 'from', 'cc', 'bcc', 'subject', 'body']
|
||||
|
||||
session.changes.commit(noSyncback: true).then =>
|
||||
draft = session.draft().clone()
|
||||
|
||||
Promise.each @extensions(), (ext) ->
|
||||
extApply = ext.applyTransformsToDraft
|
||||
extUnapply = ext.unapplyTransformsToDraft
|
||||
unless extApply and extUnapply
|
||||
return Promise.resolve()
|
||||
|
||||
Promise.resolve(extUnapply({draft})).then (cleaned) =>
|
||||
cleaned = draft if cleaned is 'unnecessary'
|
||||
Promise.resolve(extApply({draft: cleaned})).then (transformed) =>
|
||||
Promise.resolve(extUnapply({draft: transformed.clone()})).then (untransformed) =>
|
||||
untransformed = cleaned if untransformed is 'unnecessary'
|
||||
|
||||
if not _.isEqual(_.pick(untransformed, allowedFields), _.pick(cleaned, allowedFields))
|
||||
console.log("-- BEFORE --")
|
||||
console.log(draft.body)
|
||||
console.log("-- TRANSFORMED --")
|
||||
console.log(transformed.body)
|
||||
console.log("-- UNTRANSFORMED (should match BEFORE) --")
|
||||
console.log(untransformed.body)
|
||||
NylasEnv.reportError(new Error("Extension #{ext.name} applied a tranform to the draft that it could not reverse."))
|
||||
draft = transformed
|
||||
|
||||
.then =>
|
||||
DatabaseStore.inTransaction (t) =>
|
||||
t.persistModel(draft)
|
||||
if NylasEnv.config.get("core.sending.sounds")
|
||||
SoundRegistry.playSound('hit-send')
|
||||
if @_isPopout()
|
||||
NylasEnv.close()
|
||||
|
||||
__testExtensionTransforms: ->
|
||||
clientId = NylasEnv.getWindowProps().draftClientId
|
||||
|
@ -436,7 +364,7 @@ class DraftStore
|
|||
session.changes.add({files})
|
||||
session.changes.commit()
|
||||
|
||||
_onDraftSendingFailed: ({draftClientId, threadId, errorMessage}) ->
|
||||
_onSendDraftFailed: ({draftClientId, threadId, errorMessage}) ->
|
||||
@_draftsSending[draftClientId] = false
|
||||
@trigger(draftClientId)
|
||||
if NylasEnv.isMainWindow()
|
||||
|
@ -447,6 +375,9 @@ class DraftStore
|
|||
@_notifyUserOfError({draftClientId, threadId, errorMessage})
|
||||
, 100
|
||||
|
||||
_isPopout: ->
|
||||
NylasEnv.getWindowType() is "composer"
|
||||
|
||||
_notifyUserOfError: ({draftClientId, threadId, errorMessage}) ->
|
||||
focusedThread = FocusedContentStore.focused('thread')
|
||||
if threadId and focusedThread?.id is threadId
|
||||
|
|
|
@ -18,16 +18,16 @@ class TaskQueueStatusStore extends NylasStore
|
|||
query = DatabaseStore.findJSONBlob(TaskQueue.JSONBlobStorageKey)
|
||||
Rx.Observable.fromQuery(query).subscribe (json) =>
|
||||
@_queue = json || []
|
||||
@_waitingLocals = @_waitingLocals.filter ({taskId, resolve}) =>
|
||||
task = _.findWhere(@_queue, {id: taskId})
|
||||
if not task or task.queueState.localComplete
|
||||
resolve()
|
||||
@_waitingLocals = @_waitingLocals.filter ({task, resolve}) =>
|
||||
queuedTask = _.findWhere(@_queue, {id: task.id})
|
||||
if not queuedTask or queuedTask.queueState.localComplete
|
||||
resolve(task)
|
||||
return false
|
||||
return true
|
||||
@_waitingRemotes = @_waitingRemotes.filter ({taskId, resolve}) =>
|
||||
task = _.findWhere(@_queue, {id: taskId})
|
||||
if not task
|
||||
resolve()
|
||||
@_waitingRemotes = @_waitingRemotes.filter ({task, resolve}) =>
|
||||
queuedTask = _.findWhere(@_queue, {id: task.id})
|
||||
if not queuedTask
|
||||
resolve(task)
|
||||
return false
|
||||
return true
|
||||
@trigger()
|
||||
|
@ -37,11 +37,11 @@ class TaskQueueStatusStore extends NylasStore
|
|||
|
||||
waitForPerformLocal: (task) =>
|
||||
new Promise (resolve, reject) =>
|
||||
@_waitingLocals.push({taskId: task.id, resolve: resolve})
|
||||
@_waitingLocals.push({task, resolve})
|
||||
|
||||
waitForPerformRemote: (task) =>
|
||||
new Promise (resolve, reject) =>
|
||||
@_waitingRemotes.push({taskId: task.id, resolve: resolve})
|
||||
@_waitingRemotes.push({task, resolve})
|
||||
|
||||
tasksMatching: (type, matching = {}) ->
|
||||
type = type.name unless _.isString(type)
|
||||
|
|
|
@ -24,11 +24,12 @@ try {
|
|||
|
||||
export default class SendDraftTask extends BaseDraftTask {
|
||||
|
||||
constructor(draftClientId) {
|
||||
constructor(draftClientId, {playSound = true, emitError = true} = {}) {
|
||||
super(draftClientId);
|
||||
this.uploaded = [];
|
||||
this.draft = null;
|
||||
this.message = null;
|
||||
this.emitError = emitError
|
||||
this.playSound = playSound
|
||||
}
|
||||
|
||||
label() {
|
||||
|
@ -183,7 +184,7 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
NylasAPI.makeDraftDeletionRequest(this.draft);
|
||||
|
||||
// Play the sending sound
|
||||
if (NylasEnv.config.get("core.sending.sounds")) {
|
||||
if (this.playSound && NylasEnv.config.get("core.sending.sounds")) {
|
||||
SoundRegistry.playSound('send');
|
||||
}
|
||||
|
||||
|
@ -215,11 +216,13 @@ export default class SendDraftTask extends BaseDraftTask {
|
|||
}
|
||||
}
|
||||
|
||||
Actions.draftSendingFailed({
|
||||
threadId: this.draft.threadId,
|
||||
draftClientId: this.draft.clientId,
|
||||
errorMessage: message,
|
||||
});
|
||||
if (this.emitError) {
|
||||
Actions.sendDraftFailed({
|
||||
threadId: this.draft.threadId,
|
||||
draftClientId: this.draft.clientId,
|
||||
errorMessage: message,
|
||||
});
|
||||
}
|
||||
NylasEnv.reportError(err);
|
||||
|
||||
return Promise.resolve([Task.Status.Failed, err]);
|
||||
|
|
|
@ -14,10 +14,14 @@ export default class SyncbackMetadataTask extends SyncbackModelTask {
|
|||
}
|
||||
|
||||
getRequestData = (model) => {
|
||||
if (!model.serverId) {
|
||||
throw new Error(`Can't syncback metadata for a ${this.modelClassName} instance that doesn't have a serverId`)
|
||||
}
|
||||
|
||||
const metadata = model.metadataObjectForPluginId(this.pluginId);
|
||||
|
||||
return {
|
||||
path: `/metadata/${model.id}?client_id=${this.pluginId}`,
|
||||
path: `/metadata/${model.serverId}?client_id=${this.pluginId}`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
object_id: model.serverId,
|
||||
|
|
|
@ -49,12 +49,15 @@ export default class SyncbackModelTask extends Task {
|
|||
};
|
||||
|
||||
makeRequest = (model) => {
|
||||
const options = _.extend({
|
||||
accountId: model.accountId,
|
||||
returnsModel: false,
|
||||
}, this.getRequestData(model));
|
||||
|
||||
return NylasAPI.makeRequest(options);
|
||||
try {
|
||||
const options = _.extend({
|
||||
accountId: model.accountId,
|
||||
returnsModel: false,
|
||||
}, this.getRequestData(model));
|
||||
return NylasAPI.makeRequest(options);
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
};
|
||||
|
||||
getRequestData = (model) => {
|
||||
|
|
|
@ -89,6 +89,7 @@ class NylasExports
|
|||
@lazyLoad "Task", 'flux/tasks/task'
|
||||
@lazyLoad "TaskFactory", 'flux/tasks/task-factory'
|
||||
@lazyLoadAndRegisterTask "EventRSVPTask", 'event-rsvp-task'
|
||||
@lazyLoadAndRegisterTask "BaseDraftTask", 'base-draft-task'
|
||||
@lazyLoadAndRegisterTask "SendDraftTask", 'send-draft-task'
|
||||
@lazyLoadAndRegisterTask "MultiSendToIndividualTask", 'multi-send-to-individual-task'
|
||||
@lazyLoadAndRegisterTask "MultiSendSessionCloseTask", 'multi-send-session-close-task'
|
||||
|
@ -166,10 +167,11 @@ class NylasExports
|
|||
@lazyLoad "CanvasUtils", 'canvas-utils'
|
||||
@lazyLoad "RegExpUtils", 'regexp-utils'
|
||||
@lazyLoad "MenuHelpers", 'menu-helpers'
|
||||
@lazyLoad "MessageUtils", 'flux/models/message-utils'
|
||||
@lazyLoad "DeprecateUtils", 'deprecate-utils'
|
||||
@lazyLoad "VirtualDOMUtils", 'virtual-dom-utils'
|
||||
@lazyLoad "NylasSpellchecker", 'nylas-spellchecker'
|
||||
@lazyLoad "DraftHelpers", 'flux/stores/draft-helpers'
|
||||
@lazyLoad "MessageUtils", 'flux/models/message-utils'
|
||||
@lazyLoad "EditorAPI", 'components/contenteditable/editor-api'
|
||||
|
||||
# Services
|
||||
|
|
2
src/pro
2
src/pro
|
@ -1 +1 @@
|
|||
Subproject commit cfd64332c29ecfe965e2b556c77ba10c2a88dca5
|
||||
Subproject commit dc09d78e0bad22597b5bba6511091102e26bbaef
|
Loading…
Add table
Reference in a new issue