diff --git a/internal_packages/composer-scheduler/lib/composer/new-event-card-container.jsx b/internal_packages/composer-scheduler/lib/composer/new-event-card-container.jsx index 61ee77fa0..0e59c89ac 100644 --- a/internal_packages/composer-scheduler/lib/composer/new-event-card-container.jsx +++ b/internal_packages/composer-scheduler/lib/composer/new-event-card-container.jsx @@ -1,7 +1,8 @@ import React, {Component, PropTypes} from 'react'; +import SchedulerActions from '../scheduler-actions' import NewEventCard from './new-event-card' import {PLUGIN_ID} from '../scheduler-constants' -import {Utils, Event, Actions, DraftStore} from 'nylas-exports'; +import {Utils, Event} from 'nylas-exports'; /** * When you're creating an event you can either be creating: @@ -18,84 +19,80 @@ export default class NewEventCardContainer extends Component { static displayName = 'NewEventCardContainer'; static propTypes = { - draftClientId: PropTypes.string, + draft: PropTypes.object.isRequired, + session: PropTypes.object.isRequired, } constructor(props) { super(props); - this.state = {event: null}; - this._session = null; - this._mounted = false; - this._usub = () => {} } - componentWillMount() { - this._mounted = true; - this._loadDraft(this.props.draftClientId); - } - - componentWillReceiveProps(newProps) { - this._loadDraft(newProps.draftClientId); + componentDidMount() { + this._unlisten = SchedulerActions.confirmChoices.listen(this._onConfirmChoices); } componentWillUnmount() { - this._mounted = false; - this._usub() + if (this._unlisten) { + this._unlisten(); + } } - _loadDraft(draftClientId) { - DraftStore.sessionForClientId(draftClientId).then(session => { - // Only run if things are still relevant: component is mounted - // and draftClientIds still match - if (this._mounted) { - this._session = session; - this._usub() - this._usub = session.listen(this._onDraftChange); - this._onDraftChange(); - } - }); - } + _onConfirmChoices = ({proposals = [], draftClientId}) => { + const {draft} = this.props; - _onDraftChange = () => { - this.setState({event: this._getEvent()}); + if (draft.clientId !== draftClientId) { + return; + } + + const metadata = draft.metadataForPluginId(PLUGIN_ID) || {}; + if (proposals.length === 0) { + delete metadata.proposals; + } else { + metadata.proposals = proposals; + } + this.props.session.changes.addPluginMetadata(PLUGIN_ID, metadata); } _getEvent() { - const metadata = this._session.draft().metadataForPluginId(PLUGIN_ID); + const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID); if (metadata && metadata.pendingEvent) { - return new Event().fromJSON(metadata.pendingEvent || {}) + return new Event().fromJSON(metadata.pendingEvent || {}); } return null } _updateEvent = (newData) => { - const newEvent = Object.assign(this._getEvent().clone(), newData) + const {draft, session} = this.props; + + const newEvent = Object.assign(this._getEvent().clone(), newData); const newEventJSON = newEvent.toJSON(); - const metadata = this._session.draft().metadataForPluginId(PLUGIN_ID); + const metadata = draft.metadataForPluginId(PLUGIN_ID); if (!Utils.isEqual(metadata.pendingEvent, newEventJSON)) { metadata.pendingEvent = newEventJSON; - this._session.changes.addPluginMetadata(PLUGIN_ID, metadata); + session.changes.addPluginMetadata(PLUGIN_ID, metadata); } } _removeEvent = () => { - const draft = this._session.draft() + const {draft, session} = this.props; const metadata = draft.metadataForPluginId(PLUGIN_ID); if (metadata) { - delete metadata.pendingEvent + delete metadata.pendingEvent; delete metadata.proposals - Actions.setMetadata(draft, PLUGIN_ID, metadata); + session.changes.addPluginMetadata(PLUGIN_ID, metadata); } } render() { + const event = this._getEvent(); let card = false; - if (this._session && this.state.event) { + + if (event) { card = ( - {}} diff --git a/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx b/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx index 3b6ae6aec..5f0d9d320 100644 --- a/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx +++ b/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx @@ -173,9 +173,14 @@ export default class NewEventCard extends React.Component {
{this._renderIcon("ic-eventcard-time@2x.png")} - + to - diff --git a/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx b/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx index 8c8fc6098..79ebc143e 100644 --- a/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx +++ b/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx @@ -4,7 +4,6 @@ import { Actions, APIError, NylasAPI, - DraftStore, } from 'nylas-exports' import {Menu, RetinaImg} from 'nylas-component-kit' import {PLUGIN_ID, PLUGIN_NAME} from '../scheduler-constants' @@ -22,47 +21,23 @@ export default class SchedulerComposerButton extends React.Component { static displayName = "SchedulerComposerButton"; static propTypes = { - draftClientId: React.PropTypes.string, + draft: React.PropTypes.object.isRequired, + session: React.PropTypes.object.isRequired, }; constructor(props) { super(props); this.state = {enabled: false}; - this._session = null; - this._mounted = false; this._unsubscribes = []; } - componentDidMount() { - this._mounted = true; - this.handleProps() + shouldComponentUpdate(nextProps, nextState) { + return (this.state !== nextState) || + (this._hasPendingEvent(nextProps) !== this._hasPendingEvent(this.props)); } - componentWillReceiveProps(newProps) { - this.handleProps(newProps); - } - - handleProps(newProps = null) { - const props = newProps || this.props; - DraftStore.sessionForClientId(props.draftClientId).then(session => { - // Only run if things are still relevant: component is mounted - // and draftClientIds still match - const idIsCurrent = newProps ? true : this.props.draftClientId === session.draftClientId; - if (this._mounted && idIsCurrent) { - this._session = session; - const unsub = session.listen(this._onDraftChange.bind(this)); - this._unsubscribes.push(unsub); - this._onDraftChange(); - } - }); - } - - _onDraftChange() { - this.setState({enabled: this._hasPendingEvent()}); - } - - _hasPendingEvent() { - const metadata = this._session.draft().metadataForPluginId(PLUGIN_ID); + _hasPendingEvent(props) { + const metadata = props.draft.metadataForPluginId(PLUGIN_ID); return metadata && metadata.pendingEvent } @@ -90,14 +65,14 @@ export default class SchedulerComposerButton extends React.Component { } _onSelectItem = (item) => { - NewEventHelper.addEventToSession(this._session) - const draft = this._session.draft() + NewEventHelper.addEventToSession(this.props.session) + if (item === PROPOSAL) { NylasEnv.newWindow({ title: "Calendar", windowType: "calendar", windowProps: { - draftClientId: draft.clientId, + draftClientId: this.props.draft.clientId, }, }); } @@ -105,10 +80,8 @@ export default class SchedulerComposerButton extends React.Component { } _onClick = () => { - if (!this._session) { return } - const draft = this._session.draft() const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect() - NylasAPI.authPlugin(PLUGIN_ID, PLUGIN_NAME, draft.accountId) + NylasAPI.authPlugin(PLUGIN_ID, PLUGIN_NAME, this.props.draft.accountId) .catch((error) => { let title = "Error" let msg = `Unfortunately scheduling is not currently available. \ @@ -135,8 +108,9 @@ Please try again later.\n\nError: ${error}` } render() { + const hasEvent = this._hasPendingEvent(this.props); return ( - ) } } + +SchedulerComposerButton.containerRequired = false; diff --git a/internal_packages/composer-scheduler/lib/main.es6 b/internal_packages/composer-scheduler/lib/main.es6 index 4cb0a0d89..f79e48574 100644 --- a/internal_packages/composer-scheduler/lib/main.es6 +++ b/internal_packages/composer-scheduler/lib/main.es6 @@ -3,7 +3,6 @@ import ProposedTimePicker from './calendar/proposed-time-picker' import NewEventCardContainer from './composer/new-event-card-container' import SchedulerComposerButton from './composer/scheduler-composer-button'; import ProposedTimeCalendarStore from './proposed-time-calendar-store' -import ProposedTimeMainWindowStore from './proposed-time-main-window-store' import SchedulerComposerExtension from './composer/scheduler-composer-extension'; import { @@ -26,9 +25,6 @@ export function activate() { ComponentRegistry.register(ProposedTimePicker, {location: WorkspaceStore.Location.Center}) } else { - if (NylasEnv.isMainWindow()) { - ProposedTimeMainWindowStore.activate() - } ComponentRegistry.register(NewEventCardContainer, {role: 'Composer:Footer'}); @@ -48,7 +44,6 @@ export function deactivate() { ComponentRegistry.unregister(ProposedTimeEvent); ComponentRegistry.unregister(ProposedTimePicker); } else { - ProposedTimeMainWindowStore.deactivate() ComponentRegistry.unregister(NewEventCardContainer); ComponentRegistry.unregister(SchedulerComposerButton); ExtensionRegistry.Composer.unregister(SchedulerComposerExtension); diff --git a/internal_packages/composer-scheduler/lib/proposed-time-main-window-store.es6 b/internal_packages/composer-scheduler/lib/proposed-time-main-window-store.es6 deleted file mode 100644 index 666ab5655..000000000 --- a/internal_packages/composer-scheduler/lib/proposed-time-main-window-store.es6 +++ /dev/null @@ -1,48 +0,0 @@ -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 - } else { - metadata.proposals = proposals; - } - Actions.setMetadata(draft, PLUGIN_ID, metadata); - }) - } -} - -export default new ProposedTimeMainWindowStore() diff --git a/internal_packages/composer-scheduler/spec/composer-scheduler-spec-helper.es6 b/internal_packages/composer-scheduler/spec/composer-scheduler-spec-helper.es6 index f6e67e50b..f2ef76d8b 100644 --- a/internal_packages/composer-scheduler/spec/composer-scheduler-spec-helper.es6 +++ b/internal_packages/composer-scheduler/spec/composer-scheduler-spec-helper.es6 @@ -32,14 +32,8 @@ export const prepareDraft = function prepareDraft() { }) spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(draft)); - - this.session = null; - runs(() => { - DraftStore.sessionForClientId(DRAFT_CLIENT_ID).then((session) => { - this.session = session - }); - }) - waitsFor(() => this.session); + this.session = DraftStore._createSession(DRAFT_CLIENT_ID, draft); + advanceClock(); } export const cleanupDraft = function cleanupDraft() { @@ -59,4 +53,3 @@ export const setupCalendars = function setupCalendars() { return Promise.resolve(cals); }) } - diff --git a/internal_packages/composer-scheduler/spec/new-event-card-spec.jsx b/internal_packages/composer-scheduler/spec/new-event-card-spec.jsx index fd192d396..9ec1c5e27 100644 --- a/internal_packages/composer-scheduler/spec/new-event-card-spec.jsx +++ b/internal_packages/composer-scheduler/spec/new-event-card-spec.jsx @@ -6,6 +6,9 @@ import NewEventCard from '../lib/composer/new-event-card' import ReactTestUtils from 'react-addons-test-utils'; import NewEventCardContainer from '../lib/composer/new-event-card-container' +import Proposal from '../lib/proposal' +import SchedulerActions from '../lib/scheduler-actions' + import { Calendar, Event, @@ -23,16 +26,11 @@ const now = window.testNowMoment describe("NewEventCard", () => { beforeEach(() => { this.session = null - // Will eventually fill this.session prepareDraft.call(this) - runs(() => { - this.eventCardContainer = ReactTestUtils.renderIntoDocument( - - ); - }) - - waitsFor(() => this.eventCardContainer._session) + this.eventCardContainer = ReactTestUtils.renderIntoDocument( + + ); }); afterEach(() => { @@ -55,27 +53,28 @@ describe("NewEventCard", () => { }) } - const setNewTestEvent = (callback) => { - runs(() => { - if (!this.session) { - throw new Error("Setup test session first") - } - const metadata = {} - metadata.uid = DRAFT_CLIENT_ID; - metadata.pendingEvent = new Event({ + const setNewTestEvent = () => { + if (!this.session) { + throw new Error("Setup test session first") + } + this.session.changes.addPluginMetadata(PLUGIN_ID, { + uid: DRAFT_CLIENT_ID, + pendingEvent: new Event({ calendarId: "TEST_CALENDAR_ID", title: "", start: now().unix(), end: now().add(1, 'hour').unix(), - }).toJSON(); - this.session.changes.addPluginMetadata(PLUGIN_ID, metadata); - }) + }).toJSON(), + }); - waitsFor(() => this.eventCardContainer.state.event); - - runs(callback) + this.eventCardContainer = ReactTestUtils.renderIntoDocument( + + ); } + const getPendingEvent = () => + this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent + it("creates a new event card", () => { const el = ReactTestUtils.findRenderedComponentWithType(this.eventCardContainer, NewEventCardContainer); @@ -88,15 +87,14 @@ describe("NewEventCard", () => { it("renders the event card when an event is created", () => { stubCalendars() - setNewTestEvent(() => { - expect(this.eventCardContainer.refs.newEventCard).toBeDefined(); - expect(this.eventCardContainer.refs.newEventCard instanceof NewEventCard).toBe(true); - }) + setNewTestEvent() + expect(this.eventCardContainer.refs.newEventCard).toBeDefined(); + expect(this.eventCardContainer.refs.newEventCard instanceof NewEventCard).toBe(true); }); it("loads the calendars for email", () => { stubCalendars(testCalendars()) - setNewTestEvent(() => { }) + setNewTestEvent() waitsFor(() => this.eventCardContainer.refs.newEventCard.state.calendars.length > 0 ) @@ -108,125 +106,125 @@ describe("NewEventCard", () => { it("removes the event and clears metadata", () => { stubCalendars(testCalendars()) - setNewTestEvent(() => { - const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, - this.eventCardContainer); - const rmBtn = ReactDOM.findDOMNode($("remove-button")[0]); + setNewTestEvent() - // The event is there before clicking remove - expect(this.eventCardContainer.state.event).toBeDefined() - expect(this.eventCardContainer.refs.newEventCard).toBeDefined() - expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).toBeDefined() + const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, + this.eventCardContainer); + const rmBtn = ReactDOM.findDOMNode($("remove-button")[0]); - ReactTestUtils.Simulate.click(rmBtn); + // The event is there before clicking remove + expect(this.eventCardContainer.refs.newEventCard).toBeDefined() + expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).toBeDefined() - // The event has been removed from metadata and state - expect(this.eventCardContainer.state.event).toBe(null) - expect(this.eventCardContainer.refs.newEventCard).not.toBeDefined() - expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).not.toBeDefined() - }) + ReactTestUtils.Simulate.click(rmBtn); + + // The event has been removed from metadata + expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).not.toBeDefined() }); - const getPendingEvent = () => - this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent - it("properly updates the event", () => { stubCalendars(testCalendars()) - setNewTestEvent(() => { - const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, - this.eventCardContainer); - const title = ReactDOM.findDOMNode($("event-title")[0]); + setNewTestEvent() + const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, + this.eventCardContainer); + const title = ReactDOM.findDOMNode($("event-title")[0]); - // The event has the old title - expect(this.eventCardContainer.state.event.title).toBe("") - expect(getPendingEvent().title).toBe("") + // The event has the old title + expect(getPendingEvent().title).toBe("") - title.value = "Test" - ReactTestUtils.Simulate.change(title); + title.value = "Test" + ReactTestUtils.Simulate.change(title); - // The event has the new title - expect(this.eventCardContainer.state.event.title).toBe("Test") - expect(getPendingEvent().title).toBe("Test") - }) + // The event has the new title + expect(getPendingEvent().title).toBe("Test") }); it("updates the day", () => { stubCalendars(testCalendars()) - setNewTestEvent(() => { - const eventCard = this.eventCardContainer.refs.newEventCard; + setNewTestEvent() + const eventCard = this.eventCardContainer.refs.newEventCard; - // The event has the default day - const nowUnix = now().unix() - expect(this.eventCardContainer.state.event.start).toBe(nowUnix) - expect(getPendingEvent()._start).toBe(nowUnix) + // The event has the default day + const nowUnix = now().unix() + expect(getPendingEvent()._start).toBe(nowUnix) - // The event has the new day - const newDay = now().add(2, 'days'); - eventCard._onChangeDay(newDay.valueOf()); + // The event has the new day + const newDay = now().add(2, 'days'); + eventCard._onChangeDay(newDay.valueOf()); - expect(this.eventCardContainer.state.event.start).toBe(newDay.unix()) - expect(getPendingEvent()._start).toBe(newDay.unix()) - }) + expect(getPendingEvent()._start).toBe(newDay.unix()) }); it("updates the time properly", () => { stubCalendars(testCalendars()) - setNewTestEvent(() => { - const eventCard = this.eventCardContainer.refs.newEventCard; + setNewTestEvent() + const eventCard = this.eventCardContainer.refs.newEventCard; - const oldEnd = now().add(1, 'hour').unix() - expect(this.eventCardContainer.state.event.start).toBe(now().unix()) - expect(getPendingEvent()._start).toBe(now().unix()) - expect(getPendingEvent()._end).toBe(oldEnd) + const oldEnd = now().add(1, 'hour').unix() + expect(getPendingEvent()._start).toBe(now().unix()) + expect(getPendingEvent()._end).toBe(oldEnd) - const newStart = now().subtract(1, 'hour'); - eventCard._onChangeStartTime(newStart.valueOf()); + const newStart = now().subtract(1, 'hour'); + eventCard._onChangeStartTime(newStart.valueOf()); - expect(this.eventCardContainer.state.event.start).toBe(newStart.unix()) - expect(getPendingEvent()._start).toBe(newStart.unix()) - expect(this.eventCardContainer.state.event.end).toBe(oldEnd) - expect(getPendingEvent()._end).toBe(oldEnd) - }) + expect(getPendingEvent()._start).toBe(newStart.unix()) + expect(getPendingEvent()._end).toBe(oldEnd) }); it("adjusts the times to prevent invalid times", () => { stubCalendars(testCalendars()) - setNewTestEvent(() => { - const eventCard = this.eventCardContainer.refs.newEventCard; - let event = this.eventCardContainer.state.event; + setNewTestEvent() + const eventCard = this.eventCardContainer.refs.newEventCard; - const start0 = now(); - const end0 = now().add(1, 'hour'); + const start0 = now(); + const end0 = now().add(1, 'hour'); - const start1 = now().add(2, 'hours'); - const expectedEnd1 = now().add(3, 'hours'); + const start1 = now().add(2, 'hours'); + const expectedEnd1 = now().add(3, 'hours'); - const expectedStart2 = now().subtract(3, 'hours'); - const end2 = now().subtract(2, 'hours'); + const expectedStart2 = now().subtract(3, 'hours'); + const end2 = now().subtract(2, 'hours'); - // The event has the start times - expect(event.start).toBe(start0.unix()) - expect(event.end).toBe(end0.unix()) - expect(getPendingEvent()._start).toBe(start0.unix()) - expect(getPendingEvent()._end).toBe(end0.unix()) + // The event has the start times + expect(getPendingEvent()._start).toBe(start0.unix()) + expect(getPendingEvent()._end).toBe(end0.unix()) - eventCard._onChangeStartTime(start1.valueOf()); + eventCard._onChangeStartTime(start1.valueOf()); - // The event the new start time and also moved the end to match - event = this.eventCardContainer.state.event; - expect(event.start).toBe(start1.unix()) - expect(event.end).toBe(expectedEnd1.unix()) - expect(getPendingEvent()._start).toBe(start1.unix()) - expect(getPendingEvent()._end).toBe(expectedEnd1.unix()) + // The event the new start time and also moved the end to match + expect(getPendingEvent()._start).toBe(start1.unix()) + expect(getPendingEvent()._end).toBe(expectedEnd1.unix()) - eventCard._onChangeEndTime(end2.valueOf()); + eventCard._onChangeEndTime(end2.valueOf()); - // The event the new end time and also moved the start to match - event = this.eventCardContainer.state.event; - expect(event.start).toBe(expectedStart2.unix()) - expect(event.end).toBe(end2.unix()) - expect(getPendingEvent()._start).toBe(expectedStart2.unix()) - expect(getPendingEvent()._end).toBe(end2.unix()) - }) + // The event the new end time and also moved the start to match + expect(getPendingEvent()._start).toBe(expectedStart2.unix()) + expect(getPendingEvent()._end).toBe(end2.unix()) + }); + + describe("Inserting proposed times", () => { + beforeEach(() => { + const draft = this.session.draft() + spyOn(DatabaseStore, "find").andReturn(Promise.resolve(draft)); + const start = now().add(1, 'hour').unix(); + const end = now().add(2, 'hours').unix(); + this.proposals = [new Proposal({start, end})] + + runs(() => { + SchedulerActions.confirmChoices({ + proposals: this.proposals, + draftClientId: DRAFT_CLIENT_ID, + }); + }) + waitsFor(() => { + const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID); + return (metadata.proposals || []).length > 0; + }) + }); + + it("inserts proposed times on metadata", () => { + const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID); + expect(JSON.stringify(metadata.proposals)).toEqual(JSON.stringify(this.proposals)); + }); }); }); diff --git a/internal_packages/composer-scheduler/spec/scheduler-composer-button-spec.jsx b/internal_packages/composer-scheduler/spec/scheduler-composer-button-spec.jsx index e302f092d..82210458e 100644 --- a/internal_packages/composer-scheduler/spec/scheduler-composer-button-spec.jsx +++ b/internal_packages/composer-scheduler/spec/scheduler-composer-button-spec.jsx @@ -26,18 +26,11 @@ describe("SchedulerComposerButton", () => { spyOn(NylasEnv, "reportError") spyOn(NylasEnv, "showErrorDialog") spyOn(NewEventHelper, "now").andReturn(now()) - // Will eventually fill this.session + prepareDraft.call(this) - - // Note: Needs to be in a `runs` block so it happens after the async - // activities of `prepareDraft` - runs(() => { - this.schedulerBtn = ReactTestUtils.renderIntoDocument( - - ); - }) - - waitsFor(() => this.schedulerBtn._session) + this.schedulerBtn = ReactTestUtils.renderIntoDocument( + + ); }); afterEach(() => { diff --git a/internal_packages/composer-scheduler/spec/scheduler-composer-extension-spec.es6 b/internal_packages/composer-scheduler/spec/scheduler-composer-extension-spec.es6 index 8a6af11ff..b7a542e90 100644 --- a/internal_packages/composer-scheduler/spec/scheduler-composer-extension-spec.es6 +++ b/internal_packages/composer-scheduler/spec/scheduler-composer-extension-spec.es6 @@ -3,15 +3,13 @@ import { prepareDraft, setupCalendars, cleanupDraft, - DRAFT_CLIENT_ID, } from './composer-scheduler-spec-helper' import NewEventHelper from '../lib/composer/new-event-helper' import SchedulerComposerExtension from '../lib/composer/scheduler-composer-extension' -import {DatabaseStore} from 'nylas-exports'; +import {Message, Event} from 'nylas-exports'; import Proposal from '../lib/proposal' -import SchedulerActions from '../lib/scheduler-actions' const now = window.testNowMoment; @@ -57,35 +55,18 @@ describe("SchedulerComposerExtension", () => { }); }); - describe("Inserting proposed times", () => { - beforeEach(() => { - const draft = this.session.draft() - spyOn(DatabaseStore, "find").andReturn(Promise.resolve(draft)); + describe("When proposals are prsent", () => { + it("inserts the proposals into the draft body", () => { const start = now().add(1, 'hour').unix(); const end = now().add(2, 'hours').unix(); - this.proposals = [new Proposal({start, end})] - runs(() => { - SchedulerActions.confirmChoices({ - proposals: this.proposals, - draftClientId: DRAFT_CLIENT_ID, - }); + const draft = new Message({body: ''}) + draft.applyPluginMetadata(PLUGIN_ID, { + pendingEvent: new Event(), + proposals: [new Proposal({start, end})], }) - waitsFor(() => { - const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID); - return (metadata.proposals || []).length > 0; - }) - }); - it("inserts proposed times on metadata", () => { - const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID); - expect(metadata.proposals).toBe(this.proposals); - }); - - it("inserts the proposals into the draft body", () => { - const nextDraft = SchedulerComposerExtension.applyTransformsToDraft({ - draft: this.session.draft(), - }); + const nextDraft = SchedulerComposerExtension.applyTransformsToDraft({draft}); expect(nextDraft.body).not.toMatch(/new-event-preview/); expect(nextDraft.body).toMatch(/proposed-time-table/); expect(nextDraft.body).toMatch(/1:00 PM — 2:00 PM/); diff --git a/internal_packages/composer-templates/lib/template-status-bar.jsx b/internal_packages/composer-templates/lib/template-status-bar.jsx index 7c2565fcc..eb955270a 100644 --- a/internal_packages/composer-templates/lib/template-status-bar.jsx +++ b/internal_packages/composer-templates/lib/template-status-bar.jsx @@ -1,53 +1,26 @@ -import {DraftStore, React} from 'nylas-exports'; +import {React} from 'nylas-exports'; class TemplateStatusBar extends React.Component { static displayName = 'TemplateStatusBar'; static propTypes = { - draftClientId: React.PropTypes.string, + draft: React.PropTypes.object.isRequired, }; constructor() { super(); - this.state = { draft: null }; } - componentDidMount() { - DraftStore.sessionForClientId(this.props.draftClientId).then((_proxy)=> { - if (this._unmounted) { - return; - } - if (_proxy.draftClientId === this.props.draftClientId) { - this._proxy = _proxy; - this.unsubscribe = this._proxy.listen(this._onDraftChange.bind(this), this); - this._onDraftChange(); - } - }); + shouldComponentUpdate(nextProps) { + return (this._usingTemplate(nextProps) !== this._usingTemplate(this.props)); } - componentWillUnmount() { - this._unmounted = true; - if (this.unsubscribe) this.unsubscribe(); - } - - static containerStyles = { - textAlign: 'center', - width: 580, - margin: 'auto', - }; - - _onDraftChange() { - this.setState({draft: this._proxy.draft()}); - } - - _draftUsesTemplate() { - if (this.state.draft) { - return this.state.draft.body.search(/]*class="var[^>]*>/i) > 0; - } + _usingTemplate({draft}) { + return draft && draft.body.search(/]*class="var[^>]*>/i) > 0; } render() { - if (this._draftUsesTemplate()) { + if (this._usingTemplate(this.props)) { return (
Press "tab" to quickly move between the blanks - highlighting will not be visible to recipients. @@ -59,4 +32,10 @@ class TemplateStatusBar extends React.Component { } +TemplateStatusBar.containerStyles = { + textAlign: 'center', + width: 580, + margin: 'auto', +}; + export default TemplateStatusBar; diff --git a/internal_packages/composer-translate/lib/main.jsx b/internal_packages/composer-translate/lib/main.jsx index d8adb55f1..866af57b2 100644 --- a/internal_packages/composer-translate/lib/main.jsx +++ b/internal_packages/composer-translate/lib/main.jsx @@ -11,7 +11,6 @@ import { ReactDOM, ComponentRegistry, QuotedHTMLTransformer, - DraftStore, Actions, } from 'nylas-exports'; @@ -44,9 +43,16 @@ class TranslateButton extends React.Component { // we receive the local id of the current draft as a `prop` (a read-only // property). Since our code depends on this prop, we mark it as a requirement. static propTypes = { - draftClientId: React.PropTypes.string.isRequired, + draft: React.PropTypes.object.isRequired, + session: React.PropTypes.object.isRequired, }; + shouldComponentUpdate(nextProps) { + // Our render method doesn't use the provided `draft`, and the draft changes + // constantly (on every keystroke!) `shouldComponentUpdate` helps keep N1 fast. + return nextProps.session !== this.props.session; + } + _onError(error) { Actions.closePopover() const dialog = require('electron').remote.dialog; @@ -59,37 +65,35 @@ class TranslateButton extends React.Component { // Obtain the session for the current draft. The draft session provides us // the draft object and also manages saving changes to the local cache and // Nilas API as multiple parts of the application touch the draft. - DraftStore.sessionForClientId(this.props.draftClientId).then((session)=> { - const draftHtml = session.draft().body; - const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml); + const draftHtml = this.props.draft.body; + const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml); - const query = { - key: YandexTranslationKey, - lang: YandexLanguages[lang], - text: text, - format: 'html', - }; + const query = { + key: YandexTranslationKey, + lang: YandexLanguages[lang], + text: text, + format: 'html', + }; - // Use Node's `request` library to perform the translation using the Yandex API. - request({url: YandexTranslationURL, qs: query}, (error, resp, data)=> { - if (resp.statusCode !== 200) { - this._onError(error); - return; - } + // Use Node's `request` library to perform the translation using the Yandex API. + request({url: YandexTranslationURL, qs: query}, (error, resp, data)=> { + if (resp.statusCode !== 200) { + this._onError(error); + return; + } - const json = JSON.parse(data); - let translated = json.text.join(''); + const json = JSON.parse(data); + let translated = json.text.join(''); - // The new text of the draft is our translated response, plus any quoted text - // that we didn't process. - translated = QuotedHTMLTransformer.appendQuotedHTML(translated, draftHtml); + // The new text of the draft is our translated response, plus any quoted text + // that we didn't process. + translated = QuotedHTMLTransformer.appendQuotedHTML(translated, draftHtml); - // To update the draft, we add the new body to it's session. The session object - // automatically marshalls changes to the database and ensures that others accessing - // the same draft are notified of changes. - session.changes.add({body: translated}); - session.changes.commit(); - }); + // To update the draft, we add the new body to it's session. The session object + // automatically marshalls changes to the database and ensures that others accessing + // the same draft are notified of changes. + this.props.session.changes.add({body: translated}); + this.props.session.changes.commit(); }); }; diff --git a/internal_packages/composer/lib/composer-view.jsx b/internal_packages/composer/lib/composer-view.jsx index 0aef0a1ae..7cdf95ca7 100644 --- a/internal_packages/composer/lib/composer-view.jsx +++ b/internal_packages/composer/lib/composer-view.jsx @@ -239,7 +239,12 @@ export default class ComposerView extends React.Component {
); @@ -311,7 +316,13 @@ export default class ComposerView extends React.Component { + exposedProps={{ + draft: this.props.draft, + threadId: this.props.draft.threadId, + draftClientId: this.props.draft.clientId, + session: this.props.session, + }} + /> @@ -105,8 +111,4 @@ class SendLaterButton extends Component { } } -SendLaterButton.containerStyles = { - order: -99, -}; - -export default SendLaterButton; +SendLaterButton.containerRequired = false diff --git a/internal_packages/send-later/lib/send-later-popover.jsx b/internal_packages/send-later/lib/send-later-popover.jsx index ef5585c45..a54aee385 100644 --- a/internal_packages/send-later/lib/send-later-popover.jsx +++ b/internal_packages/send-later/lib/send-later-popover.jsx @@ -20,7 +20,7 @@ class SendLaterPopover extends Component { static displayName = 'SendLaterPopover'; static propTypes = { - scheduledDate: PropTypes.string, + sendLaterDate: PropTypes.string, onSendLater: PropTypes.func.isRequired, onCancelSendLater: PropTypes.func.isRequired, }; @@ -71,7 +71,7 @@ class SendLaterPopover extends Component { onSubmitDate={this.onSelectCustomOption} />, ]; - if (this.props.scheduledDate) { + if (this.props.sendLaterDate) { footerComponents.push(
) footerComponents.push(
diff --git a/internal_packages/send-later/spec/send-later-button-spec.jsx b/internal_packages/send-later/spec/send-later-button-spec.jsx index 56da2bc11..4c7c8012f 100644 --- a/internal_packages/send-later/spec/send-later-button-spec.jsx +++ b/internal_packages/send-later/spec/send-later-button-spec.jsx @@ -1,98 +1,66 @@ import React from 'react'; -import {findDOMNode} from 'react-dom'; +import ReactDOM from 'react-dom'; import {findRenderedDOMComponentWithClass} from 'react-addons-test-utils'; -import {Rx, DatabaseStore, DateUtils} from 'nylas-exports' +import {DateUtils} from 'nylas-exports' import SendLaterButton from '../lib/send-later-button'; import SendLaterActions from '../lib/send-later-actions'; -import {renderIntoDocument} from '../../../spec/nylas-test-utils' -const makeButton = (props = {})=> { - const button = renderIntoDocument(); - if (props.initialState) { - button.setState(props.initialState) +const node = document.createElement('div'); + +const makeButton = (initialState, metadataValue)=> { + const message = { + metadataForPluginId: ()=> metadataValue, + } + const button = ReactDOM.render(, node); + if (initialState) { + button.setState(initialState) } return button }; describe('SendLaterButton', ()=> { beforeEach(()=> { - spyOn(DatabaseStore, 'findBy') - spyOn(Rx.Observable, 'fromQuery').andReturn(Rx.Observable.empty()) spyOn(DateUtils, 'format').andReturn('formatted') spyOn(SendLaterActions, 'sendLater') }); - describe('onMessageChanged', ()=> { - it('sets scheduled date correctly', ()=> { - const button = makeButton({initialState: {scheduledDate: 'old'}}) - const message = { - metadataForPluginId: ()=> ({sendLaterDate: 'date'}), - } - spyOn(button, 'setState') - spyOn(NylasEnv, 'isComposerWindow').andReturn(false) - - button.onMessageChanged(message) - - expect(button.setState).toHaveBeenCalledWith({scheduledDate: 'date'}) - }); - + describe('componentWillReceiveProps', ()=> { it('closes window if window is composer window and saving has finished', ()=> { - const button = makeButton({initialState: {scheduledDate: 'saving'}}) - const message = { - metadataForPluginId: ()=> ({sendLaterDate: 'date'}), - } - spyOn(button, 'setState') + makeButton({saving: true}, null) spyOn(NylasEnv, 'close') spyOn(NylasEnv, 'isComposerWindow').andReturn(true) - - button.onMessageChanged(message) - - expect(button.setState).toHaveBeenCalledWith({scheduledDate: 'date'}) + makeButton(null, {sendLaterDate: 'date'}) expect(NylasEnv.close).toHaveBeenCalled() }); - - it('does nothing if new date is the same as current date', ()=> { - const button = makeButton({initialState: {scheduledDate: 'date'}}) - const message = { - metadataForPluginId: ()=> ({sendLaterDate: 'date'}), - } - spyOn(button, 'setState') - - button.onMessageChanged(message) - - expect(button.setState).not.toHaveBeenCalled() - }); }); describe('onSendLater', ()=> { it('sets scheduled date to "saving" and dispatches action', ()=> { - const button = makeButton() + const button = makeButton(null, {sendLaterDate: 'date'}) spyOn(button, 'setState') button.onSendLater({utc: ()=> 'utc'}) expect(SendLaterActions.sendLater).toHaveBeenCalled() - expect(button.setState).toHaveBeenCalledWith({scheduledDate: 'saving'}) + expect(button.setState).toHaveBeenCalledWith({saving: true}) }); }); describe('render', ()=> { it('renders spinner if saving', ()=> { - const button = findDOMNode( - makeButton({initialState: {scheduledDate: 'saving'}}) - ) + const button = ReactDOM.findDOMNode(makeButton({saving: true}, null)) expect(button.title).toEqual('Saving send date...') }); it('renders date if message is scheduled', ()=> { spyOn(DateUtils, 'futureDateFromString').andReturn({fromNow: ()=> '5 minutes'}) - const button = makeButton({initialState: {scheduledDate: 'date'}}) - const span = findDOMNode(findRenderedDOMComponentWithClass(button, 'at')) + const button = makeButton(null, {sendLaterDate: 'date'}) + const span = ReactDOM.findDOMNode(findRenderedDOMComponentWithClass(button, 'at')) expect(span.textContent).toEqual('Sending in 5 minutes') }); it('does not render date if message is not scheduled', ()=> { - const button = makeButton({initialState: {scheduledDate: null}}) + const button = makeButton(null, null) expect(()=> { findRenderedDOMComponentWithClass(button, 'at') }).toThrow() diff --git a/internal_packages/send-later/spec/send-later-popover-spec.jsx b/internal_packages/send-later/spec/send-later-popover-spec.jsx index 708a79452..f0cb3f142 100644 --- a/internal_packages/send-later/spec/send-later-popover-spec.jsx +++ b/internal_packages/send-later/spec/send-later-popover-spec.jsx @@ -9,7 +9,7 @@ import {renderIntoDocument} from '../../../spec/nylas-test-utils' const makePopover = (props = {})=> { return renderIntoDocument( {}} onCancelSendLater={()=>{}} {...props} /> @@ -50,7 +50,7 @@ describe('SendLaterPopover', ()=> { describe('render', ()=> { it('renders cancel button if scheduled', ()=> { const onCancelSendLater = jasmine.createSpy('onCancelSendLater') - const popover = makePopover({onCancelSendLater, scheduledDate: 'date'}) + const popover = makePopover({onCancelSendLater, sendLaterDate: 'date'}) const button = findDOMNode( findRenderedDOMComponentWithClass(popover, 'btn-cancel') ) diff --git a/spec/stores/draft-store-proxy-spec.coffee b/spec/stores/draft-store-proxy-spec.coffee index 60cc4e27e..adf8cc5fe 100644 --- a/spec/stores/draft-store-proxy-spec.coffee +++ b/spec/stores/draft-store-proxy-spec.coffee @@ -118,7 +118,7 @@ describe "DraftStoreProxy", -> it "prepare should resolve without querying for the draft", -> waitsForPromise => @proxy.prepare().then => - expect(@proxy.draft()).toEqual(@draft) + expect(@proxy.draft()).toBeDefined() expect(DatabaseStore.run).not.toHaveBeenCalled() describe "teardown", -> diff --git a/src/components/metadata-composer-toggle-button.jsx b/src/components/metadata-composer-toggle-button.jsx index 15d81addc..9b5488c6f 100644 --- a/src/components/metadata-composer-toggle-button.jsx +++ b/src/components/metadata-composer-toggle-button.jsx @@ -1,6 +1,7 @@ -import {DraftStore, React, Actions, NylasAPI, APIError, DatabaseStore, Message, Rx} from 'nylas-exports' +import {React, Actions, NylasAPI, APIError} from 'nylas-exports' import {RetinaImg} from 'nylas-component-kit' import classnames from 'classnames' +import _ from 'underscore' export default class MetadataComposerToggleButton extends React.Component { @@ -15,7 +16,9 @@ export default class MetadataComposerToggleButton extends React.Component { metadataEnabledValue: React.PropTypes.object.isRequired, stickyToggle: React.PropTypes.bool, errorMessage: React.PropTypes.func.isRequired, - draftClientId: React.PropTypes.string.isRequired, + + draft: React.PropTypes.object.isRequired, + session: React.PropTypes.object.isRequired, }; static defaultProps = { @@ -24,99 +27,85 @@ export default class MetadataComposerToggleButton extends React.Component { constructor(props) { super(props); + this.state = { - enabled: false, - isSetup: false, + pending: false, }; } - componentDidMount() { - this._mounted = true; - const query = DatabaseStore.findBy(Message, {clientId: this.props.draftClientId}); - this._subscription = Rx.Observable.fromQuery(query).subscribe(this._onDraftChange) - } - - componentWillUnmount() { - this._mounted = false - this._subscription.dispose(); + componentWillMount() { + if (this._isEnabledByDefault() && !this._isEnabled()) { + this._setEnabled(true); + } } _configKey() { return `plugins.${this.props.pluginId}.defaultOn` } - _isDefaultOn() { + _isEnabled() { + const {pluginId, draft, metadataEnabledValue} = this.props; + const value = draft.metadataForPluginId(pluginId); + return _.isEqual(value, metadataEnabledValue) || _.isMatch(value, metadataEnabledValue); + } + + _isEnabledByDefault() { return NylasEnv.config.get(this._configKey()) } - _onDraftChange = (draft)=> { - if (!this._mounted || !draft) { return; } - const metadata = draft.metadataForPluginId(this.props.pluginId); - if (!metadata) { - if (!this.state.isSetup) { - if (this._isDefaultOn()) { - this._setEnabled(true) - } - this.setState({isSetup: true}) - } - } else { - this.setState({enabled: true, isSetup: true}); - } - }; - _setEnabled(enabled) { - const metadataValue = enabled ? this.props.metadataEnabledValue : null; - this.setState({enabled, pending: true}); + const {pluginId, pluginName, draft, session, metadataEnabledValue} = this.props; - return DraftStore.sessionForClientId(this.props.draftClientId).then((session)=> { - const draft = session.draft(); + const metadataValue = enabled ? metadataEnabledValue : null; + this.setState({pending: true}); - return NylasAPI.authPlugin(this.props.pluginId, this.props.pluginName, draft.accountId) - .then(() => { - Actions.setMetadata(draft, this.props.pluginId, metadataValue); - }) - .catch((error) => { - this.setState({enabled: false}); + NylasAPI.authPlugin(pluginId, pluginName, draft.accountId) + .then(() => { + session.changes.addPluginMetadata(pluginId, metadataValue); + }) + .catch((error) => { + const {stickyToggle, errorMessage} = this.props; - if (this.props.stickyToggle) { - NylasEnv.config.set(this._configKey(), false) - } + if (stickyToggle) { + NylasEnv.config.set(this._configKey(), false) + } - let title = "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" - } + let title = "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" + } - NylasEnv.showErrorDialog({title, message: this.props.errorMessage(error)}); - }) + NylasEnv.showErrorDialog({title, message: errorMessage(error)}); }).finally(() => { this.setState({pending: false}) }); } _onClick = () => { - // Toggle. if (this.state.pending) { return; } - const dir = this.state.enabled ? "Disabled" : "Enabled" + + const enabled = this._isEnabled(); + const dir = enabled ? "Disabled" : "Enabled" Actions.recordUserEvent(`${this.props.pluginName} ${dir}`) if (this.props.stickyToggle) { NylasEnv.config.set(this._configKey(), !this.state.enabled) } - this._setEnabled(!this.state.enabled) + this._setEnabled(!enabled); }; render() { - const title = this.props.title(this.state.enabled) + const enabled = this._isEnabled(); + const title = this.props.title(enabled); const className = classnames({ "btn": true, "btn-toolbar": true, "btn-pending": this.state.pending, - "btn-enabled": this.state.enabled, + "btn-enabled": enabled, }); const attrs = {} diff --git a/src/flux/models/model-with-metadata.es6 b/src/flux/models/model-with-metadata.es6 index 1bef007cb..cfcd6f9ea 100644 --- a/src/flux/models/model-with-metadata.es6 +++ b/src/flux/models/model-with-metadata.es6 @@ -76,7 +76,7 @@ export default class ModelWithMetadata extends Model { if (!metadata) { return null; } - return metadata.value; + return JSON.parse(JSON.stringify(metadata.value)); } // Private helpers diff --git a/src/flux/stores/draft-store-proxy.coffee b/src/flux/stores/draft-store-proxy.coffee index 3df4bffb5..f2755c7f2 100644 --- a/src/flux/stores/draft-store-proxy.coffee +++ b/src/flux/stores/draft-store-proxy.coffee @@ -26,7 +26,7 @@ DraftChangeSet associated with the store proxy. The DraftChangeSet does two thin Section: Drafts ### class DraftChangeSet - constructor: (@_onTrigger, @_onCommit) -> + constructor: (@_onAltered, @_onCommit) -> @_commitChain = Promise.resolve() @_pending = {} @_saving = {} @@ -39,10 +39,10 @@ class DraftChangeSet clearTimeout(@_timer) @_timer = null - add: (changes, {silent} = {}) => + add: (changes) => @_pending = _.extend(@_pending, changes) @_pending['pristine'] = false - @_onTrigger() unless silent + @_onAltered() clearTimeout(@_timer) if @_timer @_timer = setTimeout(@commit, 10000) @@ -101,7 +101,7 @@ class DraftStoreProxy @_draftPristineBody = null @_destroyed = false - @changes = new DraftChangeSet(@_changeSetTrigger, @_changeSetCommit) + @changes = new DraftChangeSet(@_changeSetAltered, @_changeSetCommit) if draft @_draftPromise = @_setDraft(draft) @@ -113,7 +113,7 @@ class DraftStoreProxy draft: -> return null if not @_draft @changes.applyToModel(@_draft) - @_draft + @_draft.clone() # Public: Returns the initial body of the draft when it was pristine, or null if the # draft was never pristine in this editing session. Useful for determining if the @@ -163,13 +163,15 @@ class DraftStoreProxy # Is this change an update to our draft? myDrafts = _.filter(change.objects, (obj) => obj.clientId is @_draft.clientId) if myDrafts.length > 0 - @_draft = _.extend @_draft, _.last(myDrafts) + @_draft = Object.assign(new Message(), @_draft, myDrafts.pop()) @trigger() - _changeSetTrigger: => + _changeSetAltered: => return if @_destroyed if !@_draft throw new Error("DraftChangeSet was modified before the draft was prepared.") + + @changes.applyToModel(@_draft) @trigger() _changeSetCommit: ({noSyncback}={}) => diff --git a/static/package-template/lib/my-composer-button.jsx b/static/package-template/lib/my-composer-button.jsx index fbf8a7541..62551d9b0 100644 --- a/static/package-template/lib/my-composer-button.jsx +++ b/static/package-template/lib/my-composer-button.jsx @@ -1,4 +1,4 @@ -import {DraftStore, React} from 'nylas-exports'; +import {React} from 'nylas-exports'; export default class MyComposerButton extends React.Component { @@ -10,26 +10,33 @@ export default class MyComposerButton extends React.Component { // reference to the draft, and you can look it up to perform // actions and retrieve data. static propTypes = { - draftClientId: React.PropTypes.string.isRequired, + draft: React.PropTypes.object.isRequired, + session: React.PropTypes.object.isRequired, }; + shouldComponentUpdate(nextProps) { + // Our render method doesn't use the provided `draft`, and the draft changes + // constantly (on every keystroke!) `shouldComponentUpdate` helps keep N1 fast. + return nextProps.session !== this.props.session; + } + _onClick = () => { + const {session, draft} = this.props; + // To retrieve information about the draft, we fetch the current editing // session from the draft store. We can access attributes of the draft // and add changes to the session which will be appear immediately. - DraftStore.sessionForClientId(this.props.draftClientId).then((session) => { - const newSubject = `${session.draft().subject} - It Worked!`; + const newSubject = `${draft.subject} - It Worked!`; - const dialog = this._getDialog(); - dialog.showMessageBox({ - title: 'Here we go...', - detail: `Adjusting the subject line To "${newSubject}"`, - buttons: ['OK'], - type: 'info', - }); - - session.changes.add({subject: newSubject}); + const dialog = this._getDialog(); + dialog.showMessageBox({ + title: 'Here we go...', + detail: `Adjusting the subject line To "${newSubject}"`, + buttons: ['OK'], + type: 'info', }); + + session.changes.add({subject: newSubject}); } _getDialog() {