feat(reorder): Re-order mail rules (#1074) and accounts (#631)

Summary:
This implements EditableList re-ordering via a new prop callback.
You can drag and drop items in the mail rules list and the accounts list.
Note that you can't drag between lists - right now this is just to enable
re-ordering.

Test Plan: No new specs yet

Reviewers: evan, juan

Reviewed By: juan

Differential Revision: https://phab.nylas.com/D2495
This commit is contained in:
Ben Gotow 2016-02-01 14:06:54 -08:00
parent 7612ecc8cd
commit 0fb109aeee
8 changed files with 155 additions and 3 deletions

View file

@ -7,6 +7,7 @@ class PreferencesAccountList extends Component {
accounts: PropTypes.array,
selected: PropTypes.object,
onAddAccount: PropTypes.func.isRequired,
onReorderAccount: PropTypes.func.isRequired,
onSelectAccount: PropTypes.func.isRequired,
onRemoveAccount: PropTypes.func.isRequired,
}
@ -45,6 +46,7 @@ class PreferencesAccountList extends Component {
items={this.props.accounts}
itemContent={this._renderAccount}
selected={this.props.selected}
onReorderItem={this.props.onReorderAccount}
onCreateItem={this.props.onAddAccount}
onSelectItem={this.props.onSelectAccount}
onDeleteItem={this.props.onRemoveAccount} />

View file

@ -30,6 +30,9 @@ class PreferencesAccounts extends React.Component
ipc = require('electron').ipcRenderer
ipc.send('command', 'application:add-account')
_onReorderAccount: (account, oldIdx, newIdx) =>
Actions.reorderAccount(account.id, newIdx)
_onSelectAccount: (account) =>
@setState(selected: account)
@ -49,6 +52,7 @@ class PreferencesAccounts extends React.Component
accounts={@state.accounts}
selected={@state.selected}
onAddAccount={@_onAddAccount}
onReorderAccount={@_onReorderAccount}
onSelectAccount={@_onSelectAccount}
onRemoveAccount={@_onRemoveAccount} />
<PreferencesAccountDetails

View file

@ -86,6 +86,7 @@ class PreferencesMailRules extends React.Component
items={@state.rules}
itemContent={@_renderListItemContent}
onCreateItem={@_onAddRule}
onReorderItem={@_onReorderRule}
onDeleteItem={@_onDeleteRule}
onItemEdited={@_onRuleNameEdited}
selected={@state.selectedRule}
@ -96,7 +97,7 @@ class PreferencesMailRules extends React.Component
return <div className="item-rule-disabled">{rule.name}</div>
else
return rule.name
_renderDetail: =>
rule = @state.selectedRule
@ -177,6 +178,9 @@ class PreferencesMailRules extends React.Component
_onSelectRule: (rule, idx) =>
@setState(selectedRule: rule)
_onReorderRule: (rule, idx, newIdx) =>
Actions.reorderMailRule(rule.id, newIdx)
_onDeleteRule: (rule, idx) =>
Actions.deleteMailRule(rule.id)

View file

@ -34,6 +34,7 @@ import React, {Component, PropTypes} from 'react';
* @param {props.onCreateItem} props.onCreateItem
* @param {props.onDeleteItem} props.onDeleteItem
* @param {props.onSelectItem} props.onSelectItem
* @param {props.onReorderItem} props.onReorderItem
* @param {props.onItemEdited} props.onItemEdited
* @param {props.onItemCreated} props.onItemCreated
* @class EditableList
@ -76,6 +77,15 @@ class EditableList extends Component {
* @callback props.onItemCreated
* @param {string} value - The value for the new item
*/
/**
* If provided, the user will be able to drag and drop items to re-arrange them
* within the list. Note that dragging between lists is not supported.
* @callback props.onReorderItem
* @param {Object} item - The item that was dragged
* @param {number} startIdx - The index the item was dragged from
* @param {number} endIdx - The new index of the item, assuming it was
already removed from startIdx.
*/
static propTypes = {
items: PropTypes.array.isRequired,
itemContent: PropTypes.func,
@ -84,6 +94,7 @@ class EditableList extends Component {
createInputProps: PropTypes.object,
onCreateItem: PropTypes.func,
onDeleteItem: PropTypes.func,
onReorderItem: PropTypes.func,
onItemEdited: PropTypes.func,
onItemCreated: PropTypes.func,
initialState: PropTypes.object,
@ -107,6 +118,7 @@ class EditableList extends Component {
constructor(props) {
super(props);
this.state = props.initialState || {
dropInsertionIndex: -1,
editingIndex: null,
creatingItem: false,
};
@ -263,6 +275,70 @@ class EditableList extends Component {
}
}
_onItemDragStart = (event)=> {
if (!this.props.onReorderItem) {
event.preventDefault();
return;
}
const row = event.target.closest('[data-item-idx]') || event.target;
const wrapperId = React.findDOMNode(this.refs.itemsWrapper).dataset.reactid;
if (!row.dataset.itemIdx) {
return;
}
event.dataTransfer.setData('editablelist-index', row.dataset.itemIdx);
event.dataTransfer.setData('editablelist-reactid', wrapperId);
event.dataTransfer.effectAllowed = "move";
}
_onDragOver = (event)=> {
const wrapperNode = React.findDOMNode(this.refs.itemsWrapper);
const originWrapperId = event.dataTransfer.getData('editablelist-reactid')
const originSameList = (originWrapperId === wrapperNode.dataset.reactid);
let dropInsertionIndex = 0;
if ((event.currentTarget === wrapperNode) && originSameList) {
const itemNodes = wrapperNode.querySelectorAll('[data-item-idx]')
for (let i = 0; i < itemNodes.length; i ++) {
const itemNode = itemNodes[i];
const rect = itemNode.getBoundingClientRect();
if (event.clientY > rect.top + (rect.height / 2)) {
dropInsertionIndex = itemNode.dataset.itemIdx / 1 + 1;
} else {
break;
}
}
} else {
dropInsertionIndex = -1;
}
if (this.state.dropInsertionIndex !== dropInsertionIndex) {
this.setState({dropInsertionIndex: dropInsertionIndex});
}
}
_onDragLeave = ()=> {
this.setState({dropInsertionIndex: -1});
}
_onDrop = (event)=> {
if (this.state.dropInsertionIndex !== -1) {
const startIdx = event.dataTransfer.getData('editablelist-index');
if (startIdx && (this.state.dropInsertionIndex !== startIdx)) {
const item = this.props.items[startIdx];
let endIdx = this.state.dropInsertionIndex;
if (endIdx > startIdx) {
endIdx -= 1
}
this.props.onReorderItem(item, startIdx, endIdx);
this.setState({dropInsertionIndex: -1});
}
}
}
// Renderers
@ -327,6 +403,9 @@ class EditableList extends Component {
<div
className={classes}
key={idx}
data-item-idx={idx}
draggable
onDragStart={this._onItemDragStart}
onClick={_.partial(onClick, _, item, idx)}
onDoubleClick={_.partial(onEdit, _, item, idx)}>
{itemContent}
@ -357,12 +436,22 @@ class EditableList extends Component {
);
}
_renderDropInsertion = ()=> {
return (
<div className="insertion-point"><div></div></div>
)
}
render() {
let items = this.props.items.map( (item, idx)=> this._renderItem(item, idx));
if (this.state.creatingItem === true) {
items = items.concat(this._renderCreateInput());
}
if (this.state.dropInsertionIndex !== -1) {
items.splice(this.state.dropInsertionIndex, 0, this._renderDropInsertion());
}
return (
<KeyCommandsRegion
tabIndex="1"
@ -370,7 +459,10 @@ class EditableList extends Component {
className={`nylas-editable-list ${this.props.className}`}>
<ScrollRegion
className="items-wrapper"
ref="itemsWrapper" >
ref="itemsWrapper"
onDragOver={this._onDragOver}
onDragLeave={this._onDragLeave}
onDrop={this._onDrop}>
{items}
</ScrollRegion>
{this._renderButtons()}

View file

@ -167,6 +167,17 @@ class Actions
###
@updateAccount: ActionScopeWindow
###
Public: Re-order the provided account in the account list.
*Scope: Window*
```
Actions.reorderAccount(account.id, newIndex)
```
###
@reorderAccount: ActionScopeWindow
###
Public: Select the provided sheet in the current window. This action changes
the top level sheet.
@ -487,6 +498,7 @@ class Actions
@recordUserEvent: ActionScopeWindow
@addMailRule: ActionScopeWindow
@reorderMailRule: ActionScopeWindow
@updateMailRule: ActionScopeWindow
@deleteMailRule: ActionScopeWindow
@disableMailRule: ActionScopeWindow

View file

@ -26,6 +26,7 @@ class AccountStore
@_load()
@listenTo Actions.removeAccount, @_onRemoveAccount
@listenTo Actions.updateAccount, @_onUpdateAccount
@listenTo Actions.reorderAccount, @_onReorderAccount
@_caches = {}
@ -81,6 +82,16 @@ class AccountStore
else
@trigger()
_onReorderAccount: (id, newIdx) =>
existingIdx = _.findIndex @_accounts, (a) -> a.id is id
return if existingIdx is -1
account = @_accounts[existingIdx]
@_caches = {}
@_accounts.splice(existingIdx, 1)
@_accounts.splice(newIdx, 0, account)
@_save()
@trigger()
addAccountFromJSON: (json) =>
if not json.email_address or not json.provider
console.error("Returned account data is invalid", json)

View file

@ -14,7 +14,7 @@ RulesJSONBlobKey = "MailRules-V2"
class MailRulesStore extends NylasStore
constructor: ->
@_rules = []
query = DatabaseStore.findJSONBlob(RulesJSONBlobKey)
@_subscription = Rx.Observable.fromQuery(query).subscribe (rules) =>
@_rules = rules ? []
@ -22,6 +22,7 @@ class MailRulesStore extends NylasStore
@listenTo Actions.addMailRule, @_onAddMailRule
@listenTo Actions.deleteMailRule, @_onDeleteMailRule
@listenTo Actions.reorderMailRule, @_onReorderMailRule
@listenTo Actions.updateMailRule, @_onUpdateMailRule
@listenTo Actions.disableMailRule, @_onDisableMailRule
@listenTo Actions.notificationActionTaken, @_onNotificationActionTaken
@ -37,6 +38,15 @@ class MailRulesStore extends NylasStore
@_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()

View file

@ -21,6 +21,23 @@
color: @component-active-bg;
}
.insertion-point {
display:block;
width:100%;
height: 0;
position: relative;
pointer-events: none;
div {
height:2px;
width: 100%;
position: absolute;
top: -1px;
background-color: @component-active-color;
box-shadow: 0 0 1px fade(@component-active-color, 50%);
}
}
.edit-icon {
display: none;
cursor: pointer;