mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-07 21:24:24 +08:00
Convert mail rules code to ES2016
This commit is contained in:
parent
0e214f0c3e
commit
5164899c46
12 changed files with 742 additions and 620 deletions
|
@ -7,7 +7,7 @@ import PreferencesGeneral from './tabs/preferences-general';
|
|||
import PreferencesAccounts from './tabs/preferences-accounts';
|
||||
import PreferencesAppearance from './tabs/preferences-appearance';
|
||||
import PreferencesKeymaps from './tabs/preferences-keymaps';
|
||||
// import PreferencesMailRules from './tabs/preferences-mail-rules';
|
||||
import PreferencesMailRules from './tabs/preferences-mail-rules';
|
||||
import PreferencesIdentity from './tabs/preferences-identity';
|
||||
|
||||
export function activate() {
|
||||
|
@ -41,12 +41,12 @@ export function activate() {
|
|||
component: PreferencesKeymaps,
|
||||
order: 5,
|
||||
}))
|
||||
// PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
|
||||
// tabId: 'Mail Rules',
|
||||
// displayName: 'Mail Rules',
|
||||
// component: PreferencesMailRules,
|
||||
// order: 6,
|
||||
// }))
|
||||
PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({
|
||||
tabId: 'Mail Rules',
|
||||
displayName: 'Mail Rules',
|
||||
component: PreferencesMailRules,
|
||||
order: 6,
|
||||
}))
|
||||
|
||||
WorkspaceStore.defineSheet('Preferences', {}, {
|
||||
split: ['Preferences'],
|
||||
|
|
|
@ -53,7 +53,7 @@ class PreferencesMailRules extends React.Component {
|
|||
currentAccount: currentAccount,
|
||||
rules: rules,
|
||||
selectedRule: selectedRule,
|
||||
tasks: TaskQueue.tasksMatching(ReprocessMailRulesTask, {}),
|
||||
tasks: TaskQueue.findTasks(ReprocessMailRulesTask, {}),
|
||||
actionTemplates: ActionTemplatesForAccount(currentAccount),
|
||||
conditionTemplates: ConditionTemplatesForAccount(currentAccount),
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ class PreferencesMailRules extends React.Component {
|
|||
}
|
||||
|
||||
_onTasksChanged = () => {
|
||||
this.setState({tasks: TaskQueue.tasksMatching(ReprocessMailRulesTask, {})})
|
||||
this.setState({tasks: TaskQueue.findTasks(ReprocessMailRulesTask, {})})
|
||||
}
|
||||
|
||||
_renderAccountPicker() {
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
Rx = require 'rx-lite'
|
||||
{RetinaImg, Flexbox} = require 'nylas-component-kit'
|
||||
{CategoryStore, Actions, Utils} = require 'nylas-exports'
|
||||
|
||||
{Comparator, Template} = require './scenario-editor-models'
|
||||
|
||||
SOURCE_SELECT_NULL = 'NULL'
|
||||
|
||||
class SourceSelect extends React.Component
|
||||
@displayName: 'SourceSelect'
|
||||
@propTypes:
|
||||
value: React.PropTypes.string
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
options: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object
|
||||
React.PropTypes.array
|
||||
]).isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
options: []
|
||||
|
||||
componentDidMount: =>
|
||||
@_setupValuesSubscription()
|
||||
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
@_setupValuesSubscription(nextProps)
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_subscription?.dispose()
|
||||
@_subscription = null
|
||||
|
||||
_setupValuesSubscription: (props = @props) =>
|
||||
@_subscription?.dispose()
|
||||
@_subscription = null
|
||||
if props.options instanceof Rx.Observable
|
||||
@_subscription = props.options.subscribe (options) =>
|
||||
@setState({options})
|
||||
else
|
||||
@setState(options: props.options)
|
||||
|
||||
render: =>
|
||||
options = @state.options
|
||||
|
||||
# The React <select> component won't select the correct option if the value
|
||||
# is null or undefined - it just leaves the selection whatever it was in the
|
||||
# previous render. To work around this, we coerce null/undefined to SOURCE_SELECT_NULL.
|
||||
|
||||
<select value={@props.value || SOURCE_SELECT_NULL} onChange={@_onChange}>
|
||||
<option key={SOURCE_SELECT_NULL} value={SOURCE_SELECT_NULL}></option>
|
||||
{ @state.options.map ({value, name}) =>
|
||||
<option key={value} value={value}>{name}</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
_onChange: (event) =>
|
||||
value = event.target.value
|
||||
value = null if value is SOURCE_SELECT_NULL
|
||||
@props.onChange(target: {value})
|
||||
|
||||
class ScenarioEditorRow extends React.Component
|
||||
@displayName: 'ScenarioEditorRow'
|
||||
@propTypes:
|
||||
instance: React.PropTypes.object.isRequired
|
||||
removable: React.PropTypes.bool
|
||||
templates: React.PropTypes.array.isRequired
|
||||
onChange: React.PropTypes.func
|
||||
onInsert: React.PropTypes.func
|
||||
onRemove: React.PropTypes.func
|
||||
|
||||
constructor: (@props) ->
|
||||
|
||||
render: =>
|
||||
template = _.findWhere(@props.templates, {key: @props.instance.templateKey})
|
||||
unless template
|
||||
return <span> Could not find template for instance key: {@props.instance.templateKey}</span>
|
||||
|
||||
<Flexbox direction="row" className="well-row">
|
||||
<span>
|
||||
{@_renderTemplateSelect(template)}
|
||||
{@_renderComparator(template)}
|
||||
<span>{template.valueLabel}</span>
|
||||
{@_renderValue(template)}
|
||||
</span>
|
||||
<div style={flex: 1}></div>
|
||||
{@_renderActions()}
|
||||
</Flexbox>
|
||||
|
||||
_renderTemplateSelect: (template) =>
|
||||
options = @props.templates.map ({key, name}) =>
|
||||
<option value={key} key={key}>{name}</option>
|
||||
|
||||
<select
|
||||
value={@props.instance.templateKey}
|
||||
onChange={@_onChangeTemplate}>
|
||||
{options}
|
||||
</select>
|
||||
|
||||
_renderComparator: (template) =>
|
||||
options = _.map template.comparators, ({name}, key) =>
|
||||
<option key={key} value={key}>{name}</option>
|
||||
|
||||
return false unless options.length > 0
|
||||
|
||||
<select
|
||||
value={@props.instance.comparatorKey}
|
||||
onChange={@_onChangeComparator}>
|
||||
{options}
|
||||
</select>
|
||||
|
||||
_renderValue: (template) =>
|
||||
if template.type is Template.Type.Enum
|
||||
<SourceSelect
|
||||
value={@props.instance.value}
|
||||
onChange={@_onChangeValue}
|
||||
options={template.values} />
|
||||
|
||||
else if template.type is Template.Type.String
|
||||
<input
|
||||
type="text"
|
||||
value={@props.instance.value}
|
||||
onChange={@_onChangeValue} />
|
||||
|
||||
else
|
||||
false
|
||||
|
||||
_renderActions: =>
|
||||
<div className="actions">
|
||||
{ if @props.removable then <div className="btn" onClick={@props.onRemove}>−</div> }
|
||||
<div className="btn" onClick={@props.onInsert}>+</div>
|
||||
</div>
|
||||
|
||||
_onChangeValue: (event) =>
|
||||
instance = _.clone(@props.instance)
|
||||
instance.value = event.target.value
|
||||
@props.onChange(instance)
|
||||
|
||||
_onChangeComparator: (event) =>
|
||||
instance = _.clone(@props.instance)
|
||||
instance.comparatorKey = event.target.value
|
||||
@props.onChange(instance)
|
||||
|
||||
_onChangeTemplate: (event) =>
|
||||
instance = _.clone(@props.instance)
|
||||
|
||||
existingTemplate = _.findWhere(@props.templates, key: instance.key)
|
||||
newTemplate = _.findWhere(@props.templates, key: event.target.value)
|
||||
|
||||
instance = newTemplate.coerceInstance(instance)
|
||||
|
||||
@props.onChange(instance)
|
||||
|
||||
module.exports = ScenarioEditorRow
|
186
packages/client-app/src/components/scenario-editor-row.jsx
Normal file
186
packages/client-app/src/components/scenario-editor-row.jsx
Normal file
|
@ -0,0 +1,186 @@
|
|||
import React from 'react';
|
||||
import Rx from 'rx-lite';
|
||||
import {Flexbox} from 'nylas-component-kit';
|
||||
|
||||
import {Template} from './scenario-editor-models';
|
||||
|
||||
const SOURCE_SELECT_NULL = 'NULL';
|
||||
|
||||
class SourceSelect extends React.Component {
|
||||
static displayName = 'SourceSelect';
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
options: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.array,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._setupValuesSubscription()
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._setupValuesSubscription(nextProps);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._subscription) { this._subscription.dispose(); }
|
||||
this._subscription = null;
|
||||
}
|
||||
|
||||
_setupValuesSubscription(props = this.props) {
|
||||
if (this._subscription) { this._subscription.dispose(); }
|
||||
this._subscription = null;
|
||||
if (props.options instanceof Rx.Observable) {
|
||||
this._subscription = props.options.subscribe((options) =>
|
||||
this.setState({options})
|
||||
)
|
||||
} else {
|
||||
this.setState({options: props.options});
|
||||
}
|
||||
}
|
||||
|
||||
_onChange = (event) => {
|
||||
this.props.onChange({target: {
|
||||
value: event.target.value === SOURCE_SELECT_NULL ? null : event.target.value,
|
||||
}});
|
||||
}
|
||||
|
||||
render() {
|
||||
// The React <select> component won't select the correct option if the value
|
||||
// is null or undefined - it just leaves the selection whatever it was in the
|
||||
// previous render. To work around this, we coerce null/undefined to SOURCE_SELECT_NULL.
|
||||
|
||||
return (
|
||||
<select value={this.props.value || SOURCE_SELECT_NULL} onChange={this._onChange}>
|
||||
<option key={SOURCE_SELECT_NULL} value={SOURCE_SELECT_NULL} />
|
||||
{this.state.options.map(({value, name}) =>
|
||||
<option key={value} value={value}>{name}</option>
|
||||
)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ScenarioEditorRow extends React.Component {
|
||||
static displayName = 'ScenarioEditorRow';
|
||||
static propTypes = {
|
||||
instance: React.PropTypes.object.isRequired,
|
||||
removable: React.PropTypes.bool,
|
||||
templates: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
onInsert: React.PropTypes.func,
|
||||
onRemove: React.PropTypes.func,
|
||||
};
|
||||
|
||||
_onChangeValue = (event) => {
|
||||
const instance = JSON.parse(JSON.stringify(this.props.instance));
|
||||
instance.value = event.target.value
|
||||
this.props.onChange(instance)
|
||||
}
|
||||
|
||||
_onChangeComparator = (event) => {
|
||||
const instance = JSON.parse(JSON.stringify(this.props.instance));
|
||||
instance.comparatorKey = event.target.value
|
||||
this.props.onChange(instance);
|
||||
}
|
||||
|
||||
_onChangeTemplate = (event) => {
|
||||
const instance = JSON.parse(JSON.stringify(this.props.instance));
|
||||
const newTemplate = this.props.templates.find(t => t.key === event.target.value);
|
||||
this.props.onChange(newTemplate.coerceInstance(instance));
|
||||
}
|
||||
|
||||
_renderTemplateSelect() {
|
||||
const options = this.props.templates.map(({key, name}) =>
|
||||
<option value={key} key={key}>{name}</option>
|
||||
);
|
||||
return (
|
||||
<select
|
||||
value={this.props.instance.templateKey}
|
||||
onChange={this._onChangeTemplate}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
_renderComparator(template) {
|
||||
const options = Object.keys(template.comparators).map(key =>
|
||||
<option key={key} value={key}>{template.comparators[key].name}</option>
|
||||
);
|
||||
if (options.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<select
|
||||
value={this.props.instance.comparatorKey}
|
||||
onChange={this._onChangeComparator}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
_renderValue(template) {
|
||||
if (template.type === Template.Type.Enum) {
|
||||
return (
|
||||
<SourceSelect
|
||||
value={this.props.instance.value}
|
||||
onChange={this._onChangeValue}
|
||||
options={template.values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (template.type === Template.Type.String) {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={this.props.instance.value}
|
||||
onChange={this._onChangeValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_renderActions() {
|
||||
return (
|
||||
<div className="actions">
|
||||
{ this.props.removable && <div className="btn" onClick={this.props.onRemove}>−</div> }
|
||||
<div className="btn" onClick={this.props.onInsert}>+</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const template = this.props.templates.find(t => t.key === this.props.instance.templateKey);
|
||||
if (!template) {
|
||||
return (
|
||||
<span> Could not find template for instance key: {this.props.instance.templateKey}</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flexbox direction="row" className="well-row">
|
||||
<span>
|
||||
{this._renderTemplateSelect(template)}
|
||||
{this._renderComparator(template)}
|
||||
<span>{template.valueLabel}</span>
|
||||
{this._renderValue(template)}
|
||||
</span>
|
||||
<div style={{flex: 1}} />
|
||||
{this._renderActions()}
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
{Comparator, Template} = require './scenario-editor-models'
|
||||
ScenarioEditorRow = require './scenario-editor-row'
|
||||
{RetinaImg, Flexbox} = require 'nylas-component-kit'
|
||||
{Actions, Utils} = require 'nylas-exports'
|
||||
|
||||
###
|
||||
The ScenarioEditor takes an array of ScenarioTemplate objects which define the
|
||||
scenario value space. Each ScenarioTemplate defines a `key` and it's valid
|
||||
`comparators` and `values`. The ScenarioEditor gives the user the option to
|
||||
create and combine instances of different templates to create a scenario.
|
||||
|
||||
For example:
|
||||
|
||||
Scenario Space:
|
||||
- ScenarioFactory("user-name", "The name of the user")
|
||||
+ valueType: String
|
||||
+ comparators: "contains", "starts with", etc.
|
||||
- SecnarioFactor("profession", "The profession of the user")
|
||||
+ valueType: Enum
|
||||
+ comparators: 'is'
|
||||
|
||||
Scenario Value:
|
||||
[{
|
||||
'key': 'user-name'
|
||||
'comparator': 'contains'
|
||||
'value': 'Ben'
|
||||
},{
|
||||
'key': 'profession'
|
||||
'comparator': 'is'
|
||||
'value': 'Engineer'
|
||||
}]
|
||||
###
|
||||
|
||||
class ScenarioEditor extends React.Component
|
||||
@displayName: 'ScenarioEditor'
|
||||
|
||||
@propTypes:
|
||||
instances: React.PropTypes.array
|
||||
className: React.PropTypes.string
|
||||
onChange: React.PropTypes.func
|
||||
templates: React.PropTypes.array
|
||||
|
||||
@Template: Template
|
||||
@Comparator: Comparator
|
||||
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
collapsed: true
|
||||
|
||||
render: =>
|
||||
<div className={@props.className}>
|
||||
{ (@props.instances || []).map (instance, idx) =>
|
||||
<ScenarioEditorRow
|
||||
key={idx}
|
||||
instance={instance}
|
||||
removable={@props.instances.length > 1}
|
||||
templates={@props.templates}
|
||||
onRemove={ => @_onRemoveRule(idx) }
|
||||
onInsert={ => @_onInsertRule(idx) }
|
||||
onChange={ (instance) => @_onChangeRowValue(instance, idx) } />
|
||||
}
|
||||
</div>
|
||||
|
||||
_performChange: (block) =>
|
||||
instances = JSON.parse(JSON.stringify(@props.instances))
|
||||
block(instances)
|
||||
@props.onChange(instances)
|
||||
|
||||
_onRemoveRule: (idx) =>
|
||||
@_performChange (instances) =>
|
||||
return if instances.length is 1
|
||||
instances.splice(idx, 1)
|
||||
|
||||
_onInsertRule: (idx) =>
|
||||
@_performChange (instances) =>
|
||||
instances.push @props.templates[0].createDefaultInstance()
|
||||
|
||||
_onChangeRowValue: (newInstance, idx) =>
|
||||
@_performChange (instances) =>
|
||||
instances[idx] = newInstance
|
||||
|
||||
module.exports = ScenarioEditor
|
94
packages/client-app/src/components/scenario-editor.jsx
Normal file
94
packages/client-app/src/components/scenario-editor.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import React from 'react'
|
||||
import {Comparator, Template} from './scenario-editor-models'
|
||||
import ScenarioEditorRow from './scenario-editor-row'
|
||||
|
||||
/**
|
||||
The ScenarioEditor takes an array of ScenarioTemplate objects which define the
|
||||
scenario value space. Each ScenarioTemplate defines a `key` and it's valid
|
||||
`comparators` and `values`. The ScenarioEditor gives the user the option to
|
||||
create and combine instances of different templates to create a scenario.
|
||||
|
||||
For example:
|
||||
|
||||
Scenario Space:
|
||||
- ScenarioFactory("user-name", "The name of the user")
|
||||
+ valueType: String
|
||||
+ comparators: "contains", "starts with", etc.
|
||||
- SecnarioFactor("profession", "The profession of the user")
|
||||
+ valueType: Enum
|
||||
+ comparators: 'is'
|
||||
|
||||
Scenario Value:
|
||||
[{
|
||||
'key': 'user-name'
|
||||
'comparator': 'contains'
|
||||
'value': 'Ben'
|
||||
},{
|
||||
'key': 'profession'
|
||||
'comparator': 'is'
|
||||
'value': 'Engineer'
|
||||
}]
|
||||
*/
|
||||
|
||||
export default class ScenarioEditor extends React.Component {
|
||||
static displayName = 'ScenarioEditor';
|
||||
|
||||
static propTypes = {
|
||||
instances: React.PropTypes.array,
|
||||
className: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
templates: React.PropTypes.array,
|
||||
};
|
||||
|
||||
static Template = Template;
|
||||
static Comparator = Comparator;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
collapsed: true,
|
||||
};
|
||||
}
|
||||
|
||||
_performChange(block) {
|
||||
const instances = JSON.parse(JSON.stringify(this.props.instances));
|
||||
block(instances);
|
||||
this.props.onChange(instances);
|
||||
}
|
||||
|
||||
_onRemoveRule = (idx) => {
|
||||
this._performChange((instances) => {
|
||||
if (instances.length > 1) { instances.splice(idx, 1) }
|
||||
});
|
||||
}
|
||||
|
||||
_onInsertRule = () => {
|
||||
this._performChange((instances) => {
|
||||
instances.push(this.props.templates[0].createDefaultInstance())
|
||||
});
|
||||
}
|
||||
|
||||
_onChangeRowValue = (newInstance, idx) => {
|
||||
this._performChange((instances) => {
|
||||
instances[idx] = newInstance;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
{(this.props.instances || []).map((instance, idx) =>
|
||||
<ScenarioEditorRow
|
||||
key={idx}
|
||||
instance={instance}
|
||||
removable={this.props.instances.length > 1}
|
||||
templates={this.props.templates}
|
||||
onRemove={() => this._onRemoveRule(idx)}
|
||||
onInsert={() => this._onInsertRule(idx)}
|
||||
onChange={(updatedInstance) => this._onChangeRowValue(updatedInstance, idx)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
NylasStore = require 'nylas-store'
|
||||
_ = require 'underscore'
|
||||
Rx = require 'rx-lite'
|
||||
AccountStore = require('./account-store').default
|
||||
DatabaseStore = require('./database-store').default
|
||||
TaskQueue = require('./task-queue').default;
|
||||
ReprocessMailRulesTask = require('../tasks/reprocess-mail-rules-task').default
|
||||
Utils = require '../models/utils'
|
||||
Actions = require('../actions').default
|
||||
|
||||
{ConditionMode, ConditionTemplates, ActionTemplates} = require '../../mail-rules-templates'
|
||||
|
||||
RulesJSONKey = "MailRules-V2"
|
||||
|
||||
class MailRulesStore extends NylasStore
|
||||
constructor: ->
|
||||
@_rules = [];
|
||||
try
|
||||
txt = window.localStorage.getItem(RulesJSONKey)
|
||||
if txt
|
||||
@_rules = JSON.parse()
|
||||
catch e
|
||||
console.warn("Could not load saved mail rules", e)
|
||||
|
||||
@listenTo Actions.addMailRule, @_onAddMailRule
|
||||
@listenTo Actions.deleteMailRule, @_onDeleteMailRule
|
||||
@listenTo Actions.reorderMailRule, @_onReorderMailRule
|
||||
@listenTo Actions.updateMailRule, @_onUpdateMailRule
|
||||
@listenTo Actions.disableMailRule, @_onDisableMailRule
|
||||
|
||||
rules: =>
|
||||
@_rules
|
||||
|
||||
rulesForAccountId: (accountId) =>
|
||||
@_rules.filter (f) => f.accountId is accountId
|
||||
|
||||
disabledRules: (accountId) =>
|
||||
@_rules.filter (f) => f.disabled
|
||||
|
||||
_onDeleteMailRule: (id) =>
|
||||
@_rules = @_rules.filter (f) -> f.id isnt id
|
||||
@_saveMailRules()
|
||||
@trigger()
|
||||
|
||||
_onReorderMailRule: (id, newIdx) =>
|
||||
currentIdx = _.findIndex(@_rules, _.matcher({id}))
|
||||
return if currentIdx is -1
|
||||
rule = @_rules[currentIdx]
|
||||
@_rules.splice(currentIdx, 1)
|
||||
@_rules.splice(newIdx, 0, rule)
|
||||
@_saveMailRules()
|
||||
@trigger()
|
||||
|
||||
_onAddMailRule: (properties) =>
|
||||
defaults =
|
||||
id: Utils.generateTempId()
|
||||
name: "Untitled Rule"
|
||||
conditionMode: ConditionMode.All
|
||||
conditions: [ConditionTemplates[0].createDefaultInstance()]
|
||||
actions: [ActionTemplates[0].createDefaultInstance()]
|
||||
disabled: false
|
||||
|
||||
unless properties.accountId
|
||||
throw new Error("AddMailRule: you must provide an account id.")
|
||||
|
||||
@_rules.push(_.extend(defaults, properties))
|
||||
@_saveMailRules()
|
||||
@trigger()
|
||||
|
||||
_onUpdateMailRule: (id, properties) =>
|
||||
existing = _.find @_rules, (f) -> id is f.id
|
||||
existing[key] = val for key, val of properties
|
||||
@_saveMailRules()
|
||||
@trigger()
|
||||
|
||||
_onDisableMailRule: (id, reason) =>
|
||||
existing = _.find @_rules, (f) -> id is f.id
|
||||
return if not existing or existing.disabled is true
|
||||
|
||||
# Disable the task
|
||||
existing.disabled = true
|
||||
existing.disabledReason = reason
|
||||
@_saveMailRules()
|
||||
|
||||
# Cancel all bulk processing jobs
|
||||
for task in TaskQueue.findTasks(ReprocessMailRulesTask, {})
|
||||
Actions.dequeueTask(task.id)
|
||||
|
||||
@trigger()
|
||||
|
||||
_saveMailRules: =>
|
||||
@_saveMailRulesDebounced ?= _.debounce =>
|
||||
window.localStorage.setItem(RulesJSONKey, JSON.stringify(@_rules))
|
||||
,1000
|
||||
@_saveMailRulesDebounced()
|
||||
|
||||
|
||||
module.exports = new MailRulesStore()
|
116
packages/client-app/src/flux/stores/mail-rules-store.es6
Normal file
116
packages/client-app/src/flux/stores/mail-rules-store.es6
Normal file
|
@ -0,0 +1,116 @@
|
|||
import NylasStore from 'nylas-store';
|
||||
import _ from 'underscore';
|
||||
import TaskQueue from './task-queue';
|
||||
import ReprocessMailRulesTask from '../tasks/reprocess-mail-rules-task';
|
||||
import Utils from '../models/utils';
|
||||
import Actions from '../actions';
|
||||
|
||||
import {ConditionMode, ConditionTemplates, ActionTemplates} from '../../mail-rules-templates';
|
||||
|
||||
const RulesJSONKey = "MailRules-V2"
|
||||
|
||||
class MailRulesStore extends NylasStore {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._rules = [];
|
||||
try {
|
||||
const txt = window.localStorage.getItem(RulesJSONKey);
|
||||
if (txt) {
|
||||
this._rules = JSON.parse(txt);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Could not load saved mail rules", err);
|
||||
}
|
||||
|
||||
this.listenTo(Actions.addMailRule, this._onAddMailRule);
|
||||
this.listenTo(Actions.deleteMailRule, this._onDeleteMailRule);
|
||||
this.listenTo(Actions.reorderMailRule, this._onReorderMailRule);
|
||||
this.listenTo(Actions.updateMailRule, this._onUpdateMailRule);
|
||||
this.listenTo(Actions.disableMailRule, this._onDisableMailRule);
|
||||
}
|
||||
|
||||
rules() {
|
||||
return this._rules;
|
||||
}
|
||||
|
||||
rulesForAccountId(accountId) {
|
||||
return this._rules.filter((f) => f.accountId === accountId);
|
||||
}
|
||||
|
||||
disabledRules(accountId) {
|
||||
return this._rules.filter((f) => f.accountId === accountId && f.disabled);
|
||||
}
|
||||
|
||||
_onDeleteMailRule = (id) => {
|
||||
this._rules = this._rules.filter((f) => f.id !== id);
|
||||
this._saveMailRules();
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
_onReorderMailRule = (id, newIdx) => {
|
||||
const currentIdx = _.findIndex(this._rules, _.matcher({id}))
|
||||
if (currentIdx === -1) {
|
||||
return;
|
||||
}
|
||||
const rule = this._rules[currentIdx];
|
||||
this._rules.splice(currentIdx, 1);
|
||||
this._rules.splice(newIdx, 0, rule);
|
||||
this._saveMailRules();
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
_onAddMailRule = (properties) => {
|
||||
const defaults = {
|
||||
id: Utils.generateTempId(),
|
||||
name: "Untitled Rule",
|
||||
conditionMode: ConditionMode.All,
|
||||
conditions: [ConditionTemplates[0].createDefaultInstance()],
|
||||
actions: [ActionTemplates[0].createDefaultInstance()],
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
if (!properties.accountId) {
|
||||
throw new Error("AddMailRule: you must provide an account id.");
|
||||
}
|
||||
|
||||
this._rules.push(Object.assign(defaults, properties));
|
||||
this._saveMailRules();
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
_onUpdateMailRule = (id, properties) => {
|
||||
const existing = this._rules.find(f => id === f.id);
|
||||
Object.assign(existing, properties);
|
||||
this._saveMailRules();
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
_onDisableMailRule = (id, reason) => {
|
||||
const existing = this._rules.find(f => id === f.id);
|
||||
if (!existing || existing.disabled === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the task
|
||||
existing.disabled = true;
|
||||
existing.disabledReason = reason;
|
||||
this._saveMailRules();
|
||||
|
||||
// Cancel all bulk processing jobs
|
||||
for (const task of TaskQueue.findTasks(ReprocessMailRulesTask, {})) {
|
||||
Actions.dequeueTask(task.id);
|
||||
}
|
||||
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
_saveMailRules() {
|
||||
this._saveMailRulesDebounced = this._saveMailRulesDebounced || _.debounce(() => {
|
||||
window.localStorage.setItem(RulesJSONKey, JSON.stringify(this._rules));
|
||||
}, 1000);
|
||||
this._saveMailRulesDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
export default new MailRulesStore()
|
|
@ -1,153 +0,0 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
Task = require('./flux/tasks/task').default
|
||||
Actions = require('./flux/actions').default
|
||||
Category = require('./flux/models/category').default
|
||||
Thread = require('./flux/models/thread').default
|
||||
Message = require('./flux/models/message').default
|
||||
AccountStore = require('./flux/stores/account-store').default
|
||||
DatabaseStore = require('./flux/stores/database-store').default
|
||||
TaskQueue = require('./flux/stores/task-queue').default;
|
||||
|
||||
{ConditionMode, ConditionTemplates} = require './mail-rules-templates'
|
||||
|
||||
ChangeUnreadTask = require('./flux/tasks/change-unread-task').default
|
||||
ChangeFolderTask = require('./flux/tasks/change-folder-task').default
|
||||
ChangeStarredTask = require('./flux/tasks/change-starred-task').default
|
||||
ChangeLabelsTask = require('./flux/tasks/change-labels-task').default
|
||||
MailRulesStore = null
|
||||
|
||||
###
|
||||
Note: At first glance, it seems like these task factory methods should use the
|
||||
TaskFactory. Unfortunately, the TaskFactory uses the CategoryStore and other
|
||||
information about the current view. Maybe after the unified inbox refactor...
|
||||
###
|
||||
MailRulesActions =
|
||||
markAsImportant: (message, thread) ->
|
||||
DatabaseStore.findBy(Category, {
|
||||
name: 'important',
|
||||
accountId: thread.accountId
|
||||
}).then (important) ->
|
||||
return Promise.reject(new Error("Could not find `important` label")) unless important
|
||||
return new ChangeLabelsTask(labelsToAdd: [important], labelsToRemove: [], threads: [thread.id], source: "Mail Rules")
|
||||
|
||||
moveToTrash: (message, thread) ->
|
||||
if AccountStore.accountForId(thread.accountId).usesLabels()
|
||||
return MailRulesActions.moveToLabel(message, thread, 'trash')
|
||||
else
|
||||
DatabaseStore.findBy(Category, { name: 'trash', accountId: thread.accountId }).then (folder) ->
|
||||
return Promise.reject(new Error("The folder could not be found.")) unless folder
|
||||
return new ChangeFolderTask(folder: folder, threads: [thread.id], source: "Mail Rules")
|
||||
|
||||
markAsRead: (message, thread) ->
|
||||
new ChangeUnreadTask(unread: false, threads: [thread.id], source: "Mail Rules")
|
||||
|
||||
star: (message, thread) ->
|
||||
new ChangeStarredTask(starred: true, threads: [thread.id], source: "Mail Rules")
|
||||
|
||||
changeFolder: (message, thread, value) ->
|
||||
return Promise.reject(new Error("A folder is required.")) unless value
|
||||
DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId }).then (folder) ->
|
||||
return Promise.reject(new Error("The folder could not be found.")) unless folder
|
||||
return new ChangeFolderTask(folder: folder, threads: [thread.id], source: "Mail Rules")
|
||||
|
||||
applyLabel: (message, thread, value) ->
|
||||
return Promise.reject(new Error("A label is required.")) unless value
|
||||
DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId }).then (label) ->
|
||||
return Promise.reject(new Error("The label could not be found.")) unless label
|
||||
return new ChangeLabelsTask(labelsToAdd: [label], labelsToRemove: [], threads: [thread.id], source: "Mail Rules")
|
||||
|
||||
# Should really be moveToArchive but stuck with legacy name
|
||||
applyLabelArchive: (message, thread) ->
|
||||
return MailRulesActions.moveToLabel(message, thread, 'all')
|
||||
|
||||
moveToLabel: (message, thread, nameOrId) ->
|
||||
return Promise.reject(new Error("A label is required.")) unless nameOrId
|
||||
|
||||
Promise.props(
|
||||
withId: DatabaseStore.findBy(Category, { id: nameOrId, accountId: thread.accountId })
|
||||
withName: DatabaseStore.findBy(Category, { name: nameOrId, accountId: thread.accountId })
|
||||
).then ({withId, withName}) ->
|
||||
label = withId || withName
|
||||
return Promise.reject(new Error("The label could not be found.")) unless label
|
||||
return new ChangeLabelsTask({
|
||||
source: "Mail Rules"
|
||||
labelsToRemove: [].concat(thread.labels).filter((l) =>
|
||||
!l.isLockedCategory() and l.id isnt label.id
|
||||
),
|
||||
labelsToAdd: [label],
|
||||
threads: [thread.id]
|
||||
})
|
||||
|
||||
|
||||
class MailRulesProcessor
|
||||
constructor: ->
|
||||
|
||||
processMessages: (messages) =>
|
||||
MailRulesStore ?= require './flux/stores/mail-rules-store'
|
||||
return Promise.resolve() unless messages.length > 0
|
||||
|
||||
enabledRules = MailRulesStore.rules().filter (r) -> not r.disabled
|
||||
|
||||
# When messages arrive, we process all the messages in parallel, but one
|
||||
# rule at a time. This is important, because users can order rules which
|
||||
# may do and undo a change. Ie: "Star if from Ben, Unstar if subject is "Bla"
|
||||
return Promise.each enabledRules, (rule) =>
|
||||
matching = messages.filter (message) =>
|
||||
@_checkRuleForMessage(rule, message)
|
||||
|
||||
# Rules are declared at the message level, but actions are applied to
|
||||
# threads. To ensure we don't apply the same action 50x on the same thread,
|
||||
# just process one match per thread.
|
||||
matching = _.uniq matching, false, (message) ->
|
||||
message.threadId
|
||||
|
||||
return Promise.all(
|
||||
matching.map((message) =>
|
||||
# We always pull the thread from the database, even though it may be in
|
||||
# `incoming.thread`, because rules may be modifying it as they run!
|
||||
DatabaseStore.find(Thread, message.threadId).then((thread) =>
|
||||
return console.warn("Cannot find thread #{message.threadId} to process mail rules.") unless thread
|
||||
return @_applyRuleToMessage(rule, message, thread)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
_checkRuleForMessage: (rule, message) =>
|
||||
if rule.conditionMode is ConditionMode.All
|
||||
fn = _.every
|
||||
else
|
||||
fn = _.any
|
||||
|
||||
return false unless message.accountId is rule.accountId
|
||||
|
||||
fn rule.conditions, (condition) =>
|
||||
template = _.findWhere(ConditionTemplates, {key: condition.templateKey})
|
||||
value = template.valueForMessage(message)
|
||||
template.evaluate(condition, value)
|
||||
|
||||
_applyRuleToMessage: (rule, message, thread) =>
|
||||
actionPromises = rule.actions.map (action) =>
|
||||
actionFunction = MailRulesActions[action.templateKey]
|
||||
if not actionFunction
|
||||
return Promise.reject(new Error("#{action.templateKey} is not a supported action."))
|
||||
return actionFunction(message, thread, action.value)
|
||||
|
||||
Promise.all(actionPromises).then (actionResults) ->
|
||||
performLocalPromises = []
|
||||
|
||||
actionTasks = actionResults.filter (r) -> r instanceof Task
|
||||
actionTasks.forEach (task) ->
|
||||
performLocalPromises.push TaskQueue.waitForPerformLocal(task)
|
||||
Actions.queueTask(task)
|
||||
|
||||
return Promise.all(performLocalPromises)
|
||||
|
||||
.catch (err) ->
|
||||
# Errors can occur if a mail rule specifies an invalid label or folder, etc.
|
||||
# Disable the rule. Disable the mail rule so the failure is reflected in the
|
||||
# interface.
|
||||
Actions.disableMailRule(rule.id, err.toString())
|
||||
return Promise.resolve()
|
||||
|
||||
module.exports = new MailRulesProcessor
|
206
packages/client-app/src/mail-rules-processor.es6
Normal file
206
packages/client-app/src/mail-rules-processor.es6
Normal file
|
@ -0,0 +1,206 @@
|
|||
import _ from 'underscore';
|
||||
|
||||
import Task from './flux/tasks/task';
|
||||
import Actions from './flux/actions';
|
||||
import Category from './flux/models/category';
|
||||
import Thread from './flux/models/thread';
|
||||
import Label from './flux/models/label';
|
||||
import CategoryStore from './flux/stores/category-store';
|
||||
import DatabaseStore from './flux/stores/database-store';
|
||||
import TaskQueue from './flux/stores/task-queue';
|
||||
|
||||
import {ConditionMode, ConditionTemplates} from './mail-rules-templates';
|
||||
|
||||
import ChangeUnreadTask from './flux/tasks/change-unread-task';
|
||||
import ChangeFolderTask from './flux/tasks/change-folder-task';
|
||||
import ChangeStarredTask from './flux/tasks/change-starred-task';
|
||||
import ChangeLabelsTask from './flux/tasks/change-labels-task';
|
||||
let MailRulesStore = null
|
||||
|
||||
/**
|
||||
Note: At first glance, it seems like these task factory methods should use the
|
||||
TaskFactory. Unfortunately, the TaskFactory uses the CategoryStore and other
|
||||
information about the current view. Maybe after the unified inbox refactor...
|
||||
*/
|
||||
const MailRulesActions = {
|
||||
markAsImportant: async (message, thread) => {
|
||||
const important = await DatabaseStore.findBy(Category, {
|
||||
name: 'important',
|
||||
accountId: thread.accountId,
|
||||
})
|
||||
if (!important) {
|
||||
throw new Error("Could not find `important` label");
|
||||
}
|
||||
return new ChangeLabelsTask({
|
||||
labelsToAdd: [important],
|
||||
labelsToRemove: [],
|
||||
threads: [thread.id],
|
||||
source: "Mail Rules",
|
||||
});
|
||||
},
|
||||
|
||||
moveToTrash: async (message, thread) => {
|
||||
if (CategoryStore.getInboxCategory(thread.accountId) instanceof Label) {
|
||||
return MailRulesActions.moveToLabel(message, thread, 'trash');
|
||||
}
|
||||
const folder = await DatabaseStore.findBy(Category, { name: 'trash', accountId: thread.accountId });
|
||||
if (!folder) {
|
||||
throw new Error("The folder could not be found.");
|
||||
}
|
||||
return new ChangeFolderTask({
|
||||
folder: folder,
|
||||
threads: [thread.id],
|
||||
source: "Mail Rules",
|
||||
});
|
||||
},
|
||||
|
||||
markAsRead: (message, thread) => {
|
||||
return new ChangeUnreadTask({
|
||||
unread: false,
|
||||
threads: [thread.id],
|
||||
source: "Mail Rules",
|
||||
})
|
||||
},
|
||||
|
||||
star: (message, thread) => {
|
||||
return new ChangeStarredTask({
|
||||
starred: true,
|
||||
threads: [thread.id],
|
||||
source: "Mail Rules",
|
||||
})
|
||||
},
|
||||
|
||||
changeFolder: async (message, thread, value) => {
|
||||
if (!value) {
|
||||
throw new Error("A folder is required.");
|
||||
}
|
||||
const folder = await DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId });
|
||||
if (!folder) {
|
||||
throw new Error("The folder could not be found.");
|
||||
}
|
||||
return new ChangeFolderTask({
|
||||
folder: folder,
|
||||
threads: [thread.id],
|
||||
source: "Mail Rules",
|
||||
});
|
||||
},
|
||||
|
||||
applyLabel: async (message, thread, value) => {
|
||||
if (!value) {
|
||||
throw new Error("A label is required.");
|
||||
}
|
||||
const label = await DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId });
|
||||
if (!label) {
|
||||
throw new Error("The label could not be found.");
|
||||
}
|
||||
return new ChangeLabelsTask({
|
||||
labelsToAdd: [label],
|
||||
labelsToRemove: [],
|
||||
threads: [thread.id],
|
||||
source: "Mail Rules",
|
||||
});
|
||||
},
|
||||
|
||||
// Should really be moveToArchive but stuck with legacy name
|
||||
applyLabelArchive: (message, thread) => {
|
||||
return MailRulesActions.moveToLabel(message, thread, 'all');
|
||||
},
|
||||
|
||||
moveToLabel: async (message, thread, nameOrId) => {
|
||||
if (!nameOrId) {
|
||||
throw new Error("A label is required.");
|
||||
}
|
||||
|
||||
const {withId, withName} = await Promise.props({
|
||||
withId: DatabaseStore.findBy(Category, { id: nameOrId, accountId: thread.accountId }),
|
||||
withName: DatabaseStore.findBy(Category, { name: nameOrId, accountId: thread.accountId }),
|
||||
});
|
||||
const label = withId || withName;
|
||||
if (!label) {
|
||||
throw new Error("The label could not be found.");
|
||||
}
|
||||
return new ChangeLabelsTask({
|
||||
source: "Mail Rules",
|
||||
labelsToRemove: [].concat(thread.labels).filter((l) =>
|
||||
!l.isLockedCategory() && l.id !== label.id
|
||||
),
|
||||
labelsToAdd: [label],
|
||||
threads: [thread.id],
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
class MailRulesProcessor {
|
||||
async processMessages(messages) {
|
||||
MailRulesStore = MailRulesStore || require('./flux/stores/mail-rules-store').default; //eslint-disable-line
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabledRules = MailRulesStore.rules().filter((r) => !r.disabled);
|
||||
|
||||
// When messages arrive, we process all the messages in parallel, but one
|
||||
// rule at a time. This is important, because users can order rules which
|
||||
// may do and undo a change. Ie: "Star if from Ben, Unstar if subject is "Bla"
|
||||
for (const rule of enabledRules) {
|
||||
let matching = messages.filter((message) =>
|
||||
this._checkRuleForMessage(rule, message)
|
||||
);
|
||||
|
||||
// Rules are declared at the message level, but actions are applied to
|
||||
// threads. To ensure we don't apply the same action 50x on the same thread,
|
||||
// just process one match per thread.
|
||||
matching = _.uniq(matching, false, (message) => message.threadId);
|
||||
for (const message of matching) {
|
||||
// We always pull the thread from the database, even though it may be in
|
||||
// `incoming.thread`, because rules may be modifying it as they run!
|
||||
const thread = await DatabaseStore.find(Thread, message.threadId);
|
||||
if (!thread) {
|
||||
console.warn(`Cannot find thread ${message.threadId} to process mail rules.`);
|
||||
continue;
|
||||
}
|
||||
await this._applyRuleToMessage(rule, message, thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_checkRuleForMessage(rule, message) {
|
||||
const fn = rule.conditionMode === ConditionMode.All ? Array.prototype.every : Array.prototype.some;
|
||||
if (message.accountId !== rule.accountId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fn.call(rule.conditions, (condition) => {
|
||||
const template = ConditionTemplates.find(t => t.key === condition.templateKey);
|
||||
const value = template.valueForMessage(message);
|
||||
return template.evaluate(condition, value);
|
||||
});
|
||||
}
|
||||
|
||||
async _applyRuleToMessage(rule, message, thread) {
|
||||
try {
|
||||
const actionPromises = rule.actions.map((action) => {
|
||||
const actionFn = MailRulesActions[action.templateKey]
|
||||
if (!actionFn) {
|
||||
throw new Error(`${action.templateKey} is not a supported action.`);
|
||||
}
|
||||
return actionFn(message, thread, action.value);
|
||||
});
|
||||
|
||||
const actionResults = await Promise.all(actionPromises);
|
||||
const actionTasks = actionResults.filter((r) => r instanceof Task);
|
||||
const performLocalPromises = actionTasks.map(t => TaskQueue.waitForPerformLocal(t));
|
||||
Actions.queueTasks(actionTasks);
|
||||
await performLocalPromises;
|
||||
} catch (err) {
|
||||
// Errors can occur if a mail rule specifies an invalid label or folder, etc.
|
||||
// Disable the rule. Disable the mail rule so the failure is reflected in the
|
||||
// interface.
|
||||
Actions.disableMailRule(rule.id, err.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MailRulesProcessor();
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
NylasObservables = require 'nylas-observables'
|
||||
{Template} = require './components/scenario-editor-models'
|
||||
|
||||
ConditionTemplates = [
|
||||
new Template('from', Template.Type.String, {
|
||||
name: 'From',
|
||||
valueForMessage: (message) ->
|
||||
[].concat(message.from.map((c) -> c.email), message.from.map((c) -> c.name))
|
||||
})
|
||||
|
||||
new Template('to', Template.Type.String, {
|
||||
name: 'To',
|
||||
valueForMessage: (message) ->
|
||||
[].concat(message.to.map((c) -> c.email), message.to.map((c) -> c.name))
|
||||
})
|
||||
|
||||
new Template('cc', Template.Type.String, {
|
||||
name: 'Cc',
|
||||
valueForMessage: (message) ->
|
||||
[].concat(message.cc.map((c) -> c.email), message.cc.map((c) -> c.name))
|
||||
})
|
||||
|
||||
new Template('bcc', Template.Type.String, {
|
||||
name: 'Bcc',
|
||||
valueForMessage: (message) ->
|
||||
[].concat(message.bcc.map((c) -> c.email), message.bcc.map((c) -> c.name))
|
||||
})
|
||||
|
||||
new Template('anyRecipient', Template.Type.String, {
|
||||
name: 'Any Recipient',
|
||||
valueForMessage: (message) ->
|
||||
recipients = [].concat(message.to, message.cc, message.bcc, message.from)
|
||||
[].concat(recipients.map((c) -> c.email), recipients.map((c) -> c.name))
|
||||
})
|
||||
|
||||
new Template('anyAttachmentName', Template.Type.String, {
|
||||
name: 'Any attachment name',
|
||||
valueForMessage: (message) ->
|
||||
message.files.map((f) -> f.filename)
|
||||
})
|
||||
|
||||
new Template('starred', Template.Type.Enum, {
|
||||
name: 'Starred',
|
||||
values: [{name: 'True', value: 'true'}, {name: 'False', value: 'false'}]
|
||||
valueLabel: 'is:'
|
||||
valueForMessage: (message) ->
|
||||
if message.starred then return 'true' else return 'false'
|
||||
})
|
||||
|
||||
new Template('subject', Template.Type.String, {
|
||||
name: 'Subject',
|
||||
valueForMessage: (message) ->
|
||||
message.subject
|
||||
})
|
||||
|
||||
new Template('body', Template.Type.String, {
|
||||
name: 'Body',
|
||||
valueForMessage: (message) ->
|
||||
message.body
|
||||
})
|
||||
]
|
||||
|
||||
ActionTemplates = [
|
||||
new Template('markAsRead', Template.Type.None, {name: 'Mark as Read'})
|
||||
new Template('moveToTrash', Template.Type.None, {name: 'Move to Trash'})
|
||||
new Template('star', Template.Type.None, {name: 'Star'})
|
||||
]
|
||||
|
||||
|
||||
module.exports =
|
||||
ConditionMode:
|
||||
Any: 'any'
|
||||
All: 'all'
|
||||
|
||||
ConditionTemplates: ConditionTemplates
|
||||
|
||||
ConditionTemplatesForAccount: (account) ->
|
||||
return [] unless account
|
||||
return ConditionTemplates
|
||||
|
||||
ActionTemplates: ActionTemplates
|
||||
|
||||
ActionTemplatesForAccount: (account) ->
|
||||
return [] unless account
|
||||
|
||||
templates = [].concat(ActionTemplates)
|
||||
|
||||
CategoryNamesObservable = NylasObservables.Categories
|
||||
.forAccount(account)
|
||||
.sort()
|
||||
.map (cats) ->
|
||||
cats.filter (cat) -> not cat.isLockedCategory()
|
||||
.map (cats) ->
|
||||
cats.map (cat) ->
|
||||
name: cat.displayName || cat.name
|
||||
value: cat.id
|
||||
|
||||
if account.usesLabels()
|
||||
templates.unshift new Template('markAsImportant', Template.Type.None, {
|
||||
name: 'Mark as Important'
|
||||
})
|
||||
templates.unshift new Template('applyLabelArchive', Template.Type.None, {
|
||||
name: 'Archive'
|
||||
})
|
||||
templates.unshift new Template('applyLabel', Template.Type.Enum, {
|
||||
name: 'Apply Label'
|
||||
values: CategoryNamesObservable
|
||||
})
|
||||
templates.unshift new Template('moveToLabel', Template.Type.Enum, {
|
||||
name: 'Move to Label'
|
||||
values: CategoryNamesObservable
|
||||
})
|
||||
|
||||
else
|
||||
templates.push new Template('changeFolder', Template.Type.Enum, {
|
||||
name: 'Move Message'
|
||||
valueLabel: 'to folder:'
|
||||
values: CategoryNamesObservable
|
||||
})
|
||||
|
||||
templates
|
131
packages/client-app/src/mail-rules-templates.es6
Normal file
131
packages/client-app/src/mail-rules-templates.es6
Normal file
|
@ -0,0 +1,131 @@
|
|||
import NylasObservables from 'nylas-observables';
|
||||
import Label from './flux/models/label';
|
||||
import CategoryStore from './flux/stores/category-store';
|
||||
import {Template} from './components/scenario-editor-models';
|
||||
|
||||
export const ConditionTemplates = [
|
||||
new Template('from', Template.Type.String, {
|
||||
name: 'From',
|
||||
valueForMessage: (message) =>
|
||||
[].concat(message.from.map((c) => c.email), message.from.map((c) => c.name)),
|
||||
}),
|
||||
|
||||
new Template('to', Template.Type.String, {
|
||||
name: 'To',
|
||||
valueForMessage: (message) =>
|
||||
[].concat(message.to.map((c) => c.email), message.to.map((c) => c.name)),
|
||||
}),
|
||||
|
||||
new Template('cc', Template.Type.String, {
|
||||
name: 'Cc',
|
||||
valueForMessage: (message) =>
|
||||
[].concat(message.cc.map((c) => c.email), message.cc.map((c) => c.name)),
|
||||
}),
|
||||
|
||||
new Template('bcc', Template.Type.String, {
|
||||
name: 'Bcc',
|
||||
valueForMessage: (message) =>
|
||||
[].concat(message.bcc.map((c) => c.email), message.bcc.map((c) => c.name)),
|
||||
}),
|
||||
|
||||
new Template('anyRecipient', Template.Type.String, {
|
||||
name: 'Any Recipient',
|
||||
valueForMessage: (message) => {
|
||||
const recipients = [].concat(message.to, message.cc, message.bcc, message.from)
|
||||
return [].concat(recipients.map((c) => c.email), recipients.map((c) => c.name))
|
||||
},
|
||||
}),
|
||||
|
||||
new Template('anyAttachmentName', Template.Type.String, {
|
||||
name: 'Any attachment name',
|
||||
valueForMessage: (message) =>
|
||||
message.files.map((f) => f.filename),
|
||||
}),
|
||||
|
||||
new Template('starred', Template.Type.Enum, {
|
||||
name: 'Starred',
|
||||
values: [{name: 'True', value: 'true'}, {name: 'False', value: 'false'}],
|
||||
valueLabel: 'is:',
|
||||
valueForMessage: (message) => {
|
||||
return message.starred ? 'true' : 'false';
|
||||
},
|
||||
}),
|
||||
|
||||
new Template('subject', Template.Type.String, {
|
||||
name: 'Subject',
|
||||
valueForMessage: (message) => {
|
||||
return message.subject;
|
||||
},
|
||||
}),
|
||||
|
||||
new Template('body', Template.Type.String, {
|
||||
name: 'Body',
|
||||
valueForMessage: (message) => {
|
||||
return message.body;
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export const ActionTemplates = [
|
||||
new Template('markAsRead', Template.Type.None, {name: 'Mark as Read'}),
|
||||
new Template('moveToTrash', Template.Type.None, {name: 'Move to Trash'}),
|
||||
new Template('star', Template.Type.None, {name: 'Star'}),
|
||||
]
|
||||
|
||||
|
||||
export const ConditionMode = {
|
||||
Any: 'any',
|
||||
All: 'all',
|
||||
}
|
||||
|
||||
export function ConditionTemplatesForAccount(account) {
|
||||
return account ? ConditionTemplates : [];
|
||||
}
|
||||
|
||||
export function ActionTemplatesForAccount(account) {
|
||||
if (!account) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const templates = [].concat(ActionTemplates);
|
||||
|
||||
const CategoryNamesObservable = NylasObservables.Categories
|
||||
.forAccount(account)
|
||||
.sort()
|
||||
.map((cats) =>
|
||||
cats.filter((cat) => !cat.isLockedCategory())
|
||||
)
|
||||
.map((cats) =>
|
||||
cats.map((cat) => {
|
||||
return {
|
||||
name: cat.displayName || cat.name,
|
||||
value: cat.id,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (CategoryStore.getInboxCategory(account.id) instanceof Label) {
|
||||
templates.unshift(new Template('markAsImportant', Template.Type.None, {
|
||||
name: 'Mark as Important',
|
||||
}));
|
||||
templates.unshift(new Template('applyLabelArchive', Template.Type.None, {
|
||||
name: 'Archive',
|
||||
}));
|
||||
templates.unshift(new Template('applyLabel', Template.Type.Enum, {
|
||||
name: 'Apply Label',
|
||||
values: CategoryNamesObservable,
|
||||
}));
|
||||
templates.unshift(new Template('moveToLabel', Template.Type.Enum, {
|
||||
name: 'Move to Label',
|
||||
values: CategoryNamesObservable,
|
||||
}));
|
||||
} else {
|
||||
templates.push(new Template('changeFolder', Template.Type.Enum, {
|
||||
name: 'Move Message',
|
||||
valueLabel: 'to folder:',
|
||||
values: CategoryNamesObservable,
|
||||
}));
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
Loading…
Add table
Reference in a new issue