From 0fb109aeee46ffbbd22f3e4af266ab3b4ae29b93 Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Mon, 1 Feb 2016 14:06:54 -0800 Subject: [PATCH] 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 --- .../lib/tabs/preferences-account-list.jsx | 2 + .../lib/tabs/preferences-accounts.cjsx | 4 + .../lib/tabs/preferences-mail-rules.cjsx | 6 +- src/components/editable-list.jsx | 94 ++++++++++++++++++- src/flux/actions.coffee | 12 +++ src/flux/stores/account-store.coffee | 11 +++ src/flux/stores/mail-rules-store.coffee | 12 ++- static/components/editable-list.less | 17 ++++ 8 files changed, 155 insertions(+), 3 deletions(-) diff --git a/internal_packages/preferences/lib/tabs/preferences-account-list.jsx b/internal_packages/preferences/lib/tabs/preferences-account-list.jsx index f1e3bfec3..1b18e94bc 100644 --- a/internal_packages/preferences/lib/tabs/preferences-account-list.jsx +++ b/internal_packages/preferences/lib/tabs/preferences-account-list.jsx @@ -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} /> diff --git a/internal_packages/preferences/lib/tabs/preferences-accounts.cjsx b/internal_packages/preferences/lib/tabs/preferences-accounts.cjsx index 847797a8f..cb9d4c6e7 100644 --- a/internal_packages/preferences/lib/tabs/preferences-accounts.cjsx +++ b/internal_packages/preferences/lib/tabs/preferences-accounts.cjsx @@ -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} /> {rule.name} 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) diff --git a/src/components/editable-list.jsx b/src/components/editable-list.jsx index 4cf9894d7..b39677d5d 100644 --- a/src/components/editable-list.jsx +++ b/src/components/editable-list.jsx @@ -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 {
{itemContent} @@ -357,12 +436,22 @@ class EditableList extends Component { ); } + _renderDropInsertion = ()=> { + return ( +
+ ) + } + 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 ( + ref="itemsWrapper" + onDragOver={this._onDragOver} + onDragLeave={this._onDragLeave} + onDrop={this._onDrop}> {items} {this._renderButtons()} diff --git a/src/flux/actions.coffee b/src/flux/actions.coffee index 3f2fff8df..1759347a3 100644 --- a/src/flux/actions.coffee +++ b/src/flux/actions.coffee @@ -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 diff --git a/src/flux/stores/account-store.coffee b/src/flux/stores/account-store.coffee index fe195e681..54d8dfa66 100644 --- a/src/flux/stores/account-store.coffee +++ b/src/flux/stores/account-store.coffee @@ -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) diff --git a/src/flux/stores/mail-rules-store.coffee b/src/flux/stores/mail-rules-store.coffee index e8de887a1..b91276590 100644 --- a/src/flux/stores/mail-rules-store.coffee +++ b/src/flux/stores/mail-rules-store.coffee @@ -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() diff --git a/static/components/editable-list.less b/static/components/editable-list.less index a4ea847f8..ff7c6f9d7 100644 --- a/static/components/editable-list.less +++ b/static/components/editable-list.less @@ -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;