diff --git a/build/config/eslint.json b/build/config/eslint.json
index 992ea2ddc..54f00c671 100644
--- a/build/config/eslint.json
+++ b/build/config/eslint.json
@@ -2,7 +2,8 @@
"extends": "airbnb",
"globals": {
"NylasEnv": false,
- "$n": false
+ "$n": false,
+ "waitsForPromise": false
},
"env": {
"browser": true,
diff --git a/internal_packages/send-later/lib/main.js b/internal_packages/send-later/lib/main.js
index bbed9857c..580c07422 100644
--- a/internal_packages/send-later/lib/main.js
+++ b/internal_packages/send-later/lib/main.js
@@ -5,7 +5,9 @@ import SendLaterStore from './send-later-store'
import SendLaterStatus from './send-later-status'
export function activate() {
- SendLaterStore.activate()
+ this.store = new SendLaterStore()
+
+ this.store.activate()
ComponentRegistry.register(SendLaterPopover, {role: 'Composer:ActionButton'})
ComponentRegistry.register(SendLaterStatus, {role: 'DraftList:DraftStatus'})
}
@@ -13,7 +15,7 @@ export function activate() {
export function deactivate() {
ComponentRegistry.unregister(SendLaterPopover)
ComponentRegistry.unregister(SendLaterStatus)
- SendLaterStore.deactivate()
+ this.store.deactivate()
}
export function serialize() {
diff --git a/internal_packages/send-later/lib/send-later-popover.jsx b/internal_packages/send-later/lib/send-later-popover.jsx
index d62ebd6eb..59de3c7e0 100644
--- a/internal_packages/send-later/lib/send-later-popover.jsx
+++ b/internal_packages/send-later/lib/send-later-popover.jsx
@@ -2,10 +2,9 @@
import Rx from 'rx-lite'
import React, {Component, PropTypes} from 'react'
import {DateUtils, Message, DatabaseStore} from 'nylas-exports'
-import {Popover, RetinaImg, Menu} from 'nylas-component-kit'
+import {Popover, RetinaImg, Menu, DateInput} from 'nylas-component-kit'
import SendLaterActions from './send-later-actions'
-import SendLaterStore from './send-later-store'
-import {DATE_FORMAT_SHORT, DATE_FORMAT_LONG} from './send-later-constants'
+import {DATE_FORMAT_SHORT, DATE_FORMAT_LONG, PLUGIN_ID} from './send-later-constants'
const SendLaterOptions = {
@@ -28,7 +27,6 @@ class SendLaterPopover extends Component {
constructor(props) {
super(props)
this.state = {
- inputDate: null,
scheduledDate: null,
}
}
@@ -36,85 +34,52 @@ class SendLaterPopover extends Component {
componentDidMount() {
this._subscription = Rx.Observable.fromQuery(
DatabaseStore.findBy(Message, {clientId: this.props.draftClientId})
- ).subscribe((draft)=> {
- const nextScheduledDate = SendLaterStore.getScheduledDateForMessage(draft);
-
- if (nextScheduledDate !== this.state.scheduledDate) {
- const isPopout = (NylasEnv.getWindowType() === "composer");
- const isFinishedSelecting = ((this.state.scheduledDate === 'saving') && (nextScheduledDate !== null));
- if (isPopout && isFinishedSelecting) {
- NylasEnv.close();
- }
- this.setState({scheduledDate: nextScheduledDate});
- }
- });
+ ).subscribe(this.onMessageChanged);
}
componentWillUnmount() {
this._subscription.dispose();
}
- onSelectMenuOption = (optionKey)=> {
- const date = SendLaterOptions[optionKey]();
- this.onSelectDate(date, optionKey);
- };
+ onMessageChanged = (message)=> {
+ if (!message) return;
+ const messageMetadata = message.metadataForPluginId(PLUGIN_ID) || {}
+ const nextScheduledDate = messageMetadata.sendLaterDate
- onSelectCustomOption = (value)=> {
- const date = DateUtils.futureDateFromString(value);
- if (date) {
- this.onSelectDate(date, "Custom");
- } else {
- NylasEnv.showErrorDialog(`Sorry, we can't parse ${value} as a valid date.`);
+ if (nextScheduledDate !== this.state.scheduledDate) {
+ const isComposer = NylasEnv.isComposerWindow()
+ const isFinishedSelecting = ((this.state.scheduledDate === 'saving') && (nextScheduledDate !== null));
+ if (isComposer && isFinishedSelecting) {
+ NylasEnv.close();
+ }
+ this.setState({scheduledDate: nextScheduledDate});
}
};
- onSelectDate = (date, label)=> {
- const formatted = DateUtils.format(date.utc());
- SendLaterActions.sendLater(this.props.draftClientId, formatted, label);
- this.setState({scheduledDate: 'saving', inputDate: null});
- this.refs.popover.close();
+ onSelectMenuOption = (optionKey)=> {
+ const date = SendLaterOptions[optionKey]();
+ this.selectDate(date, optionKey);
+ };
+
+ onSelectCustomOption = (date, inputValue)=> {
+ if (date) {
+ this.selectDate(date, "Custom");
+ } else {
+ NylasEnv.showErrorDialog(`Sorry, we can't parse ${inputValue} as a valid date.`);
+ }
};
onCancelSendLater = ()=> {
SendLaterActions.cancelSendLater(this.props.draftClientId);
- this.setState({inputDate: null});
this.refs.popover.close();
};
- renderCustomTimeSection() {
- const onChange = (event)=> {
- this.setState({inputDate: DateUtils.futureDateFromString(event.target.value)});
- }
-
- const onKeyDown = (event)=> {
- // we need to swallow these events so they don't reach the menu
- // containing the text input, but only when you've typed something.
- const val = event.target.value;
- if ((val.length > 0) && ["Enter", "Return"].includes(event.key)) {
- this.onSelectCustomOption(val);
- event.stopPropagation();
- }
- };
-
- let dateInterpretation = false;
- if (this.state.inputDate) {
- dateInterpretation = (
- {DateUtils.format(this.state.inputDate, DATE_FORMAT_LONG)}
- );
- }
-
- return (
-
-
- {dateInterpretation}
-
- )
- }
+ selectDate = (date, dateLabel)=> {
+ const formatted = DateUtils.format(date.utc());
+ SendLaterActions.sendLater(this.props.draftClientId, formatted, dateLabel);
+ this.setState({scheduledDate: 'saving'});
+ this.refs.popover.close();
+ };
renderMenuOption(optionKey) {
const date = SendLaterOptions[optionKey]();
@@ -139,7 +104,7 @@ class SendLaterPopover extends Component {
);
}
- let dateInterpretation = false;
+ let dateInterpretation;
if (scheduledDate) {
className += ' btn-enabled';
const momentDate = DateUtils.futureDateFromString(scheduledDate);
@@ -163,7 +128,11 @@ class SendLaterPopover extends Component {
]
const footerComponents = [
,
- this.renderCustomTimeSection(),
+ ,
];
if (this.state.scheduledDate) {
diff --git a/internal_packages/send-later/lib/send-later-store.js b/internal_packages/send-later/lib/send-later-store.js
index 07ea9bd1c..2d8532b2d 100644
--- a/internal_packages/send-later/lib/send-later-store.js
+++ b/internal_packages/send-later/lib/send-later-store.js
@@ -7,9 +7,10 @@ import {PLUGIN_ID, PLUGIN_NAME} from './send-later-constants'
class SendLaterStore extends NylasStore {
- constructor(pluginId = PLUGIN_ID) {
+ constructor(pluginId = PLUGIN_ID, pluginName = PLUGIN_NAME) {
super()
this.pluginId = pluginId;
+ this.pluginName = pluginName;
}
activate() {
@@ -19,19 +20,12 @@ class SendLaterStore extends NylasStore {
];
}
- getScheduledDateForMessage = (message)=> {
- if (!message) {
- return null;
- }
- const metadata = message.metadataForPluginId(this.pluginId) || {};
- return metadata.sendLaterDate || null;
- };
-
setMetadata = (draftClientId, metadata)=> {
- DatabaseStore.modelify(Message, [draftClientId]).then((messages)=> {
+ return DatabaseStore.modelify(Message, [draftClientId])
+ .then((messages)=> {
const {accountId} = messages[0];
- NylasAPI.authPlugin(this.pluginId, PLUGIN_NAME, accountId)
+ return NylasAPI.authPlugin(this.pluginId, this.pluginName, accountId)
.then(()=> {
Actions.setMetadata(messages, this.pluginId, metadata);
})
@@ -42,13 +36,13 @@ class SendLaterStore extends NylasStore {
});
};
- recordAction(sendLaterDate, label) {
+ recordAction(sendLaterDate, dateLabel) {
try {
if (sendLaterDate) {
const min = Math.round(((new Date(sendLaterDate)).valueOf() - Date.now()) / 1000 / 60);
Actions.recordUserEvent("Send Later", {
sendLaterTime: min,
- optionLabel: label,
+ optionLabel: dateLabel,
});
} else {
Actions.recordUserEvent("Send Later Cancel");
@@ -58,8 +52,8 @@ class SendLaterStore extends NylasStore {
}
}
- onSendLater = (draftClientId, sendLaterDate, label)=> {
- this.recordAction(sendLaterDate, label)
+ onSendLater = (draftClientId, sendLaterDate, dateLabel)=> {
+ this.recordAction(sendLaterDate, dateLabel)
this.setMetadata(draftClientId, {sendLaterDate});
};
@@ -73,5 +67,4 @@ class SendLaterStore extends NylasStore {
};
}
-
-export default new SendLaterStore()
+export default SendLaterStore
diff --git a/internal_packages/send-later/spec/send-later-popover-spec.jsx b/internal_packages/send-later/spec/send-later-popover-spec.jsx
new file mode 100644
index 000000000..478f877b3
--- /dev/null
+++ b/internal_packages/send-later/spec/send-later-popover-spec.jsx
@@ -0,0 +1,106 @@
+import React, {addons} from 'react/addons';
+import {Rx, DatabaseStore, DateUtils} from 'nylas-exports'
+import SendLaterPopover from '../lib/send-later-popover';
+import SendLaterActions from '../lib/send-later-actions';
+import {renderIntoDocument} from '../../../spec/nylas-test-utils'
+
+const {findDOMNode} = React;
+const {TestUtils: {
+ findRenderedDOMComponentWithClass,
+}} = addons;
+
+const makePopover = (props = {})=> {
+ const popover = renderIntoDocument();
+ if (props.initialState) {
+ popover.setState(props.initialState)
+ }
+ return popover
+};
+
+describe('SendLaterPopover', ()=> {
+ 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 popover = makePopover({initialState: {scheduledDate: 'old'}})
+ const message = {
+ metadataForPluginId: ()=> ({sendLaterDate: 'date'}),
+ }
+ spyOn(popover, 'setState')
+ spyOn(NylasEnv, 'isComposerWindow').andReturn(false)
+
+ popover.onMessageChanged(message)
+
+ expect(popover.setState).toHaveBeenCalledWith({scheduledDate: 'date'})
+ });
+
+ it('closes window if window is composer window and saving has finished', ()=> {
+ const popover = makePopover({initialState: {scheduledDate: 'saving'}})
+ const message = {
+ metadataForPluginId: ()=> ({sendLaterDate: 'date'}),
+ }
+ spyOn(popover, 'setState')
+ spyOn(NylasEnv, 'close')
+ spyOn(NylasEnv, 'isComposerWindow').andReturn(true)
+
+ popover.onMessageChanged(message)
+
+ expect(popover.setState).toHaveBeenCalledWith({scheduledDate: 'date'})
+ expect(NylasEnv.close).toHaveBeenCalled()
+ });
+
+ it('does nothing if new date is the same as current date', ()=> {
+ const popover = makePopover({initialState: {scheduledDate: 'date'}})
+ const message = {
+ metadataForPluginId: ()=> ({sendLaterDate: 'date'}),
+ }
+ spyOn(popover, 'setState')
+
+ popover.onMessageChanged(message)
+
+ expect(popover.setState).not.toHaveBeenCalled()
+ });
+ });
+
+ describe('selectDate', ()=> {
+ it('sets scheduled date to "saving" and dispatches action', ()=> {
+ const popover = makePopover()
+ spyOn(popover, 'setState')
+ spyOn(popover.refs.popover, 'close')
+ popover.selectDate({utc: ()=> 'utc'})
+
+ expect(SendLaterActions.sendLater).toHaveBeenCalled()
+ expect(popover.setState).toHaveBeenCalledWith({scheduledDate: 'saving'})
+ expect(popover.refs.popover.close).toHaveBeenCalled()
+ });
+ });
+
+ describe('renderButton', ()=> {
+ it('renders spinner if saving', ()=> {
+ const popover = makePopover({initialState: {scheduledDate: 'saving'}})
+ const button = findDOMNode(
+ findRenderedDOMComponentWithClass(popover, 'btn-send-later')
+ )
+ expect(button.title).toEqual('Saving send date...')
+ });
+
+ it('renders date if message is scheduled', ()=> {
+ spyOn(DateUtils, 'futureDateFromString').andReturn({fromNow: ()=> '5 minutes'})
+ const popover = makePopover({initialState: {scheduledDate: 'date'}})
+ const span = findDOMNode(findRenderedDOMComponentWithClass(popover, 'at'))
+ expect(span.textContent).toEqual('Sending in 5 minutes')
+ });
+
+ it('does not render date if message is not scheduled', ()=> {
+ const popover = makePopover({initialState: {scheduledDate: null}})
+ expect(()=> {
+ findRenderedDOMComponentWithClass(popover, 'at')
+ }).toThrow()
+ });
+ });
+});
diff --git a/internal_packages/send-later/spec/send-later-store-spec.es6 b/internal_packages/send-later/spec/send-later-store-spec.es6
new file mode 100644
index 000000000..4290bd372
--- /dev/null
+++ b/internal_packages/send-later/spec/send-later-store-spec.es6
@@ -0,0 +1,68 @@
+import {
+ Message,
+ NylasAPI,
+ Actions,
+ DatabaseStore,
+} from 'nylas-exports'
+import SendLaterStore from '../lib/send-later-store'
+
+
+describe('SendLaterStore', ()=> {
+ beforeEach(()=> {
+ this.store = new SendLaterStore('plug-id', 'plug-name')
+ });
+
+ describe('setMetadata', ()=> {
+ beforeEach(()=> {
+ this.message = new Message({accountId: 123, clientId: 'c-1'})
+ this.metadata = {sendLaterDate: 'the future'}
+ spyOn(this.store, 'recordAction')
+ spyOn(DatabaseStore, 'modelify').andReturn(Promise.resolve([this.message]))
+ spyOn(NylasAPI, 'authPlugin').andReturn(Promise.resolve())
+ spyOn(Actions, 'setMetadata')
+ spyOn(NylasEnv, 'reportError')
+ spyOn(NylasEnv, 'showErrorDialog')
+ });
+
+ it('auths the plugin correctly', ()=> {
+ waitsForPromise(()=> {
+ return this.store.setMetadata('c-1', this.metadata)
+ .then(()=> {
+ expect(NylasAPI.authPlugin).toHaveBeenCalled()
+ expect(NylasAPI.authPlugin).toHaveBeenCalledWith(
+ 'plug-id',
+ 'plug-name',
+ 123
+ )
+ })
+ })
+ });
+
+ it('sets the correct metadata', ()=> {
+ waitsForPromise(()=> {
+ return this.store.setMetadata('c-1', this.metadata)
+ .then(()=> {
+ expect(Actions.setMetadata).toHaveBeenCalledWith(
+ [this.message],
+ 'plug-id',
+ this.metadata
+ )
+ expect(NylasEnv.reportError).not.toHaveBeenCalled()
+ })
+ })
+ });
+
+ it('displays dialog if an error occurs', ()=> {
+ jasmine.unspy(NylasAPI, 'authPlugin')
+ spyOn(NylasAPI, 'authPlugin').andReturn(Promise.reject(new Error('Oh no!')))
+ waitsForPromise(()=> {
+ return this.store.setMetadata('c-1', this.metadata)
+ .finally(()=> {
+ expect(Actions.setMetadata).not.toHaveBeenCalled()
+ expect(NylasEnv.reportError).toHaveBeenCalled()
+ expect(NylasEnv.showErrorDialog).toHaveBeenCalled()
+ })
+ })
+ });
+ });
+});
diff --git a/internal_packages/send-later/stylesheets/send-later.less b/internal_packages/send-later/stylesheets/send-later.less
index 026ddb788..ebd3ea30b 100644
--- a/internal_packages/send-later/stylesheets/send-later.less
+++ b/internal_packages/send-later/stylesheets/send-later.less
@@ -1,12 +1,6 @@
@import "ui-variables";
-
.send-later {
- .time {
- font-size: @font-size-small;
- opacity: 0.6;
- }
-
.menu {
width: 250px;
.item {
@@ -26,11 +20,8 @@
.divider {
border-top: 1px solid @border-color-divider;
}
- .custom-time-section {
+ .custom-section {
padding: @padding-base-vertical * 1.5 @padding-base-horizontal;
- .time {
- color: @text-color-subtle;
- }
}
.cancel-section {
padding: @padding-base-vertical @padding-base-horizontal;
diff --git a/internal_packages/thread-snooze/lib/main.js b/internal_packages/thread-snooze/lib/main.js
index 6b2775db6..947c7e74b 100644
--- a/internal_packages/thread-snooze/lib/main.js
+++ b/internal_packages/thread-snooze/lib/main.js
@@ -8,6 +8,8 @@ import SnoozeStore from './snooze-store'
export function activate() {
this.snoozeStore = new SnoozeStore()
+
+ this.snoozeStore.activate()
ComponentRegistry.register(ToolbarSnooze, {role: 'message:Toolbar'});
ComponentRegistry.register(SnoozeQuickActionButton, {role: 'ThreadListQuickAction'});
ComponentRegistry.register(BulkThreadSnooze, {role: 'thread:BulkAction'});
diff --git a/internal_packages/thread-snooze/lib/snooze-mail-label.jsx b/internal_packages/thread-snooze/lib/snooze-mail-label.jsx
index f855224b0..14332b2cb 100644
--- a/internal_packages/thread-snooze/lib/snooze-mail-label.jsx
+++ b/internal_packages/thread-snooze/lib/snooze-mail-label.jsx
@@ -2,7 +2,7 @@ import _ from 'underscore';
import React, {Component, PropTypes} from 'react';
import {RetinaImg, MailLabel} from 'nylas-component-kit';
import {SNOOZE_CATEGORY_NAME, PLUGIN_ID} from './snooze-constants';
-import {snoozeMessage} from './snooze-utils';
+import {snoozedUntilMessage} from './snooze-utils';
class SnoozeMailLabel extends Component {
@@ -21,7 +21,7 @@ class SnoozeMailLabel extends Component {
if (metadata) {
// TODO this is such a hack
const {snoozeDate} = metadata;
- const message = snoozeMessage(snoozeDate).replace('Snoozed', '')
+ const message = snoozedUntilMessage(snoozeDate).replace('Snoozed', '')
const content = (
{
- const inputDate = DateUtils.futureDateFromString(event.target.value)
- this.setState({inputDate})
- };
-
- onInputKeyDown = (event)=> {
- const {value} = event.target;
- if (value.length > 0 && ["Enter", "Return"].includes(event.key)) {
- const inputDate = DateUtils.futureDateFromString(value);
- if (inputDate) {
- this.onSnooze(() => {return inputDate}, "Custom");
- // Prevent onInputChange from firing
- event.stopPropagation()
- }
+ onSelectCustomDate = (date, inputValue)=> {
+ if (date) {
+ this.onSnooze(date, "Custom");
+ } else {
+ NylasEnv.showErrorDialog(`Sorry, we can't parse ${inputValue} as a valid date.`);
}
};
-
- renderItem = (label)=> {
- const dateGenerator = SnoozeDateGenerators[label];
- const iconName = SnoozeIconNames[label];
+ renderItem = (itemLabel)=> {
+ const date = SnoozeDatesFactory[itemLabel]();
+ const iconName = SnoozeIconNames[itemLabel];
const iconPath = `nylas://thread-snooze/assets/ic-snoozepopover-${iconName}@2x.png`;
return (
+ onClick={this.onSnooze.bind(this, date, itemLabel)}>
- {label}
+ {itemLabel}
)
};
@@ -121,32 +108,16 @@ class SnoozePopoverBody extends Component {
);
};
- renderInputRow = (inputDate)=> {
- let formatted = null;
- if (inputDate) {
- formatted = 'Snooze until ' + DateUtils.format(inputDate, DATE_FORMAT_LONG);
- }
- return (
-
-
- {formatted}
-
- );
- };
-
render() {
- const {inputDate} = this.state;
const rows = SnoozeOptions.map(this.renderRow);
return (
{rows}
- {this.renderInputRow(inputDate)}
+
);
}
diff --git a/internal_packages/thread-snooze/lib/snooze-store.js b/internal_packages/thread-snooze/lib/snooze-store.js
index feae7cdd3..cc747e87f 100644
--- a/internal_packages/thread-snooze/lib/snooze-store.js
+++ b/internal_packages/thread-snooze/lib/snooze-store.js
@@ -1,20 +1,28 @@
/** @babel */
import _ from 'underscore';
-import {Actions, NylasAPI, AccountStore} from 'nylas-exports';
-import {moveThreadsToSnooze, moveThreadsFromSnooze} from './snooze-utils';
+import {Actions, NylasAPI, AccountStore, CategoryStore} from 'nylas-exports';
+import {
+ moveThreadsToSnooze,
+ moveThreadsFromSnooze,
+ getSnoozeCategoriesByAccount,
+} from './snooze-utils';
import {PLUGIN_ID, PLUGIN_NAME} from './snooze-constants';
import SnoozeActions from './snooze-actions';
class SnoozeStore {
- constructor(pluginId = PLUGIN_ID) {
+ constructor(pluginId = PLUGIN_ID, pluginName = PLUGIN_NAME) {
this.pluginId = pluginId
+ this.pluginName = pluginName
+ this.snoozeCategoriesPromise = getSnoozeCategoriesByAccount()
+ }
+ activate() {
this.unsubscribe = SnoozeActions.snoozeThreads.listen(this.onSnoozeThreads)
}
- onSnoozeThreads = (threads, snoozeDate, label) => {
+ recordSnoozeEvent(threads, snoozeDate, label) {
try {
const min = Math.round(((new Date(snoozeDate)).valueOf() - Date.now()) / 1000 / 60);
Actions.recordUserEvent("Snooze Threads", {
@@ -25,23 +33,51 @@ class SnoozeStore {
} catch (e) {
// Do nothing
}
+ }
+
+ groupUpdatedThreads = (threads, snoozeCategoriesByAccount) => {
+ const getSnoozeCategory = (accId)=> snoozeCategoriesByAccount[accId]
+ const {getInboxCategory} = CategoryStore
+ const threadsByAccountId = {}
+
+ threads.forEach((thread)=> {
+ const accId = thread.accountId
+ if (!threadsByAccountId[accId]) {
+ threadsByAccountId[accId] = {
+ threads: [thread],
+ snoozeCategoryId: getSnoozeCategory(accId).serverId,
+ returnCategoryId: getInboxCategory(accId).serverId,
+ }
+ } else {
+ threadsByAccountId[accId].threads.push(thread);
+ }
+ });
+ return Promise.resolve(threadsByAccountId);
+ };
+
+ onSnoozeThreads = (threads, snoozeDate, label) => {
+ this.recordSnoozeEvent(threads, label)
const accounts = AccountStore.accountsForItems(threads)
const promises = accounts.map((acc)=> {
- return NylasAPI.authPlugin(this.pluginId, PLUGIN_NAME, acc)
+ return NylasAPI.authPlugin(this.pluginId, this.pluginName, acc)
})
- Promise.all(promises)
+ return Promise.all(promises)
.then(()=> {
- return moveThreadsToSnooze(threads, snoozeDate)
+ return moveThreadsToSnooze(threads, this.snoozeCategoriesPromise, snoozeDate)
+ })
+ .then((updatedThreads)=> {
+ return this.snoozeCategoriesPromise
+ .then(snoozeCategories => this.groupUpdatedThreads(updatedThreads, snoozeCategories))
})
.then((updatedThreadsByAccountId)=> {
_.each(updatedThreadsByAccountId, (update)=> {
- const {updatedThreads, snoozeCategoryId, returnCategoryId} = update;
- Actions.setMetadata(updatedThreads, this.pluginId, {snoozeDate, snoozeCategoryId, returnCategoryId})
+ const {snoozeCategoryId, returnCategoryId} = update;
+ Actions.setMetadata(update.threads, this.pluginId, {snoozeDate, snoozeCategoryId, returnCategoryId})
})
})
.catch((error)=> {
- moveThreadsFromSnooze(threads)
+ moveThreadsFromSnooze(threads, this.snoozeCategoriesPromise)
Actions.closePopover();
NylasEnv.reportError(error);
NylasEnv.showErrorDialog(`Sorry, we were unable to save your snooze settings. ${error.message}`);
diff --git a/internal_packages/thread-snooze/lib/snooze-utils.js b/internal_packages/thread-snooze/lib/snooze-utils.js
index 03b65c0f4..14b2a8048 100644
--- a/internal_packages/thread-snooze/lib/snooze-utils.js
+++ b/internal_packages/thread-snooze/lib/snooze-utils.js
@@ -15,139 +15,126 @@ import {
} from 'nylas-exports';
import {SNOOZE_CATEGORY_NAME, DATE_FORMAT_SHORT} from './snooze-constants'
-export function snoozeMessage(snoozeDate) {
- let message = 'Snoozed'
- if (snoozeDate) {
- let dateFormat = DATE_FORMAT_SHORT
- const date = moment(snoozeDate)
- const now = moment()
- const hourDifference = moment.duration(date.diff(now)).asHours()
- if (hourDifference < 24) {
- dateFormat = dateFormat.replace('MMM D, ', '');
- }
- if (date.minutes() === 0) {
- dateFormat = dateFormat.replace(':mm', '');
- }
+const SnoozeUtils = {
- message += ` until ${DateUtils.format(date, dateFormat)}`;
- }
- return message;
-}
+ snoozedUntilMessage(snoozeDate, now = moment()) {
+ let message = 'Snoozed'
+ if (snoozeDate) {
+ let dateFormat = DATE_FORMAT_SHORT
+ const date = moment(snoozeDate)
+ const hourDifference = moment.duration(date.diff(now)).asHours()
-export function createSnoozeCategory(accountId, name = SNOOZE_CATEGORY_NAME) {
- const category = new Category({
- displayName: name,
- accountId: accountId,
- })
- const task = new SyncbackCategoryTask({category})
-
- Actions.queueTask(task)
- return TaskQueueStatusStore.waitForPerformRemote(task).then(()=>{
- return DatabaseStore.findBy(Category, {clientId: category.clientId})
- .then((updatedCat)=> {
- if (updatedCat && updatedCat.isSavedRemotely()) {
- return Promise.resolve(updatedCat)
+ if (hourDifference < 24) {
+ dateFormat = dateFormat.replace('MMM D, ', '');
}
- return Promise.reject(new Error('Could not create Snooze category'))
+ if (date.minutes() === 0) {
+ dateFormat = dateFormat.replace(':mm', '');
+ }
+
+ message += ` until ${DateUtils.format(date, dateFormat)}`;
+ }
+ return message;
+ },
+
+ createSnoozeCategory(accountId, name = SNOOZE_CATEGORY_NAME) {
+ const category = new Category({
+ displayName: name,
+ accountId: accountId,
})
- })
-}
+ const task = new SyncbackCategoryTask({category})
-
-export function whenCategoriesReady() {
- const categoriesReady = ()=> CategoryStore.categories().length > 0
- if (!categoriesReady()) {
- return new Promise((resolve)=> {
- const unsubscribe = CategoryStore.listen(()=> {
- if (categoriesReady()) {
- unsubscribe()
- resolve()
+ Actions.queueTask(task)
+ return TaskQueueStatusStore.waitForPerformRemote(task).then(()=>{
+ return DatabaseStore.findBy(Category, {clientId: category.clientId})
+ .then((updatedCat)=> {
+ if (updatedCat && updatedCat.isSavedRemotely()) {
+ return Promise.resolve(updatedCat)
}
+ return Promise.reject(new Error('Could not create Snooze category'))
})
})
- }
- return Promise.resolve()
-}
+ },
-
-export function getSnoozeCategory(accountId, categoryName = SNOOZE_CATEGORY_NAME) {
- return whenCategoriesReady()
- .then(()=> {
- const allCategories = CategoryStore.categories(accountId)
- const category = _.findWhere(allCategories, {displayName: categoryName})
- if (category) {
- return Promise.resolve(category);
+ whenCategoriesReady() {
+ const categoriesReady = ()=> CategoryStore.categories().length > 0
+ if (!categoriesReady()) {
+ return new Promise((resolve)=> {
+ const unsubscribe = CategoryStore.listen(()=> {
+ if (categoriesReady()) {
+ unsubscribe()
+ resolve()
+ }
+ })
+ })
}
- return createSnoozeCategory(accountId, categoryName)
- })
-}
+ return Promise.resolve()
+ },
-
-export function getSnoozeCategoriesByAccount(accounts = AccountStore.accounts()) {
- const categoriesByAccountId = {}
- accounts.forEach(({id})=> {
- if (categoriesByAccountId[id] != null) return;
- categoriesByAccountId[id] = getSnoozeCategory(id)
- })
- return Promise.props(categoriesByAccountId)
-}
-
-
-export function groupProcessedThreadsByAccountId(categoriesByAccountId, threads) {
- return DatabaseStore.modelify(Thread, _.pluck(threads, 'clientId')).then((updatedThreads)=> {
- const threadsByAccountId = {}
- updatedThreads.forEach((thread)=> {
- const accId = thread.accountId
- if (!threadsByAccountId[accId]) {
- threadsByAccountId[accId] = {
- updatedThreads: [thread],
- snoozeCategoryId: categoriesByAccountId[accId].serverId,
- returnCategoryId: CategoryStore.getInboxCategory(accId).serverId,
- }
- } else {
- threadsByAccountId[accId].updatedThreads.push(thread);
+ getSnoozeCategory(accountId, categoryName = SNOOZE_CATEGORY_NAME) {
+ return SnoozeUtils.whenCategoriesReady()
+ .then(()=> {
+ const allCategories = CategoryStore.categories(accountId)
+ const category = _.findWhere(allCategories, {displayName: categoryName})
+ if (category) {
+ return Promise.resolve(category);
}
- });
- return Promise.resolve(threadsByAccountId);
- })
-}
-
-
-export function moveThreads(threads, categoriesByAccountId, {snooze, description} = {}) {
- const inbox = CategoryStore.getInboxCategory
- const snoozeCat = (accId)=> categoriesByAccountId[accId]
- const tasks = TaskFactory.tasksForApplyingCategories({
- threads,
- categoriesToRemove: snooze ? inbox : snoozeCat,
- categoryToAdd: snooze ? snoozeCat : inbox,
- taskDescription: description,
- })
-
- Actions.queueTasks(tasks)
- const promises = tasks.map(task => TaskQueueStatusStore.waitForPerformRemote(task))
- // Resolve with the updated threads
- return (
- Promise.all(promises).then(()=> {
- return groupProcessedThreadsByAccountId(categoriesByAccountId, threads)
+ return SnoozeUtils.createSnoozeCategory(accountId, categoryName)
})
- )
+ },
+
+ getSnoozeCategoriesByAccount(accounts = AccountStore.accounts()) {
+ const snoozeCategoriesByAccountId = {}
+ accounts.forEach(({id})=> {
+ if (snoozeCategoriesByAccountId[id] != null) return;
+ snoozeCategoriesByAccountId[id] = SnoozeUtils.getSnoozeCategory(id)
+ })
+ return Promise.props(snoozeCategoriesByAccountId)
+ },
+
+ moveThreads(threads, {snooze, getSnoozeCategory, getInboxCategory, description} = {}) {
+ const tasks = TaskFactory.tasksForApplyingCategories({
+ threads,
+ categoriesToRemove: snooze ? getInboxCategory : getSnoozeCategory,
+ categoryToAdd: snooze ? getSnoozeCategory : getInboxCategory,
+ taskDescription: description,
+ })
+
+ Actions.queueTasks(tasks)
+ const promises = tasks.map(task => TaskQueueStatusStore.waitForPerformRemote(task))
+ // Resolve with the updated threads
+ return (
+ Promise.all(promises).then(()=> {
+ return DatabaseStore.modelify(Thread, _.pluck(threads, 'clientId'))
+ })
+ )
+ },
+
+ moveThreadsToSnooze(threads, snoozeCategoriesByAccountPromise, snoozeDate) {
+ return snoozeCategoriesByAccountPromise
+ .then((snoozeCategoriesByAccountId)=> {
+ const getSnoozeCategory = (accId)=> snoozeCategoriesByAccountId[accId]
+ const {getInboxCategory} = CategoryStore
+ const description = SnoozeUtils.snoozedUntilMessage(snoozeDate)
+ return SnoozeUtils.moveThreads(
+ threads,
+ {snooze: true, getSnoozeCategory, getInboxCategory, description}
+ )
+ })
+ },
+
+ moveThreadsFromSnooze(threads, snoozeCategoriesByAccountPromise) {
+ return snoozeCategoriesByAccountPromise
+ .then((snoozeCategoriesByAccountId)=> {
+ const getSnoozeCategory = (accId)=> snoozeCategoriesByAccountId[accId]
+ const {getInboxCategory} = CategoryStore
+ const description = 'Unsnoozed';
+ return SnoozeUtils.moveThreads(
+ threads,
+ {snooze: false, getSnoozeCategory, getInboxCategory, description}
+ )
+ })
+ },
}
-
-export function moveThreadsToSnooze(threads, snoozeDate) {
- return getSnoozeCategoriesByAccount()
- .then((categoriesByAccountId)=> {
- const description = snoozeMessage(snoozeDate)
- return moveThreads(threads, categoriesByAccountId, {snooze: true, description})
- })
-}
-
-
-export function moveThreadsFromSnooze(threads) {
- return getSnoozeCategoriesByAccount()
- .then((categoriesByAccountId)=> {
- const description = 'Unsnoozed';
- return moveThreads(threads, categoriesByAccountId, {snooze: false, description})
- })
-}
+export default SnoozeUtils
diff --git a/internal_packages/thread-snooze/spec/snooze-store-spec.es6 b/internal_packages/thread-snooze/spec/snooze-store-spec.es6
new file mode 100644
index 000000000..395f7a0c7
--- /dev/null
+++ b/internal_packages/thread-snooze/spec/snooze-store-spec.es6
@@ -0,0 +1,133 @@
+import {
+ AccountStore,
+ CategoryStore,
+ NylasAPI,
+ Thread,
+ Actions,
+ Category,
+} from 'nylas-exports'
+import SnoozeUtils from '../lib/snooze-utils'
+import SnoozeStore from '../lib/snooze-store'
+
+
+describe('SnoozeStore', ()=> {
+ beforeEach(()=> {
+ this.store = new SnoozeStore('plug-id', 'plug-name')
+ this.name = 'Snooze folder'
+ this.accounts = [{id: 123}, {id: 321}]
+
+ this.snoozeCatsByAccount = {
+ '123': new Category({accountId: 123, displayName: this.name, serverId: 'sn-1'}),
+ '321': new Category({accountId: 321, displayName: this.name, serverId: 'sn-2'}),
+ }
+ this.inboxCatsByAccount = {
+ '123': new Category({accountId: 123, name: 'inbox', serverId: 'in-1'}),
+ '321': new Category({accountId: 321, name: 'inbox', serverId: 'in-2'}),
+ }
+ this.threads = [
+ new Thread({accountId: 123, serverId: 's-1'}),
+ new Thread({accountId: 123, serverId: 's-2'}),
+ new Thread({accountId: 321, serverId: 's-3'}),
+ ]
+ this.updatedThreadsByAccountId = {
+ '123': {
+ threads: [this.threads[0], this.threads[1]],
+ snoozeCategoryId: 'sn-1',
+ returnCategoryId: 'in-1',
+ },
+ '321': {
+ threads: [this.threads[2]],
+ snoozeCategoryId: 'sn-2',
+ returnCategoryId: 'in-2',
+ },
+ }
+ this.store.snoozeCategoriesPromise = Promise.resolve()
+ spyOn(this.store, 'recordSnoozeEvent')
+ spyOn(this.store, 'groupUpdatedThreads').andReturn(Promise.resolve(this.updatedThreadsByAccountId))
+
+ spyOn(AccountStore, 'accountsForItems').andReturn(this.accounts)
+ spyOn(NylasAPI, 'authPlugin').andReturn(Promise.resolve())
+ spyOn(SnoozeUtils, 'moveThreadsToSnooze').andReturn(Promise.resolve(this.threads))
+ spyOn(SnoozeUtils, 'moveThreadsFromSnooze')
+ spyOn(Actions, 'setMetadata')
+ spyOn(Actions, 'closePopover')
+ spyOn(NylasEnv, 'reportError')
+ spyOn(NylasEnv, 'showErrorDialog')
+ })
+
+ describe('groupUpdatedThreads', ()=> {
+ it('groups the threads correctly by account id, with their snooze and inbox categories', ()=> {
+ spyOn(CategoryStore, 'getInboxCategory').andCallFake(accId => this.inboxCatsByAccount[accId])
+
+ waitsForPromise(()=> {
+ return this.store.groupUpdatedThreads(this.threads, this.snoozeCatsByAccount)
+ .then((result)=> {
+ expect(result['123']).toEqual({
+ threads: [this.threads[0], this.threads[1]],
+ snoozeCategoryId: 'sn-1',
+ returnCategoryId: 'in-1',
+ })
+ expect(result['321']).toEqual({
+ threads: [this.threads[2]],
+ snoozeCategoryId: 'sn-2',
+ returnCategoryId: 'in-2',
+ })
+ })
+ })
+ });
+ });
+
+ describe('onSnoozeThreads', ()=> {
+ it('auths plugin against all present accounts', ()=> {
+ waitsForPromise(()=> {
+ return this.store.onSnoozeThreads(this.threads, 'date', 'label')
+ .then(()=> {
+ expect(NylasAPI.authPlugin).toHaveBeenCalled()
+ expect(NylasAPI.authPlugin.calls[0].args[2]).toEqual(this.accounts[0])
+ expect(NylasAPI.authPlugin.calls[1].args[2]).toEqual(this.accounts[1])
+ })
+ })
+ });
+
+ it('calls Actions.setMetadata with the correct metadata', ()=> {
+ waitsForPromise(()=> {
+ return this.store.onSnoozeThreads(this.threads, 'date', 'label')
+ .then(()=> {
+ expect(Actions.setMetadata).toHaveBeenCalled()
+ expect(Actions.setMetadata.calls[0].args).toEqual([
+ this.updatedThreadsByAccountId['123'].threads,
+ 'plug-id',
+ {
+ snoozeDate: 'date',
+ snoozeCategoryId: 'sn-1',
+ returnCategoryId: 'in-1',
+ },
+ ])
+ expect(Actions.setMetadata.calls[1].args).toEqual([
+ this.updatedThreadsByAccountId['321'].threads,
+ 'plug-id',
+ {
+ snoozeDate: 'date',
+ snoozeCategoryId: 'sn-2',
+ returnCategoryId: 'in-2',
+ },
+ ])
+ })
+ })
+ });
+
+ it('displays dialog on error', ()=> {
+ jasmine.unspy(SnoozeUtils, 'moveThreadsToSnooze')
+ spyOn(SnoozeUtils, 'moveThreadsToSnooze').andReturn(Promise.reject(new Error('Oh no!')))
+
+ waitsForPromise(()=> {
+ return this.store.onSnoozeThreads(this.threads, 'date', 'label')
+ .finally(()=> {
+ expect(SnoozeUtils.moveThreadsFromSnooze).toHaveBeenCalled()
+ expect(NylasEnv.reportError).toHaveBeenCalled()
+ expect(NylasEnv.showErrorDialog).toHaveBeenCalled()
+ })
+ })
+ });
+ });
+})
diff --git a/internal_packages/thread-snooze/spec/snooze-utils-spec.es6 b/internal_packages/thread-snooze/spec/snooze-utils-spec.es6
new file mode 100644
index 000000000..4df78a4ec
--- /dev/null
+++ b/internal_packages/thread-snooze/spec/snooze-utils-spec.es6
@@ -0,0 +1,232 @@
+import moment from 'moment'
+import {
+ Actions,
+ TaskQueueStatusStore,
+ TaskFactory,
+ DatabaseStore,
+ Category,
+ Thread,
+ CategoryStore,
+} from 'nylas-exports'
+import SnoozeUtils from '../lib/snooze-utils'
+
+const {
+ snoozedUntilMessage,
+ createSnoozeCategory,
+ getSnoozeCategory,
+ groupProcessedThreadsByAccountId,
+ moveThreads,
+} = SnoozeUtils
+
+
+describe('Snooze Utils', ()=> {
+ beforeEach(()=> {
+ this.name = 'Snoozed Folder'
+ this.accId = 123
+ spyOn(SnoozeUtils, 'whenCategoriesReady').andReturn(Promise.resolve())
+ })
+
+ describe('snoozedUntilMessage', ()=> {
+ it('returns correct message if no snooze date provided', ()=> {
+ expect(snoozedUntilMessage()).toEqual('Snoozed')
+ });
+
+ describe('when less than 24 hours from now', ()=> {
+ it('returns correct message if snoozeDate is on the hour of the clock', ()=> {
+ const now9AM = moment().hour(9).minute(0)
+ const tomorrowAt8 = moment(now9AM).add(1, 'day').hour(8)
+ const result = snoozedUntilMessage(tomorrowAt8, now9AM)
+ expect(result).toEqual('Snoozed until 8AM')
+ });
+
+ it('returns correct message if snoozeDate otherwise', ()=> {
+ const now9AM = moment().hour(9).minute(0)
+ const snooze10AM = moment(now9AM).hour(10).minute(5)
+ const result = snoozedUntilMessage(snooze10AM, now9AM)
+ expect(result).toEqual('Snoozed until 10:05AM')
+ });
+ });
+
+ describe('when more than 24 hourse from now', ()=> {
+ it('returns correct message if snoozeDate is on the hour of the clock', ()=> {
+ // Jan 1
+ const now9AM = moment().month(0).date(1).hour(9).minute(0)
+ const tomorrowAt10 = moment(now9AM).add(1, 'day').hour(10)
+ const result = snoozedUntilMessage(tomorrowAt10, now9AM)
+ expect(result).toEqual('Snoozed until Jan 2, 10AM')
+ });
+
+ it('returns correct message if snoozeDate otherwise', ()=> {
+ // Jan 1
+ const now9AM = moment().month(0).date(1).hour(9).minute(0)
+ const tomorrowAt930 = moment(now9AM).add(1, 'day').minute(30)
+ const result = snoozedUntilMessage(tomorrowAt930, now9AM)
+ expect(result).toEqual('Snoozed until Jan 2, 9:30AM')
+ });
+ });
+ });
+
+ describe('createSnoozeCategory', ()=> {
+ beforeEach(()=> {
+ this.category = new Category({
+ displayName: this.name,
+ accountId: this.accId,
+ clientId: 321,
+ serverId: 321,
+ })
+ spyOn(Actions, 'queueTask')
+ spyOn(TaskQueueStatusStore, 'waitForPerformRemote').andReturn(Promise.resolve())
+ spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve(this.category))
+ })
+
+ it('creates category with correct snooze name', ()=> {
+ createSnoozeCategory(this.accId, this.name)
+ expect(Actions.queueTask).toHaveBeenCalled()
+ const task = Actions.queueTask.calls[0].args[0]
+ expect(task.category.displayName).toEqual(this.name)
+ expect(task.category.accountId).toEqual(this.accId)
+ });
+
+ it('resolves with the updated category that has been saved to the server', ()=> {
+ waitsForPromise(()=> {
+ return createSnoozeCategory(this.accId, this.name).then((result)=> {
+ expect(DatabaseStore.findBy).toHaveBeenCalled()
+ expect(result).toBe(this.category)
+ })
+ })
+ });
+
+ it('rejects if the category could not be found in the database', ()=> {
+ this.category.serverId = null
+ jasmine.unspy(DatabaseStore, 'findBy')
+ spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve(this.category))
+ waitsForPromise(()=> {
+ return createSnoozeCategory(this.accId, this.name)
+ .then(()=> {
+ throw new Error('createSnoozeCategory should not resolve in this case!')
+ })
+ .catch((error)=> {
+ expect(DatabaseStore.findBy).toHaveBeenCalled()
+ expect(error.message).toEqual('Could not create Snooze category')
+ })
+ })
+ });
+
+ it('rejects if the category could not be saved to the server', ()=> {
+ jasmine.unspy(DatabaseStore, 'findBy')
+ spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve(undefined))
+ waitsForPromise(()=> {
+ return createSnoozeCategory(this.accId, this.name)
+ .then(()=> {
+ throw new Error('createSnoozeCategory should not resolve in this case!')
+ })
+ .catch((error)=> {
+ expect(DatabaseStore.findBy).toHaveBeenCalled()
+ expect(error.message).toEqual('Could not create Snooze category')
+ })
+ })
+ });
+ });
+
+ describe('getSnoozeCategory', ()=> {
+ it('resolves category if it exists in the category store', ()=> {
+ const categories = [
+ new Category({accountId: this.accId, name: 'inbox'}),
+ new Category({accountId: this.accId, displayName: this.name}),
+ ]
+ spyOn(CategoryStore, 'categories').andReturn(categories)
+ spyOn(SnoozeUtils, 'createSnoozeCategory')
+
+ waitsForPromise(()=> {
+ return getSnoozeCategory(this.accountId, this.name)
+ .then((result)=> {
+ expect(SnoozeUtils.createSnoozeCategory).not.toHaveBeenCalled()
+ expect(result).toBe(categories[1])
+ })
+ })
+ });
+
+ it('creates category if it does not exist', ()=> {
+ const categories = [
+ new Category({accountId: this.accId, name: 'inbox'}),
+ ]
+ const snoozeCat = new Category({accountId: this.accId, displayName: this.name})
+ spyOn(CategoryStore, 'categories').andReturn(categories)
+ spyOn(SnoozeUtils, 'createSnoozeCategory').andReturn(Promise.resolve(snoozeCat))
+
+ waitsForPromise(()=> {
+ return getSnoozeCategory(this.accId, this.name)
+ .then((result)=> {
+ expect(SnoozeUtils.createSnoozeCategory).toHaveBeenCalledWith(this.accId, this.name)
+ expect(result).toBe(snoozeCat)
+ })
+ })
+ });
+ });
+
+ describe('moveThreads', ()=> {
+ beforeEach(()=> {
+ this.description = 'Snoozin';
+ this.snoozeCatsByAccount = {
+ '123': new Category({accountId: 123, displayName: this.name, serverId: 'sr-1'}),
+ '321': new Category({accountId: 321, displayName: this.name, serverId: 'sr-2'}),
+ }
+ this.inboxCatsByAccount = {
+ '123': new Category({accountId: 123, name: 'inbox', serverId: 'sr-3'}),
+ '321': new Category({accountId: 321, name: 'inbox', serverId: 'sr-4'}),
+ }
+ this.threads = [
+ new Thread({accountId: 123}),
+ new Thread({accountId: 123}),
+ new Thread({accountId: 321}),
+ ]
+ this.getInboxCat = (accId) => this.inboxCatsByAccount[accId]
+ this.getSnoozeCat = (accId) => this.snoozeCatsByAccount[accId]
+
+ spyOn(DatabaseStore, 'modelify').andReturn(Promise.resolve(this.threads))
+ spyOn(TaskFactory, 'tasksForApplyingCategories').andReturn([])
+ spyOn(TaskQueueStatusStore, 'waitForPerformRemote').andReturn(Promise.resolve())
+ spyOn(Actions, 'queueTasks')
+ })
+
+ it('creates the tasks to move threads correctly when snoozing', ()=> {
+ const snooze = true
+ const description = this.description
+
+ waitsForPromise(()=> {
+ return moveThreads(this.threads, {snooze, description, getInboxCategory: this.getInboxCat, getSnoozeCategory: this.getSnoozeCat})
+ .then(()=> {
+ expect(TaskFactory.tasksForApplyingCategories).toHaveBeenCalled()
+ expect(Actions.queueTasks).toHaveBeenCalled()
+ const taskArgs = TaskFactory.tasksForApplyingCategories.calls[0].args[0]
+ expect(taskArgs.threads).toBe(this.threads)
+ expect(taskArgs.categoriesToRemove('123')).toBe(this.inboxCatsByAccount['123'])
+ expect(taskArgs.categoriesToRemove('321')).toBe(this.inboxCatsByAccount['321'])
+ expect(taskArgs.categoryToAdd('123')).toBe(this.snoozeCatsByAccount['123'])
+ expect(taskArgs.categoryToAdd('321')).toBe(this.snoozeCatsByAccount['321'])
+ expect(taskArgs.taskDescription).toEqual(description)
+ })
+ })
+ });
+
+ it('creates the tasks to move threads correctly when unsnoozing', ()=> {
+ const snooze = false
+ const description = this.description
+
+ waitsForPromise(()=> {
+ return moveThreads(this.threads, {snooze, description, getInboxCategory: this.getInboxCat, getSnoozeCategory: this.getSnoozeCat})
+ .then(()=> {
+ expect(TaskFactory.tasksForApplyingCategories).toHaveBeenCalled()
+ expect(Actions.queueTasks).toHaveBeenCalled()
+ const taskArgs = TaskFactory.tasksForApplyingCategories.calls[0].args[0]
+ expect(taskArgs.threads).toBe(this.threads)
+ expect(taskArgs.categoryToAdd('123')).toBe(this.inboxCatsByAccount['123'])
+ expect(taskArgs.categoryToAdd('321')).toBe(this.inboxCatsByAccount['321'])
+ expect(taskArgs.categoriesToRemove('123')).toBe(this.snoozeCatsByAccount['123'])
+ expect(taskArgs.categoriesToRemove('321')).toBe(this.snoozeCatsByAccount['321'])
+ expect(taskArgs.taskDescription).toEqual(description)
+ })
+ })
+ });
+ });
+});
diff --git a/internal_packages/thread-snooze/stylesheets/snooze-popover.less b/internal_packages/thread-snooze/stylesheets/snooze-popover.less
index 90c6cf9bf..94c374d77 100644
--- a/internal_packages/thread-snooze/stylesheets/snooze-popover.less
+++ b/internal_packages/thread-snooze/stylesheets/snooze-popover.less
@@ -63,11 +63,5 @@
input {
margin-bottom: 3px;
}
-
- em {
- font-size: 0.9em;
- opacity: 0.62;
- }
-
}
}
diff --git a/spec/components/date-input-spec.jsx b/spec/components/date-input-spec.jsx
new file mode 100644
index 000000000..3a8adadf3
--- /dev/null
+++ b/spec/components/date-input-spec.jsx
@@ -0,0 +1,71 @@
+import React, {addons} from 'react/addons';
+import {DateUtils} from 'nylas-exports'
+import DateInput from '../../src/components/date-input';
+import {renderIntoDocument} from '../nylas-test-utils'
+
+const {findDOMNode} = React;
+const {TestUtils: {
+ findRenderedDOMComponentWithTag,
+ findRenderedDOMComponentWithClass,
+ Simulate,
+}} = addons;
+
+const makeInput = (props = {})=> {
+ const input = renderIntoDocument();
+ if (props.initialState) {
+ input.setState(props.initialState)
+ }
+ return input
+};
+
+const getInputNode = (reactElement)=> {
+ return findDOMNode(findRenderedDOMComponentWithTag(reactElement, 'input'))
+};
+
+
+describe('DateInput', ()=> {
+ describe('onInputKeyDown', ()=> {
+ it('should submit the input if Enter or Escape pressed', ()=> {
+ const onSubmitDate = jasmine.createSpy('onSubmitDate')
+ const dateInput = makeInput({onSubmitDate: onSubmitDate})
+ const inputNode = getInputNode(dateInput)
+ const stopPropagation = jasmine.createSpy('stopPropagation')
+ const keys = ['Enter', 'Return']
+ inputNode.value = 'tomorrow'
+ spyOn(DateUtils, 'futureDateFromString').andReturn('someday')
+ spyOn(dateInput, 'setState')
+
+ keys.forEach((key)=> {
+ Simulate.keyDown(inputNode, {key, stopPropagation})
+ expect(stopPropagation).toHaveBeenCalled()
+ expect(onSubmitDate).toHaveBeenCalledWith('someday', 'tomorrow')
+ expect(dateInput.setState).toHaveBeenCalledWith({inputDate: null})
+ stopPropagation.reset()
+ onSubmitDate.reset()
+ dateInput.setState.reset()
+ })
+ });
+ });
+
+ describe('render', ()=> {
+ beforeEach(()=> {
+ spyOn(DateUtils, 'format').andReturn('formatted')
+ });
+
+ it('should render a date interpretation if a date has been inputted', ()=> {
+ const dateInput = makeInput({initialState: {inputDate: 'something!'}})
+ spyOn(dateInput, 'setState')
+ const dateInterpretation = findDOMNode(findRenderedDOMComponentWithClass(dateInput, 'date-interpretation'))
+
+ expect(dateInterpretation.textContent).toEqual('formatted')
+ });
+
+ it('should not render a date interpretation if no input date available', ()=> {
+ const dateInput = makeInput({initialState: {inputDate: null}})
+ spyOn(dateInput, 'setState')
+ expect(()=> {
+ findRenderedDOMComponentWithClass(dateInput, 'date-interpretation')
+ }).toThrow()
+ });
+ });
+});
diff --git a/spec/components/editable-list-spec.jsx b/spec/components/editable-list-spec.jsx
index 3e79e65bd..e31e98d63 100644
--- a/spec/components/editable-list-spec.jsx
+++ b/spec/components/editable-list-spec.jsx
@@ -12,7 +12,11 @@ const {TestUtils: {
const makeList = (items = [], props = {})=> {
- return renderIntoDocument();
+ const list = renderIntoDocument();
+ if (props.initialState) {
+ list.setState(props.initialState)
+ }
+ return list
};
describe('EditableList', ()=> {
diff --git a/src/components/date-input.jsx b/src/components/date-input.jsx
new file mode 100644
index 000000000..c10b3f026
--- /dev/null
+++ b/src/components/date-input.jsx
@@ -0,0 +1,70 @@
+import classnames from 'classnames';
+import React, {Component, PropTypes} from 'react';
+import {DateUtils} from 'nylas-exports';
+
+
+class DateInput extends Component {
+ static displayName = 'DateInput';
+
+ static propTypes = {
+ className: PropTypes.string,
+ dateFormat: PropTypes.string.isRequired,
+ onSubmitDate: PropTypes.func,
+ };
+
+ static defaultProps = {
+ onSubmitDate: ()=> {},
+ };
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ inputDate: null,
+ }
+ }
+
+ onInputKeyDown = (event)=> {
+ const {key, target: {value}} = event;
+ if (value.length > 0 && ["Enter", "Return"].includes(key)) {
+ // This prevents onInputChange from being fired
+ event.stopPropagation();
+ const date = DateUtils.futureDateFromString(value);
+ this.props.onSubmitDate(date, value);
+ this.setState({inputDate: null})
+ }
+ };
+
+ onInputChange = (event)=> {
+ this.setState({inputDate: DateUtils.futureDateFromString(event.target.value)});
+ };
+
+ render() {
+ let dateInterpretation;
+ if (this.state.inputDate) {
+ dateInterpretation = (
+
+ {DateUtils.format(this.state.inputDate, this.props.dateFormat)}
+
+ );
+ }
+ const {className} = this.props
+ const classes = classnames({
+ "nylas-date-input": true,
+ [className]: className != null,
+ })
+
+ return (
+
+
+ {dateInterpretation}
+
+ )
+ }
+}
+
+export default DateInput
diff --git a/src/components/editable-list.jsx b/src/components/editable-list.jsx
index 2440232f7..4cd760efb 100644
--- a/src/components/editable-list.jsx
+++ b/src/components/editable-list.jsx
@@ -29,8 +29,6 @@ import React, {Component, PropTypes} from 'react';
* @param {object} props.createInputProps - Props object to be passed on to
* the create input element. However, keep in mind that these props can not
* override the default props that EditableList will pass to the input.
- * @param {object} props.initialState - Used for testing purposes to initialize
- * the component with a given state.
* @param {props.onCreateItem} props.onCreateItem
* @param {props.onDeleteItem} props.onDeleteItem
* @param {props.onSelectItem} props.onSelectItem
@@ -97,7 +95,6 @@ class EditableList extends Component {
onReorderItem: PropTypes.func,
onItemEdited: PropTypes.func,
onItemCreated: PropTypes.func,
- initialState: PropTypes.object,
/* Optional, if you choose to control selection externally */
selected: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
@@ -117,7 +114,7 @@ class EditableList extends Component {
constructor(props) {
super(props);
- this.state = props.initialState || {
+ this.state = {
dropInsertionIndex: -1,
editingIndex: -1,
creatingItem: false,
diff --git a/src/date-utils.es6 b/src/date-utils.es6
index 5905d6ae3..0d6d8da03 100644
--- a/src/date-utils.es6
+++ b/src/date-utils.es6
@@ -3,6 +3,37 @@ import moment from 'moment'
import chrono from 'chrono-node'
import _ from 'underscore'
+// Init locale for moment
+moment.locale(navigator.language)
+
+
+const Hours = {
+ Morning: 9,
+ Evening: 20,
+ Midnight: 24,
+}
+
+const Days = {
+ NextMonday: 8,
+ ThisWeekend: 6,
+}
+
+function oclock(momentDate) {
+ return momentDate.minute(0).second(0)
+}
+
+function morning(momentDate, morningHour = Hours.Morning) {
+ return oclock(momentDate.hour(morningHour))
+}
+
+function evening(momentDate, eveningHour = Hours.Evening) {
+ return oclock(momentDate.hour(eveningHour))
+}
+
+function midnight(momentDate, midnightHour = Hours.Midnight) {
+ return oclock(momentDate.hour(midnightHour))
+}
+
function isPastDate({year, month, day}, ref) {
const refDay = ref.getDate();
const refMonth = ref.getMonth() + 1;
@@ -47,33 +78,6 @@ EnforceFutureDate.refine = (text, results)=> {
const chronoFuture = new chrono.Chrono(chrono.options.casualOption());
chronoFuture.refiners.push(EnforceFutureDate);
-const Hours = {
- Morning: 9,
- Evening: 20,
- Midnight: 24,
-}
-
-const Days = {
- NextMonday: 8,
- ThisWeekend: 6,
-}
-
-function oclock(momentDate) {
- return momentDate.minute(0).second(0)
-}
-
-function morning(momentDate, morningHour = Hours.Morning) {
- return oclock(momentDate.hour(morningHour))
-}
-
-function evening(momentDate, eveningHour = Hours.Evening) {
- return oclock(momentDate.hour(eveningHour))
-}
-
-function midnight(momentDate, midnightHour = Hours.Midnight) {
- return oclock(momentDate.hour(midnightHour))
-}
-
const DateUtils = {
diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee
index e8e7d0a41..a0610c6a6 100644
--- a/src/global/nylas-component-kit.coffee
+++ b/src/global/nylas-component-kit.coffee
@@ -38,6 +38,7 @@ class NylasComponentKit
@load "EditableList", "editable-list"
@load "OutlineViewItem", "outline-view-item"
@load "OutlineView", "outline-view"
+ @load "DateInput", "date-input"
@load "ScrollRegion", 'scroll-region'
@load "ResizableRegion", 'resizable-region'
diff --git a/static/components/date-input.less b/static/components/date-input.less
new file mode 100644
index 000000000..ffee83e1e
--- /dev/null
+++ b/static/components/date-input.less
@@ -0,0 +1,9 @@
+@import "ui-variables";
+
+.nylas-date-input {
+ .date-interpretation {
+ color: @text-color-subtle;
+ font-size: @font-size-small;
+ opacity: 0.6;
+ }
+}
diff --git a/static/index.less b/static/index.less
index ed763b84a..65e4a1d94 100644
--- a/static/index.less
+++ b/static/index.less
@@ -31,3 +31,4 @@
@import "components/editable-list";
@import "components/outline-view";
@import "components/fixed-popover";
+@import "components/date-input";