refactor(scheduler): move all event data into metadata
Summary: Moved events into metadata. Removed a lot of code Test Plan: todo Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2866
|
@ -48,7 +48,7 @@ We're working on building a plugin index that makes it super easy to add them to
|
||||||
#### Bundled Plugins
|
#### Bundled Plugins
|
||||||
Great starting points for creating your own plugins!
|
Great starting points for creating your own plugins!
|
||||||
- [Translate](https://github.com/nylas/N1/tree/master/internal_packages/composer-translate) — Works with 10 languages
|
- [Translate](https://github.com/nylas/N1/tree/master/internal_packages/composer-translate) — Works with 10 languages
|
||||||
- [Scheduler](https://github.com/nylas/N1/tree/master/internal_packages/N1-Scheduler) — Show your availability to schedule a meeting with someone
|
- [Scheduler](https://github.com/nylas/N1/tree/master/internal_packages/composer-scheduler) — Show your availability to schedule a meeting with someone
|
||||||
- [Quick Replies](https://github.com/nylas/N1/tree/master/internal_packages/composer-templates) — Send emails faster with templates
|
- [Quick Replies](https://github.com/nylas/N1/tree/master/internal_packages/composer-templates) — Send emails faster with templates
|
||||||
- [Send Later](https://github.com/nylas/N1/tree/master/internal_packages/send-later) — Schedule your emails to be sent at a later time
|
- [Send Later](https://github.com/nylas/N1/tree/master/internal_packages/send-later) — Schedule your emails to be sent at a later time
|
||||||
- [Open Tracking](https://github.com/nylas/N1/tree/master/internal_packages/open-tracking) — See if your emails have been read
|
- [Open Tracking](https://github.com/nylas/N1/tree/master/internal_packages/open-tracking) — See if your emails have been read
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
import NylasStore from 'nylas-store'
|
|
||||||
import SchedulerActions from './scheduler-actions'
|
|
||||||
import {Event, Message, Actions, DraftStore, DatabaseStore} from 'nylas-exports'
|
|
||||||
import {PLUGIN_ID} from './scheduler-constants'
|
|
||||||
|
|
||||||
// moment-round upon require patches `moment` with new functions.
|
|
||||||
require('moment-round')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maintains the creation of "Proposed Times" when scheduling with people.
|
|
||||||
*
|
|
||||||
* The proposed times are displayed in various calendar views.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class ProposedTimeMainWindowStore extends NylasStore {
|
|
||||||
activate() {
|
|
||||||
this.unsubscribers = [
|
|
||||||
SchedulerActions.confirmChoices.listen(this._onConfirmChoices),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivate() {
|
|
||||||
this.unsubscribers.forEach(unsub => unsub())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This removes the metadata on the draft and creates an `Event` on
|
|
||||||
* `draft.events`
|
|
||||||
*/
|
|
||||||
_convertToDraftEvent(draft) {
|
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
|
||||||
return DraftStore.sessionForClientId(draft.clientId).then((session) => {
|
|
||||||
if (metadata.pendingEvent) {
|
|
||||||
const event = new Event().fromJSON(metadata.pendingEvent);
|
|
||||||
session.changes.add({events: [event]});
|
|
||||||
} else {
|
|
||||||
session.changes.add({events: []})
|
|
||||||
}
|
|
||||||
|
|
||||||
delete metadata.uid
|
|
||||||
delete metadata.proposals
|
|
||||||
delete metadata.pendingEvent
|
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
|
||||||
|
|
||||||
return session.changes.commit()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_convertToPendingEvent(draft, proposals) {
|
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
|
||||||
metadata.proposals = proposals;
|
|
||||||
|
|
||||||
// This is used to so the backend can reference which draft
|
|
||||||
// corresponds to which sent message. The backend uses the key `uid`
|
|
||||||
metadata.uid = draft.clientId;
|
|
||||||
|
|
||||||
if (draft.events.length > 0) {
|
|
||||||
return DraftStore.sessionForClientId(draft.clientId).then((session) => {
|
|
||||||
metadata.pendingEvent = draft.events[0].toJSON();
|
|
||||||
session.changes.add({events: []});
|
|
||||||
return session.changes.commit().then(() => {
|
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will bundle up and attach the choices as metadata on the draft.
|
|
||||||
*
|
|
||||||
* Once we attach metadata to the draft, we need to make sure we clear
|
|
||||||
* the start and end times of the event.
|
|
||||||
*/
|
|
||||||
_onConfirmChoices = ({proposals, draftClientId}) => {
|
|
||||||
this._pendingSave = true;
|
|
||||||
this.trigger();
|
|
||||||
|
|
||||||
DatabaseStore.find(Message, draftClientId).then((draft) => {
|
|
||||||
if (proposals.length === 0) {
|
|
||||||
return this._convertToDraftEvent(draft)
|
|
||||||
}
|
|
||||||
return this._convertToPendingEvent(draft, proposals);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ProposedTimeMainWindowStore()
|
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 230 B |
Before Width: | Height: | Size: 788 B After Width: | Height: | Size: 788 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 511 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 779 B After Width: | Height: | Size: 779 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -2,8 +2,6 @@ import React, {Component, PropTypes} from 'react';
|
||||||
import NewEventCard from './new-event-card'
|
import NewEventCard from './new-event-card'
|
||||||
import {PLUGIN_ID} from '../scheduler-constants'
|
import {PLUGIN_ID} from '../scheduler-constants'
|
||||||
import {Utils, Event, Actions, DraftStore} from 'nylas-exports';
|
import {Utils, Event, Actions, DraftStore} from 'nylas-exports';
|
||||||
const MEETING_REQUEST = "MEETING_REQUEST"
|
|
||||||
const PENDING_EVENT = "PENDING_EVENT"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When you're creating an event you can either be creating:
|
* When you're creating an event you can either be creating:
|
||||||
|
@ -11,22 +9,17 @@ const PENDING_EVENT = "PENDING_EVENT"
|
||||||
* 1. A Meeting Request with a specific start and end time
|
* 1. A Meeting Request with a specific start and end time
|
||||||
* 2. OR a `pendingEvent` template that has a set of proposed times.
|
* 2. OR a `pendingEvent` template that has a set of proposed times.
|
||||||
*
|
*
|
||||||
* The former (1) is represented by an `Event` object on the `draft.events`
|
* Both are represented by a `pendingEvent` object on the `metadata` that
|
||||||
* field of a draft.
|
* holds the JSONified representation of the `Event`
|
||||||
*
|
*
|
||||||
* The latter (2) is represented by a `pendingEvent` key on the metadata
|
* #2 adds a set of `proposals` on the metadata object.
|
||||||
* of the `draft`.
|
|
||||||
*
|
|
||||||
* These are mutually exclusive and shouldn't exist at the same time on a
|
|
||||||
* draft.
|
|
||||||
*/
|
*/
|
||||||
export default class NewEventCardContainer extends Component {
|
export default class NewEventCardContainer extends Component {
|
||||||
static displayName = 'NewEventCardContainer';
|
static displayName = 'NewEventCardContainer';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
draftClientId: PropTypes.string,
|
draftClientId: PropTypes.string,
|
||||||
threadId: PropTypes.string,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -36,7 +29,7 @@ export default class NewEventCardContainer extends Component {
|
||||||
this._usub = () => {}
|
this._usub = () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentWillMount() {
|
||||||
this._mounted = true;
|
this._mounted = true;
|
||||||
this._loadDraft(this.props.draftClientId);
|
this._loadDraft(this.props.draftClientId);
|
||||||
}
|
}
|
||||||
|
@ -63,73 +56,30 @@ export default class NewEventCardContainer extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_eventType(draft) {
|
_onDraftChange = () => {
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID);
|
this.setState({event: this._getEvent()});
|
||||||
const hasPendingEvent = metadata && metadata.pendingEvent
|
}
|
||||||
if (draft.events && draft.events.length > 0) {
|
|
||||||
if (hasPendingEvent) {
|
_getEvent() {
|
||||||
throw new Error(`Assertion Failure. Can't have both a pendingEvent \
|
const metadata = this._session.draft().metadataForPluginId(PLUGIN_ID);
|
||||||
and an event on a draft at the same time!`);
|
if (metadata && metadata.pendingEvent) {
|
||||||
}
|
return new Event().fromJSON(metadata.pendingEvent || {})
|
||||||
return MEETING_REQUEST
|
|
||||||
} else if (hasPendingEvent) {
|
|
||||||
return PENDING_EVENT
|
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDraftChange = () => {
|
_updateEvent = (newData) => {
|
||||||
const draft = this._session.draft();
|
|
||||||
|
|
||||||
let event = null;
|
|
||||||
const eventType = this._eventType(draft)
|
|
||||||
|
|
||||||
if (eventType === MEETING_REQUEST) {
|
|
||||||
event = draft.events[0]
|
|
||||||
} else if (eventType === PENDING_EVENT) {
|
|
||||||
event = this._getPendingEvent(draft.metadataForPluginId(PLUGIN_ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({event});
|
|
||||||
}
|
|
||||||
|
|
||||||
_getPendingEvent(metadata) {
|
|
||||||
return new Event().fromJSON(metadata.pendingEvent || {})
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateDraftEvent(newData) {
|
|
||||||
const draft = this._session.draft();
|
|
||||||
const data = newData
|
|
||||||
const event = Object.assign(draft.events[0].clone(), data);
|
|
||||||
if (!Utils.isEqual(event, draft.events[0])) {
|
|
||||||
this._session.changes.add({events: [event]}); // triggers draft change
|
|
||||||
this._session.changes.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updatePendingEvent(newData) {
|
|
||||||
const draft = this._session.draft()
|
const draft = this._session.draft()
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID)
|
const metadata = draft.metadataForPluginId(PLUGIN_ID)
|
||||||
const pendingEvent = Object.assign(this._getPendingEvent(metadata).clone(), newData)
|
const newEvent = Object.assign(this._getEvent().clone(), newData)
|
||||||
const pendingEventJSON = pendingEvent.toJSON()
|
const pendingEventJSON = newEvent.toJSON()
|
||||||
if (!Utils.isEqual(pendingEventJSON, metadata.pendingEvent)) {
|
if (!Utils.isEqual(pendingEventJSON, metadata.pendingEvent)) {
|
||||||
metadata.pendingEvent = pendingEventJSON;
|
metadata.pendingEvent = pendingEventJSON;
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onEventChange = (newData) => {
|
_removeEvent = () => {
|
||||||
const eventType = this._eventType(this._session.draft());
|
|
||||||
if (eventType === MEETING_REQUEST) {
|
|
||||||
this._updateDraftEvent(newData)
|
|
||||||
} else if (eventType === PENDING_EVENT) {
|
|
||||||
this._updatePendingEvent(newData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onEventRemove = () => {
|
|
||||||
this._session.changes.add({events: []});
|
|
||||||
this._session.changes.commit();
|
|
||||||
const draft = this._session.draft()
|
const draft = this._session.draft()
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID);
|
const metadata = draft.metadataForPluginId(PLUGIN_ID);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
|
@ -145,8 +95,8 @@ and an event on a draft at the same time!`);
|
||||||
card = (
|
card = (
|
||||||
<NewEventCard event={this.state.event}
|
<NewEventCard event={this.state.event}
|
||||||
draft={this._session.draft()}
|
draft={this._session.draft()}
|
||||||
onRemove={this._onEventRemove}
|
onRemove={this._removeEvent}
|
||||||
onChange={this._onEventChange}
|
onChange={this._updateEvent}
|
||||||
onParticipantsClick={() => {}}
|
onParticipantsClick={() => {}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
|
@ -159,7 +159,12 @@ export default class NewEventCard extends React.Component {
|
||||||
_renderTimePicker() {
|
_renderTimePicker() {
|
||||||
const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);
|
const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);
|
||||||
if (metadata && metadata.proposals) {
|
if (metadata && metadata.proposals) {
|
||||||
return <ProposedTimeList event={this.props.event} proposals={metadata.proposals} />
|
return (
|
||||||
|
<ProposedTimeList event={this.props.event}
|
||||||
|
draft={this.props.draft}
|
||||||
|
proposals={metadata.proposals}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startVal = (this.props.event.start) * 1000;
|
const startVal = (this.props.event.start) * 1000;
|
|
@ -11,6 +11,7 @@ const TZ = moment.tz(Utils.timeZone).format("z");
|
||||||
export default class ProposedTimeList extends React.Component {
|
export default class ProposedTimeList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
draft: React.PropTypes.object,
|
draft: React.PropTypes.object,
|
||||||
|
event: React.PropTypes.object,
|
||||||
inEmail: React.PropTypes.bool,
|
inEmail: React.PropTypes.bool,
|
||||||
proposals: React.PropTypes.array.isRequired,
|
proposals: React.PropTypes.array.isRequired,
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@ export default class ProposedTimeList extends React.Component {
|
||||||
<div>
|
<div>
|
||||||
<h2 style={styles}>
|
<h2 style={styles}>
|
||||||
{this._renderB64Img("description")}
|
{this._renderB64Img("description")}
|
||||||
{((this.props.draft.events || [])[0] || {}).title || this.props.draft.subject}
|
{this.props.event.title || this.props.draft.subject}
|
||||||
</h2>
|
</h2>
|
||||||
<span style={{margin: "0 10px"}}>
|
<span style={{margin: "0 10px"}}>
|
||||||
{this._renderB64Img("time")}
|
{this._renderB64Img("time")}
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
|
Actions,
|
||||||
Calendar,
|
Calendar,
|
||||||
APIError,
|
APIError,
|
||||||
NylasAPI,
|
NylasAPI,
|
||||||
|
@ -54,61 +55,59 @@ export default class SchedulerComposerButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDraftChange() {
|
_onDraftChange() {
|
||||||
const draft = this._session.draft();
|
this.setState({enabled: this._hasPendingEvent()});
|
||||||
this.setState({
|
}
|
||||||
enabled: draft.events && draft.events.length > 0,
|
|
||||||
});
|
_hasPendingEvent() {
|
||||||
|
const metadata = this._session.draft().metadataForPluginId(PLUGIN_ID);
|
||||||
|
return metadata && metadata.pendingEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClick = () => {
|
_onClick = () => {
|
||||||
if (!this._session) { return }
|
if (!this._session) { return }
|
||||||
const draft = this._session.draft()
|
const draft = this._session.draft()
|
||||||
if (draft.events.length === 0) { // API can only handle one event
|
NylasAPI.authPlugin(PLUGIN_ID, PLUGIN_NAME, draft.accountId)
|
||||||
NylasAPI.authPlugin(PLUGIN_ID, PLUGIN_NAME, draft.accountId)
|
.then(() => {
|
||||||
.then(() => {
|
DatabaseStore.findAll(Calendar, {accountId: draft.accountId})
|
||||||
DatabaseStore.findAll(Calendar, {accountId: draft.accountId})
|
.then((allCalendars) => {
|
||||||
.then((allCalendars) => {
|
if (allCalendars.length === 0) {
|
||||||
if (allCalendars.length === 0) {
|
throw new Error(`Can't create an event. The Account \
|
||||||
throw new Error(`Can't create an event. The Account \
|
${draft.accountId} has no calendars.`);
|
||||||
${draft.accountId} has no calendars.`);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const cals = allCalendars.filter(c => !c.readOnly);
|
const cals = allCalendars.filter(c => !c.readOnly);
|
||||||
|
|
||||||
if (cals.length === 0) {
|
if (cals.length === 0) {
|
||||||
NylasEnv.showErrorDialog(`This account has no editable \
|
NylasEnv.showErrorDialog(`This account has no editable \
|
||||||
calendars. We can't create an event for you. Please make sure you have an \
|
calendars. We can't create an event for you. Please make sure you have an \
|
||||||
editable calendar with your account provider.`);
|
editable calendar with your account provider.`);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const start = moment().ceil(30, 'minutes');
|
|
||||||
const end = moment(start).add(1, 'hour');
|
|
||||||
|
|
||||||
// TODO Have a default calendar config
|
|
||||||
const event = new Event({
|
|
||||||
end: end.unix(),
|
|
||||||
start: start.unix(),
|
|
||||||
calendarId: cals[0].id,
|
|
||||||
});
|
|
||||||
this._session.changes.add({events: [event]});
|
|
||||||
this._session.changes.commit()
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
let title = "Error"
|
|
||||||
let msg = `Unfortunately scheduling is not currently available. \
|
|
||||||
Please try again later.\n\nError: ${error}`
|
|
||||||
if (!(error instanceof APIError)) {
|
|
||||||
NylasEnv.reportError(error);
|
|
||||||
} else if (error.statusCode === 400) {
|
|
||||||
NylasEnv.reportError(error);
|
|
||||||
} else if (NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {
|
|
||||||
title = "Offline"
|
|
||||||
msg = `Scheduling does not work offline. Please try again when you come back online.`
|
|
||||||
}
|
}
|
||||||
NylasEnv.showErrorDialog({title, message: msg});
|
|
||||||
|
const start = moment().ceil(30, 'minutes');
|
||||||
|
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
||||||
|
metadata.uid = draft.clientId;
|
||||||
|
metadata.pendingEvent = new Event({
|
||||||
|
calendarId: cals[0].id,
|
||||||
|
start: start.unix(),
|
||||||
|
end: moment(start).add(1, 'hour').unix(),
|
||||||
|
}).toJSON();
|
||||||
|
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
||||||
})
|
})
|
||||||
}
|
}).catch((error) => {
|
||||||
|
let title = "Error"
|
||||||
|
let msg = `Unfortunately scheduling is not currently available. \
|
||||||
|
Please try again later.\n\nError: ${error}`
|
||||||
|
if (!(error instanceof APIError)) {
|
||||||
|
NylasEnv.reportError(error);
|
||||||
|
} else if (error.statusCode === 400) {
|
||||||
|
NylasEnv.reportError(error);
|
||||||
|
} else if (NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {
|
||||||
|
title = "Offline"
|
||||||
|
msg = `Scheduling does not work offline. Please try again when you come back online.`
|
||||||
|
}
|
||||||
|
NylasEnv.showErrorDialog({title, message: msg});
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -117,7 +116,7 @@ Please try again later.\n\nError: ${error}`
|
||||||
onClick={this._onClick}
|
onClick={this._onClick}
|
||||||
title="Add an event…"
|
title="Add an event…"
|
||||||
>
|
>
|
||||||
<RetinaImg url="nylas://N1-Scheduler/assets/ic-composer-scheduler@2x.png"
|
<RetinaImg url="nylas://composer-scheduler/assets/ic-composer-scheduler@2x.png"
|
||||||
mode={RetinaImg.Mode.ContentIsMask}
|
mode={RetinaImg.Mode.ContentIsMask}
|
||||||
/>
|
/>
|
||||||
</button>)
|
</button>)
|
|
@ -1,8 +1,7 @@
|
||||||
import _ from 'underscore'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {PLUGIN_ID} from '../scheduler-constants'
|
import {PLUGIN_ID} from '../scheduler-constants'
|
||||||
import ProposedTimeList from './proposed-time-list'
|
import ProposedTimeList from './proposed-time-list'
|
||||||
import {Actions, RegExpUtils, ComposerExtension} from 'nylas-exports'
|
import {Event, Actions, RegExpUtils, ComposerExtension} from 'nylas-exports'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts the set of Proposed Times into the body of the HTML email.
|
* Inserts the set of Proposed Times into the body of the HTML email.
|
||||||
|
@ -45,7 +44,7 @@ export default class SchedulerComposerExtension extends ComposerExtension {
|
||||||
return contentBefore + wrapS + markup + wrapE + contentAfter
|
return contentBefore + wrapS + markup + wrapE + contentAfter
|
||||||
}
|
}
|
||||||
|
|
||||||
static _prepareEvent(inEvent, draft) {
|
static _prepareEvent(inEvent, draft, metadata) {
|
||||||
const event = inEvent
|
const event = inEvent
|
||||||
if (!event.title || event.title.length === 0) {
|
if (!event.title || event.title.length === 0) {
|
||||||
event.title = draft.subject;
|
event.title = draft.subject;
|
||||||
|
@ -58,15 +57,37 @@ export default class SchedulerComposerExtension extends ComposerExtension {
|
||||||
status: "noreply",
|
status: "noreply",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (metadata.proposals) {
|
||||||
|
event.end = null
|
||||||
|
event.start = null
|
||||||
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We must set the `preparedEvent` to be exactly what could be posted to
|
||||||
|
// the /events endpoint of the API.
|
||||||
|
static _cleanEventJSON(rawJSON) {
|
||||||
|
const json = rawJSON;
|
||||||
|
delete json.client_id;
|
||||||
|
delete json.id;
|
||||||
|
json.when = {
|
||||||
|
object: "timespan",
|
||||||
|
start: json._start,
|
||||||
|
end: json._end,
|
||||||
|
}
|
||||||
|
delete json._start
|
||||||
|
delete json._end
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
static _insertProposalsIntoBody(draft, metadata) {
|
static _insertProposalsIntoBody(draft, metadata) {
|
||||||
const nextDraft = draft;
|
const nextDraft = draft;
|
||||||
if (metadata && metadata.proposals) {
|
if (metadata && metadata.proposals) {
|
||||||
const el = React.createElement(ProposedTimeList,
|
const el = React.createElement(ProposedTimeList,
|
||||||
{
|
{
|
||||||
draft: nextDraft,
|
draft: nextDraft,
|
||||||
|
event: metadata.pendingEvent,
|
||||||
inEmail: true,
|
inEmail: true,
|
||||||
proposals: metadata.proposals,
|
proposals: metadata.proposals,
|
||||||
});
|
});
|
||||||
|
@ -81,18 +102,11 @@ export default class SchedulerComposerExtension extends ComposerExtension {
|
||||||
const self = SchedulerComposerExtension
|
const self = SchedulerComposerExtension
|
||||||
let nextDraft = draft.clone();
|
let nextDraft = draft.clone();
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID)
|
const metadata = draft.metadataForPluginId(PLUGIN_ID)
|
||||||
|
if (metadata && metadata.pendingEvent) {
|
||||||
if (nextDraft.events.length > 0) {
|
nextDraft = self._insertProposalsIntoBody(nextDraft, metadata);
|
||||||
if (metadata && metadata.pendingEvent) {
|
const nextEvent = new Event().fromJSON(metadata.pendingEvent);
|
||||||
throw new Error(`Assertion Failure. Can't have both a pendingEvent \
|
const nextEventPrepared = self._prepareEvent(nextEvent, draft, metadata);
|
||||||
and an event on a draft at the same time!`);
|
metadata.pendingEvent = self._cleanEventJSON(nextEventPrepared.toJSON());
|
||||||
}
|
|
||||||
const event = self._prepareEvent(nextDraft.events[0].clone(), draft)
|
|
||||||
nextDraft.events = [event]
|
|
||||||
} else if (metadata && metadata.pendingEvent) {
|
|
||||||
nextDraft = self._insertProposalsIntoBody(nextDraft, metadata)
|
|
||||||
const event = self._prepareEvent(_.clone(metadata.pendingEvent), draft);
|
|
||||||
metadata.pendingEvent = event;
|
|
||||||
Actions.setMetadata(nextDraft, PLUGIN_ID, metadata);
|
Actions.setMetadata(nextDraft, PLUGIN_ID, metadata);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,10 @@ import ProposedTimeCalendarStore from './proposed-time-calendar-store'
|
||||||
import ProposedTimeMainWindowStore from './proposed-time-main-window-store'
|
import ProposedTimeMainWindowStore from './proposed-time-main-window-store'
|
||||||
import SchedulerComposerExtension from './composer/scheduler-composer-extension';
|
import SchedulerComposerExtension from './composer/scheduler-composer-extension';
|
||||||
|
|
||||||
import {PLUGIN_ID, PLUGIN_URL} from './scheduler-constants'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Actions,
|
|
||||||
WorkspaceStore,
|
WorkspaceStore,
|
||||||
ComponentRegistry,
|
ComponentRegistry,
|
||||||
ExtensionRegistry,
|
ExtensionRegistry,
|
||||||
RegisterDraftForPluginTask,
|
|
||||||
} from 'nylas-exports'
|
} from 'nylas-exports'
|
||||||
|
|
||||||
export function activate() {
|
export function activate() {
|
||||||
|
@ -40,22 +36,6 @@ export function activate() {
|
||||||
{role: 'Composer:ActionButton'});
|
{role: 'Composer:ActionButton'});
|
||||||
|
|
||||||
ExtensionRegistry.Composer.register(SchedulerComposerExtension)
|
ExtensionRegistry.Composer.register(SchedulerComposerExtension)
|
||||||
|
|
||||||
const errorMessage = `There was a temporary problem setting up \
|
|
||||||
these proposed times. Please manually follow up to schedule your event.`
|
|
||||||
|
|
||||||
this._usub = Actions.sendDraftSuccess.listen(({message, draftClientId}) => {
|
|
||||||
if (!NylasEnv.isMainWindow()) return;
|
|
||||||
if (message.metadataForPluginId(PLUGIN_ID)) {
|
|
||||||
const task = new RegisterDraftForPluginTask({
|
|
||||||
errorMessage,
|
|
||||||
draftClientId,
|
|
||||||
messageId: message.id,
|
|
||||||
pluginServerUrl: `${PLUGIN_URL}/plugins/register-message`,
|
|
||||||
});
|
|
||||||
Actions.queueTask(task);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +52,5 @@ export function deactivate() {
|
||||||
ComponentRegistry.unregister(NewEventCardContainer);
|
ComponentRegistry.unregister(NewEventCardContainer);
|
||||||
ComponentRegistry.unregister(SchedulerComposerButton);
|
ComponentRegistry.unregister(SchedulerComposerButton);
|
||||||
ExtensionRegistry.Composer.unregister(SchedulerComposerExtension);
|
ExtensionRegistry.Composer.unregister(SchedulerComposerExtension);
|
||||||
this._usub()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import _ from 'underscore'
|
import _ from 'underscore'
|
||||||
import NylasStore from 'nylas-store'
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import Proposal from './proposal'
|
import Proposal from './proposal'
|
||||||
|
import NylasStore from 'nylas-store'
|
||||||
import SchedulerActions from './scheduler-actions'
|
import SchedulerActions from './scheduler-actions'
|
||||||
import {Event, Message, Actions, DraftStore, DatabaseStore} from 'nylas-exports'
|
import {Event} from 'nylas-exports'
|
||||||
import {PLUGIN_ID, CALENDAR_ID} from './scheduler-constants'
|
import {CALENDAR_ID} from './scheduler-constants'
|
||||||
|
|
||||||
// moment-round upon require patches `moment` with new functions.
|
// moment-round upon require patches `moment` with new functions.
|
||||||
require('moment-round')
|
require('moment-round')
|
||||||
|
@ -99,71 +99,6 @@ class ProposedTimeCalendarStore extends NylasStore {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This removes the metadata on the draft and creates an `Event` on
|
|
||||||
* `draft.events`
|
|
||||||
*/
|
|
||||||
_convertToDraftEvent(draft) {
|
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
|
||||||
return DraftStore.sessionForClientId(draft.clientId).then((session) => {
|
|
||||||
if (metadata.pendingEvent) {
|
|
||||||
const event = new Event().fromJSON(metadata.pendingEvent);
|
|
||||||
session.changes.add({events: [event]});
|
|
||||||
} else {
|
|
||||||
session.changes.add({events: []})
|
|
||||||
}
|
|
||||||
|
|
||||||
delete metadata.uid
|
|
||||||
delete metadata.proposals
|
|
||||||
delete metadata.pendingEvent
|
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
|
||||||
|
|
||||||
return session.changes.commit()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_convertToPendingEvent(draft, proposals) {
|
|
||||||
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
|
||||||
metadata.proposals = proposals;
|
|
||||||
|
|
||||||
// This is used to so the backend can reference which draft
|
|
||||||
// corresponds to which sent message. The backend uses the key `uid`
|
|
||||||
metadata.uid = draft.clientId;
|
|
||||||
|
|
||||||
if (draft.events.length > 0) {
|
|
||||||
return DraftStore.sessionForClientId(draft.clientId).then((session) => {
|
|
||||||
metadata.pendingEvent = draft.events[0].toJSON();
|
|
||||||
session.changes.add({events: []});
|
|
||||||
return session.changes.commit().then(() => {
|
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will bundle up and attach the choices as metadata on the draft.
|
|
||||||
*
|
|
||||||
* Once we attach metadata to the draft, we need to make sure we clear
|
|
||||||
* the start and end times of the event.
|
|
||||||
*/
|
|
||||||
_onConfirmChoices = (proposals) => {
|
|
||||||
this._pendingSave = true;
|
|
||||||
this.trigger();
|
|
||||||
|
|
||||||
const {draftClientId} = NylasEnv.getWindowProps();
|
|
||||||
|
|
||||||
DatabaseStore.find(Message, draftClientId).then((draft) => {
|
|
||||||
if (proposals.length === 0) {
|
|
||||||
return this._convertToDraftEvent(draft)
|
|
||||||
}
|
|
||||||
return this._convertToPendingEvent(draft, proposals);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ProposedTimeCalendarStore()
|
export default new ProposedTimeCalendarStore()
|
|
@ -0,0 +1,46 @@
|
||||||
|
import NylasStore from 'nylas-store'
|
||||||
|
import SchedulerActions from './scheduler-actions'
|
||||||
|
import {Message, Actions, DatabaseStore} from 'nylas-exports'
|
||||||
|
import {PLUGIN_ID} from './scheduler-constants'
|
||||||
|
|
||||||
|
// moment-round upon require patches `moment` with new functions.
|
||||||
|
require('moment-round')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintains the creation of "Proposed Times" when scheduling with people.
|
||||||
|
*
|
||||||
|
* The proposed times are displayed in various calendar views.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class ProposedTimeMainWindowStore extends NylasStore {
|
||||||
|
activate() {
|
||||||
|
this.unsubscribers = [
|
||||||
|
SchedulerActions.confirmChoices.listen(this._onConfirmChoices),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.unsubscribers.forEach(unsub => unsub())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will bundle up and attach the choices as metadata on the draft.
|
||||||
|
*
|
||||||
|
* Once we attach metadata to the draft, we need to make sure we clear
|
||||||
|
* the start and end times of the event.
|
||||||
|
*/
|
||||||
|
_onConfirmChoices = ({proposals = [], draftClientId}) => {
|
||||||
|
this._pendingSave = true;
|
||||||
|
this.trigger();
|
||||||
|
|
||||||
|
DatabaseStore.find(Message, draftClientId).then((draft) => {
|
||||||
|
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
||||||
|
if (proposals.length === 0) {
|
||||||
|
delete metadata.proposals
|
||||||
|
}
|
||||||
|
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ProposedTimeMainWindowStore()
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "N1-Scheduler",
|
"name": "composer-scheduler",
|
||||||
"title":"N1 Scheduler",
|
"title":"Scheduler",
|
||||||
"description": "The easiest way to schedule events",
|
"description": "The easiest way to schedule events",
|
||||||
"main": "./lib/main",
|
"main": "./lib/main",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
|
@ -1,13 +1,10 @@
|
||||||
import {
|
import {
|
||||||
Actions,
|
|
||||||
ComponentRegistry,
|
ComponentRegistry,
|
||||||
ExtensionRegistry,
|
ExtensionRegistry,
|
||||||
RegisterDraftForPluginTask,
|
|
||||||
} from 'nylas-exports';
|
} from 'nylas-exports';
|
||||||
import LinkTrackingButton from './link-tracking-button';
|
import LinkTrackingButton from './link-tracking-button';
|
||||||
import LinkTrackingComposerExtension from './link-tracking-composer-extension';
|
import LinkTrackingComposerExtension from './link-tracking-composer-extension';
|
||||||
import LinkTrackingMessageExtension from './link-tracking-message-extension';
|
import LinkTrackingMessageExtension from './link-tracking-message-extension';
|
||||||
import {PLUGIN_ID, PLUGIN_URL} from './link-tracking-constants'
|
|
||||||
|
|
||||||
|
|
||||||
export function activate() {
|
export function activate() {
|
||||||
|
@ -17,22 +14,6 @@ export function activate() {
|
||||||
ExtensionRegistry.Composer.register(LinkTrackingComposerExtension);
|
ExtensionRegistry.Composer.register(LinkTrackingComposerExtension);
|
||||||
|
|
||||||
ExtensionRegistry.MessageView.register(LinkTrackingMessageExtension);
|
ExtensionRegistry.MessageView.register(LinkTrackingMessageExtension);
|
||||||
|
|
||||||
const errorMessage = `There was a problem saving your link tracking \
|
|
||||||
settings. This message will not have link tracking.`
|
|
||||||
|
|
||||||
this._usub = Actions.sendDraftSuccess.listen(({message, draftClientId}) => {
|
|
||||||
if (!NylasEnv.isMainWindow()) return;
|
|
||||||
if (message.metadataForPluginId(PLUGIN_ID)) {
|
|
||||||
const task = new RegisterDraftForPluginTask({
|
|
||||||
errorMessage,
|
|
||||||
draftClientId,
|
|
||||||
messageId: message.id,
|
|
||||||
pluginServerUrl: `${PLUGIN_URL}/plugins/register-message`,
|
|
||||||
});
|
|
||||||
Actions.queueTask(task);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serialize() {}
|
export function serialize() {}
|
||||||
|
@ -41,5 +22,4 @@ export function deactivate() {
|
||||||
ComponentRegistry.unregister(LinkTrackingButton);
|
ComponentRegistry.unregister(LinkTrackingButton);
|
||||||
ExtensionRegistry.Composer.unregister(LinkTrackingComposerExtension);
|
ExtensionRegistry.Composer.unregister(LinkTrackingComposerExtension);
|
||||||
ExtensionRegistry.MessageView.unregister(LinkTrackingMessageExtension);
|
ExtensionRegistry.MessageView.unregister(LinkTrackingMessageExtension);
|
||||||
this._usub()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import {
|
import {
|
||||||
Actions,
|
|
||||||
ComponentRegistry,
|
ComponentRegistry,
|
||||||
ExtensionRegistry,
|
ExtensionRegistry,
|
||||||
RegisterDraftForPluginTask,
|
|
||||||
} from 'nylas-exports';
|
} from 'nylas-exports';
|
||||||
import OpenTrackingButton from './open-tracking-button';
|
import OpenTrackingButton from './open-tracking-button';
|
||||||
import OpenTrackingIcon from './open-tracking-icon';
|
import OpenTrackingIcon from './open-tracking-icon';
|
||||||
import OpenTrackingMessageStatus from './open-tracking-message-status';
|
import OpenTrackingMessageStatus from './open-tracking-message-status';
|
||||||
import OpenTrackingComposerExtension from './open-tracking-composer-extension';
|
import OpenTrackingComposerExtension from './open-tracking-composer-extension';
|
||||||
import {PLUGIN_ID, PLUGIN_URL} from './open-tracking-constants'
|
|
||||||
|
|
||||||
export function activate() {
|
export function activate() {
|
||||||
ComponentRegistry.register(OpenTrackingButton,
|
ComponentRegistry.register(OpenTrackingButton,
|
||||||
|
@ -21,22 +18,6 @@ export function activate() {
|
||||||
{role: 'MessageHeaderStatus'});
|
{role: 'MessageHeaderStatus'});
|
||||||
|
|
||||||
ExtensionRegistry.Composer.register(OpenTrackingComposerExtension);
|
ExtensionRegistry.Composer.register(OpenTrackingComposerExtension);
|
||||||
|
|
||||||
const errorMessage = `There was a problem saving your read receipt \
|
|
||||||
settings. You will not get a read receipt for this message.`
|
|
||||||
|
|
||||||
this._usub = Actions.sendDraftSuccess.listen(({message, draftClientId}) => {
|
|
||||||
if (!NylasEnv.isMainWindow()) return;
|
|
||||||
if (message.metadataForPluginId(PLUGIN_ID)) {
|
|
||||||
const task = new RegisterDraftForPluginTask({
|
|
||||||
errorMessage,
|
|
||||||
draftClientId,
|
|
||||||
messageId: message.id,
|
|
||||||
pluginServerUrl: `${PLUGIN_URL}/plugins/register-message`,
|
|
||||||
});
|
|
||||||
Actions.queueTask(task);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serialize() {}
|
export function serialize() {}
|
||||||
|
@ -46,5 +27,4 @@ export function deactivate() {
|
||||||
ComponentRegistry.unregister(OpenTrackingIcon);
|
ComponentRegistry.unregister(OpenTrackingIcon);
|
||||||
ComponentRegistry.unregister(OpenTrackingMessageStatus);
|
ComponentRegistry.unregister(OpenTrackingMessageStatus);
|
||||||
ExtensionRegistry.Composer.unregister(OpenTrackingComposerExtension);
|
ExtensionRegistry.Composer.unregister(OpenTrackingComposerExtension);
|
||||||
this._usub()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,11 @@ import {
|
||||||
SendDraftTask,
|
SendDraftTask,
|
||||||
NylasAPI,
|
NylasAPI,
|
||||||
SoundRegistry,
|
SoundRegistry,
|
||||||
|
SyncbackMetadataTask,
|
||||||
} from 'nylas-exports';
|
} from 'nylas-exports';
|
||||||
|
|
||||||
|
import NotifyPluginsOfSendTask from '../../src/flux/tasks/notify-plugis-of-send-task'
|
||||||
|
|
||||||
const DBt = DatabaseTransaction.prototype;
|
const DBt = DatabaseTransaction.prototype;
|
||||||
const withoutWhitespace = (s) => s.replace(/[\n\r\s]/g, '');
|
const withoutWhitespace = (s) => s.replace(/[\n\r\s]/g, '');
|
||||||
|
|
||||||
|
@ -169,7 +172,8 @@ describe("SendDraftTask", () => {
|
||||||
it("should queue tasks to sync back the metadata on the new message", () => {
|
it("should queue tasks to sync back the metadata on the new message", () => {
|
||||||
spyOn(Actions, 'queueTask')
|
spyOn(Actions, 'queueTask')
|
||||||
waitsForPromise(() => this.task.performRemote().then(() => {
|
waitsForPromise(() => this.task.performRemote().then(() => {
|
||||||
const metadataTasks = Actions.queueTask.calls.map((call) => call.args[0]);
|
let metadataTasks = Actions.queueTask.calls.map((call) => call.args[0]);
|
||||||
|
metadataTasks = metadataTasks.filter((task) => task instanceof SyncbackMetadataTask)
|
||||||
expect(metadataTasks.length).toEqual(this.draft.pluginMetadata.length);
|
expect(metadataTasks.length).toEqual(this.draft.pluginMetadata.length);
|
||||||
this.draft.pluginMetadata.forEach((pluginMetadatum, idx) => {
|
this.draft.pluginMetadata.forEach((pluginMetadatum, idx) => {
|
||||||
expect(metadataTasks[idx].clientId).toEqual(this.draft.clientId);
|
expect(metadataTasks[idx].clientId).toEqual(this.draft.clientId);
|
||||||
|
@ -179,6 +183,27 @@ describe("SendDraftTask", () => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should queue a task to register the messageID with the plugin server", () => {
|
||||||
|
spyOn(Actions, 'queueTask')
|
||||||
|
waitsForPromise(() => this.task.performRemote().then(() => {
|
||||||
|
let tasks = Actions.queueTask.calls.map((call) => call.args[0]);
|
||||||
|
tasks = tasks.filter((task) => task instanceof NotifyPluginsOfSendTask)
|
||||||
|
expect(tasks.length).toEqual(1);
|
||||||
|
expect(tasks[0].accountId).toEqual(this.draft.accountId);
|
||||||
|
expect(tasks[0].messageId).toEqual(this.response.id);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't queue a NotifyPluginsOfSendTask if there's no metadata", () => {
|
||||||
|
spyOn(Actions, 'queueTask');
|
||||||
|
this.draft.pluginMetadata = []
|
||||||
|
waitsForPromise(() => this.task.performRemote().then(() => {
|
||||||
|
let tasks = Actions.queueTask.calls.map((call) => call.args[0]);
|
||||||
|
tasks = tasks.filter((task) => task instanceof NotifyPluginsOfSendTask)
|
||||||
|
expect(tasks.length).toEqual(0);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
it("should play a sound", () => {
|
it("should play a sound", () => {
|
||||||
spyOn(NylasEnv.config, "get").andReturn(true)
|
spyOn(NylasEnv.config, "get").andReturn(true)
|
||||||
waitsForPromise(() => this.task.performRemote().then(() => {
|
waitsForPromise(() => this.task.performRemote().then(() => {
|
||||||
|
|
|
@ -78,7 +78,7 @@ class Application
|
||||||
|
|
||||||
if not @config.get('core.disabledPackagesInitialized')
|
if not @config.get('core.disabledPackagesInitialized')
|
||||||
exampleNewNames = {
|
exampleNewNames = {
|
||||||
'N1-Scheduler': 'N1-Scheduler',
|
'N1-Scheduler': 'composer-scheduler',
|
||||||
'N1-Composer-Templates': 'composer-templates',
|
'N1-Composer-Templates': 'composer-templates',
|
||||||
'N1-Composer-Translate': 'composer-translate',
|
'N1-Composer-Translate': 'composer-translate',
|
||||||
'N1-Message-View-on-Github':'message-view-on-github',
|
'N1-Message-View-on-Github':'message-view-on-github',
|
||||||
|
|
|
@ -84,8 +84,11 @@ export default class TimePicker extends React.Component {
|
||||||
el.setSelectionRange(0, el.value.length)
|
el.setSelectionRange(0, el.value.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
_onBlur = () => {
|
_onBlur = (event) => {
|
||||||
this.setState({focused: false})
|
this.setState({focused: false});
|
||||||
|
if (Array.from(event.relatedTarget.classList).includes("time-options")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this._saveIfValid(this.state.rawText)
|
this._saveIfValid(this.state.rawText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +195,7 @@ export default class TimePicker extends React.Component {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>{opts}</div>
|
<div className={className} tabIndex={-1}>{opts}</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ FocusedContentStore = require './focused-content-store'
|
||||||
BaseDraftTask = require '../tasks/base-draft-task'
|
BaseDraftTask = require '../tasks/base-draft-task'
|
||||||
SendDraftTask = require '../tasks/send-draft-task'
|
SendDraftTask = require '../tasks/send-draft-task'
|
||||||
SyncbackDraftFilesTask = require '../tasks/syncback-draft-files-task'
|
SyncbackDraftFilesTask = require '../tasks/syncback-draft-files-task'
|
||||||
SyncbackDraftEventsTask = require '../tasks/syncback-draft-events-task'
|
|
||||||
SyncbackDraftTask = require '../tasks/syncback-draft-task'
|
SyncbackDraftTask = require '../tasks/syncback-draft-task'
|
||||||
DestroyDraftTask = require '../tasks/destroy-draft-task'
|
DestroyDraftTask = require '../tasks/destroy-draft-task'
|
||||||
|
|
||||||
|
@ -342,8 +341,6 @@ class DraftStore
|
||||||
_queueDraftAssetTasks: (draft) =>
|
_queueDraftAssetTasks: (draft) =>
|
||||||
if draft.files.length > 0 or draft.uploads.length > 0
|
if draft.files.length > 0 or draft.uploads.length > 0
|
||||||
Actions.queueTask(new SyncbackDraftFilesTask(draft.clientId))
|
Actions.queueTask(new SyncbackDraftFilesTask(draft.clientId))
|
||||||
if draft.events.length
|
|
||||||
Actions.queueTask(new SyncbackDraftEventsTask(draft.clientId))
|
|
||||||
|
|
||||||
_isPopout: ->
|
_isPopout: ->
|
||||||
NylasEnv.getWindowType() is "composer"
|
NylasEnv.getWindowType() is "composer"
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import Task from './task'
|
import Task from './task'
|
||||||
import {APIError} from '../errors'
|
import {APIError} from '../errors'
|
||||||
import NylasAPI from '../nylas-api'
|
import NylasAPI from '../nylas-api'
|
||||||
|
import EdgehillAPI from '../edgehill-api'
|
||||||
// We use our local `request` so we can track the outgoing calls and
|
import SyncbackMetadataTask from './syncback-metadata-task'
|
||||||
// generate consistent error objects
|
|
||||||
import nylasRequest from '../../nylas-request'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a plugin:
|
* If a plugin:
|
||||||
|
@ -50,45 +48,56 @@ import nylasRequest from '../../nylas-request'
|
||||||
* The task will POST to your backend url the draftClientId and the
|
* The task will POST to your backend url the draftClientId and the
|
||||||
* coresponding messageId
|
* coresponding messageId
|
||||||
*/
|
*/
|
||||||
export default class RegisterDraftForPluginTask extends Task {
|
export default class NotifyPluginsOfSendTask extends Task {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
super(opts)
|
super(opts)
|
||||||
|
this.accountId = opts.accountId
|
||||||
this.messageId = opts.messageId
|
this.messageId = opts.messageId
|
||||||
this.errorMessage = opts.errorMessage
|
this.messageClientId = opts.messageClientId
|
||||||
this.draftClientId = opts.draftClientId
|
this.errorMessage = `We had trouble connecting to the plugin server. \
|
||||||
this.pluginServerUrl = opts.pluginServerUrl
|
Any plugins you used in your sent message will not be available.`
|
||||||
|
}
|
||||||
|
|
||||||
|
isDependentOnTask(other) {
|
||||||
|
return (other instanceof SyncbackMetadataTask) && (other.clientId === this.messageClientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
performLocal() {
|
performLocal() {
|
||||||
this.validateRequiredFields([
|
this.validateRequiredFields([
|
||||||
"messageId",
|
"messageId",
|
||||||
"draftClientId",
|
"accountId",
|
||||||
"pluginServerUrl",
|
"messageClientId",
|
||||||
]);
|
]);
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
performRemote() {
|
performRemote() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
nylasRequest.post({url: this.pluginServerUrl, body: {
|
EdgehillAPI.request({
|
||||||
message_id: this.messageId,
|
method: "POST",
|
||||||
uid: this.draftClientId,
|
path: "/plugins/send-successful",
|
||||||
}}, (err) => {
|
body: {
|
||||||
if (err instanceof APIError) {
|
message_id: this.messageId,
|
||||||
const msg = `${this.errorMessage}\n\n${err.message}`
|
account_id: this.accountId,
|
||||||
if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {
|
},
|
||||||
NylasEnv.showErrorDialog(msg, {showInMainWindow: true})
|
success: () => {
|
||||||
return resolve([Task.Status.Failed, err])
|
return resolve(Task.Status.Success)
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
if (err instanceof APIError) {
|
||||||
|
const msg = `${this.errorMessage}\n\n${err.message}`
|
||||||
|
if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {
|
||||||
|
NylasEnv.showErrorDialog(msg, {showInMainWindow: true})
|
||||||
|
return resolve([Task.Status.Failed, err])
|
||||||
|
}
|
||||||
|
return resolve(Task.Status.Retry)
|
||||||
}
|
}
|
||||||
return resolve(Task.Status.Retry)
|
|
||||||
} else if (err) {
|
|
||||||
const msg = `${this.errorMessage}\n\n${err.message}`
|
const msg = `${this.errorMessage}\n\n${err.message}`
|
||||||
NylasEnv.reportError(err);
|
NylasEnv.reportError(err);
|
||||||
NylasEnv.showErrorDialog(msg, {showInMainWindow: true})
|
NylasEnv.showErrorDialog(msg, {showInMainWindow: true})
|
||||||
return resolve([Task.Status.Failed, err])
|
return resolve([Task.Status.Failed, err])
|
||||||
}
|
},
|
||||||
return resolve(Task.Status.Success)
|
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ import DatabaseStore from '../stores/database-store';
|
||||||
import AccountStore from '../stores/account-store';
|
import AccountStore from '../stores/account-store';
|
||||||
import BaseDraftTask from './base-draft-task';
|
import BaseDraftTask from './base-draft-task';
|
||||||
import SyncbackMetadataTask from './syncback-metadata-task';
|
import SyncbackMetadataTask from './syncback-metadata-task';
|
||||||
|
import NotifyPluginsOfSendTask from './notify-plugins-of-send-task';
|
||||||
|
|
||||||
export default class SendDraftTask extends BaseDraftTask {
|
export default class SendDraftTask extends BaseDraftTask {
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ export default class SendDraftTask extends BaseDraftTask {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
.then(this.updatePluginMetadata)
|
||||||
.then(this.onSuccess)
|
.then(this.onSuccess)
|
||||||
.catch(this.onError);
|
.catch(this.onError);
|
||||||
}
|
}
|
||||||
|
@ -89,13 +91,26 @@ export default class SendDraftTask extends BaseDraftTask {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess = () => {
|
updatePluginMetadata = () => {
|
||||||
// Queue a task to save metadata on the message
|
|
||||||
this.message.pluginMetadata.forEach((m) => {
|
this.message.pluginMetadata.forEach((m) => {
|
||||||
const task = new SyncbackMetadataTask(this.message.clientId, this.message.constructor.name, m.pluginId);
|
const t1 = new SyncbackMetadataTask(this.message.clientId,
|
||||||
Actions.queueTask(task);
|
this.message.constructor.name, m.pluginId);
|
||||||
|
Actions.queueTask(t1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.message.pluginMetadata.length > 0) {
|
||||||
|
const t2 = new NotifyPluginsOfSendTask({
|
||||||
|
accountId: this.message.accountId,
|
||||||
|
messageId: this.message.id,
|
||||||
|
messageClientId: this.message.clientId,
|
||||||
|
});
|
||||||
|
Actions.queueTask(t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess = () => {
|
||||||
Actions.sendDraftSuccess({message: this.message, messageClientId: this.message.clientId, draftClientId: this.draftClientId});
|
Actions.sendDraftSuccess({message: this.message, messageClientId: this.message.clientId, draftClientId: this.draftClientId});
|
||||||
NylasAPI.makeDraftDeletionRequest(this.draft);
|
NylasAPI.makeDraftDeletionRequest(this.draft);
|
||||||
|
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import Task from './task';
|
|
||||||
import {APIError} from '../errors';
|
|
||||||
import NylasAPI from '../nylas-api';
|
|
||||||
import BaseDraftTask from './base-draft-task';
|
|
||||||
import DatabaseStore from '../stores/database-store';
|
|
||||||
import Event from '../models/event';
|
|
||||||
|
|
||||||
export default class SyncbackDraftEventsTask extends BaseDraftTask {
|
|
||||||
|
|
||||||
constructor(draftClientId) {
|
|
||||||
super(draftClientId);
|
|
||||||
this._appliedEvents = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
label() {
|
|
||||||
return "Creating meeting request...";
|
|
||||||
}
|
|
||||||
|
|
||||||
performRemote() {
|
|
||||||
return this.refreshDraftReference()
|
|
||||||
.then(this.uploadEvents)
|
|
||||||
.then(this.applyChangesToDraft)
|
|
||||||
.thenReturn(Task.Status.Success)
|
|
||||||
.catch((err) => {
|
|
||||||
if (err instanceof BaseDraftTask.DraftNotFoundError) {
|
|
||||||
return Promise.resolve(Task.Status.Continue);
|
|
||||||
}
|
|
||||||
if (err instanceof APIError && !NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {
|
|
||||||
return Promise.resolve(Task.Status.Retry);
|
|
||||||
}
|
|
||||||
return Promise.resolve([Task.Status.Failed, err]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadEvents = () => {
|
|
||||||
const events = this.draft.events;
|
|
||||||
if (events && events.length) {
|
|
||||||
const event = events[0]; // only upload one
|
|
||||||
return this.uploadEvent(event).then((savedEvent) => {
|
|
||||||
if (savedEvent) {
|
|
||||||
this._appliedEvents = [savedEvent];
|
|
||||||
}
|
|
||||||
Promise.resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve()
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadEvent = (event) => {
|
|
||||||
return NylasAPI.makeRequest({
|
|
||||||
path: "/events?notify_participants=true",
|
|
||||||
accountId: this.draft.accountId,
|
|
||||||
method: "POST",
|
|
||||||
body: this._prepareEventJson(event),
|
|
||||||
returnsModel: true,
|
|
||||||
}).then(json =>{
|
|
||||||
return (new Event()).fromJSON(json);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_prepareEventJson(inEvent) {
|
|
||||||
const event = inEvent.fromDraft(this.draft)
|
|
||||||
const json = event.toJSON();
|
|
||||||
delete json.id;
|
|
||||||
json.when = {
|
|
||||||
start_time: json._start,
|
|
||||||
end_time: json._end,
|
|
||||||
};
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyChangesToDraft = () => {
|
|
||||||
return DatabaseStore.inTransaction((t) => {
|
|
||||||
return this.refreshDraftReference().then(() => {
|
|
||||||
this.draft.events = this._appliedEvents;
|
|
||||||
return t.persistModel(this.draft);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -50,7 +50,6 @@ class NylasExports
|
||||||
@load "Actions", 'flux/actions'
|
@load "Actions", 'flux/actions'
|
||||||
|
|
||||||
# API Endpoints
|
# API Endpoints
|
||||||
@load "nylasRequest", 'nylas-request' # An extend `request` module
|
|
||||||
@load "NylasAPI", 'flux/nylas-api'
|
@load "NylasAPI", 'flux/nylas-api'
|
||||||
@load "NylasSyncStatusStore", 'flux/stores/nylas-sync-status-store'
|
@load "NylasSyncStatusStore", 'flux/stores/nylas-sync-status-store'
|
||||||
@load "EdgehillAPI", 'flux/edgehill-api'
|
@load "EdgehillAPI", 'flux/edgehill-api'
|
||||||
|
@ -109,14 +108,12 @@ class NylasExports
|
||||||
@require "DestroyCategoryTask", 'flux/tasks/destroy-category-task'
|
@require "DestroyCategoryTask", 'flux/tasks/destroy-category-task'
|
||||||
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
@require "ChangeUnreadTask", 'flux/tasks/change-unread-task'
|
||||||
@require "SyncbackDraftFilesTask", 'flux/tasks/syncback-draft-files-task'
|
@require "SyncbackDraftFilesTask", 'flux/tasks/syncback-draft-files-task'
|
||||||
@require "SyncbackDraftEventsTask", 'flux/tasks/syncback-draft-events-task'
|
|
||||||
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft-task'
|
@require "SyncbackDraftTask", 'flux/tasks/syncback-draft-task'
|
||||||
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
@require "ChangeStarredTask", 'flux/tasks/change-starred-task'
|
||||||
@require "DestroyModelTask", 'flux/tasks/destroy-model-task'
|
@require "DestroyModelTask", 'flux/tasks/destroy-model-task'
|
||||||
@require "SyncbackModelTask", 'flux/tasks/syncback-model-task'
|
@require "SyncbackModelTask", 'flux/tasks/syncback-model-task'
|
||||||
@require "SyncbackMetadataTask", 'flux/tasks/syncback-metadata-task'
|
@require "SyncbackMetadataTask", 'flux/tasks/syncback-metadata-task'
|
||||||
@require "ReprocessMailRulesTask", 'flux/tasks/reprocess-mail-rules-task'
|
@require "ReprocessMailRulesTask", 'flux/tasks/reprocess-mail-rules-task'
|
||||||
@require "RegisterDraftForPluginTask", 'flux/tasks/register-draft-for-plugin-task'
|
|
||||||
|
|
||||||
# Stores
|
# Stores
|
||||||
# These need to be required immediately since some Stores are
|
# These need to be required immediately since some Stores are
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import _ from 'underscore'
|
|
||||||
import request from 'request'
|
|
||||||
import Utils from './flux/models/utils'
|
|
||||||
import Actions from './flux/actions'
|
|
||||||
import {APIError} from './flux/errors'
|
|
||||||
/**
|
|
||||||
* A light wrapper around the `request` library to make sure we're logging
|
|
||||||
* requests and retruning standard error types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const origInit = request.Request.prototype.init
|
|
||||||
|
|
||||||
request.Request.prototype.init = function nylasRequestInit(options) {
|
|
||||||
const opts = options;
|
|
||||||
const requestId = Utils.generateTempId()
|
|
||||||
opts.startTime = Date.now();
|
|
||||||
Actions.willMakeAPIRequest({
|
|
||||||
request: opts,
|
|
||||||
requestId,
|
|
||||||
});
|
|
||||||
const origCallback = opts.callback || function noop() {}
|
|
||||||
|
|
||||||
// It's a super common error to pass an object `body`, but forget to
|
|
||||||
// pass `json:true`. If you don't pass `json:true` the body won't be
|
|
||||||
// automatically stringified. We'll take care of doing that for you.
|
|
||||||
if (!_.isString(opts.body) && !_.isArray(opts.body)) {
|
|
||||||
if (opts.json === null || opts.json === undefined) {
|
|
||||||
opts.json = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.callback = (error, response, body) => {
|
|
||||||
let statusCode;
|
|
||||||
let apiError;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
if (error.code) {
|
|
||||||
statusCode = error.code;
|
|
||||||
apiError = new APIError({
|
|
||||||
error, response, body, statusCode,
|
|
||||||
requestOptions: opts,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
} else if (response) {
|
|
||||||
statusCode = response.statusCode;
|
|
||||||
if (statusCode > 299) {
|
|
||||||
apiError = new APIError({
|
|
||||||
error, response, body, statusCode,
|
|
||||||
requestOptions: opts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error("Got a request with no error and no response!")
|
|
||||||
}
|
|
||||||
|
|
||||||
Actions.didMakeAPIRequest({
|
|
||||||
request: opts,
|
|
||||||
statusCode,
|
|
||||||
error,
|
|
||||||
requestId,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (apiError) {
|
|
||||||
NylasEnv.errorLogger.apiDebug(apiError)
|
|
||||||
}
|
|
||||||
|
|
||||||
return origCallback(apiError, response, body)
|
|
||||||
}
|
|
||||||
this.callback = opts.callback
|
|
||||||
return origInit.call(this, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default request
|
|