diff --git a/modules/list/actions/ItemActions.js b/modules/list/actions/ItemActions.js index 4815c039..bd5368b4 100644 --- a/modules/list/actions/ItemActions.js +++ b/modules/list/actions/ItemActions.js @@ -19,13 +19,16 @@ export const load = (listId) => { } }; -export const add = (title, listId) => { +/* Add a new object, with it's order at the end of the list + */ +export const add = (title, listLength, listId) => { return (dispatch) => { request() .post(serverURLs.list_item) .send({ title: title, - list: listId + list: listId, + order: listLength, }) .end((err, res) => { if (!err && res) { @@ -33,7 +36,8 @@ export const add = (title, listId) => { type: ItemConstants.ITEM_ADD, listId: listId, id: res.body.id, - title: res.body.title + title: res.body.title, + order: res.body.order, }); } else { console.error(err.toString()); @@ -114,6 +118,39 @@ export const toggleAll = (items, checked, listId) => { } }; +/* Order all items in the order that they were passed in + */ +export const orderAll = (items, listId) => { + let order = 0; + let orderedListItems = [...items].map(item => ({ + id: item.id, + order: order++, + })); + + return (dispatch) => { + // Update the list before we send teh request, so there is no weird delay or jumpy items + dispatch({ + type: ItemConstants.ITEM_ORDER_ALL, + listId: listId, + ids: orderedListItems + }); + + request() + .patch(serverURLs.bulk_list_item) + .send(orderedListItems) + .catch(err => { + console.error(err.toString()); + console.error(err.body); + // If the API throws an error, reset the order of the items + dispatch({ + type: ItemConstants.ITEM_ORDER_ALL, + listId: listId, + ids: items + }); + }) + } +}; + export const destroy = (id, listId) => { return (dispatch) => { request() diff --git a/modules/list/components/AddItem.js b/modules/list/components/AddItem.js index fcfdd2ad..5a1a76f6 100755 --- a/modules/list/components/AddItem.js +++ b/modules/list/components/AddItem.js @@ -27,7 +27,7 @@ class AddItem extends React.Component { event.preventDefault(); let val = this.state.title.trim(); if (val) { - this.props.addItem(val); + this.props.addItem(val, this.props.listLength); this.setState({title: ''}); } }; @@ -57,7 +57,8 @@ class AddItem extends React.Component { AddItem.propTypes = { addItem: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired + intl: PropTypes.object.isRequired, + listLength: PropTypes.number.isRequired, }; export default injectIntl(AddItem) diff --git a/modules/list/components/ListItem.js b/modules/list/components/ListItem.js index fcb894aa..e58a7508 100755 --- a/modules/list/components/ListItem.js +++ b/modules/list/components/ListItem.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' +import { SortableHandle } from 'react-sortable-hoc' import { ENTER_KEY, @@ -8,6 +9,14 @@ import { } from '../constants/ListStatus' import { Checkbox } from '../../common/components/FormComponents' +const DragHandle = SortableHandle(({sortable}) => +
+); + export default class ListItem extends React.Component { constructor(props) { super(props); @@ -76,6 +85,7 @@ export default class ListItem extends React.Component { { this.props.item.title }
{ + let items = [...this.props.items]; + let item = items.splice(oldIndex, 1)[0]; + items.splice(newIndex, 0, item); + + this.props.itemActions.orderAll( + items, + this.props.activeListID + ); + }; + filterStatus = (status) => { this.setState({nowShowing: status}); }; @@ -54,19 +72,24 @@ export default class ListItems extends React.Component { } }, this); - let listItems = shownItems.map(function (item) { - return ( - - ); - }, this); + const SortableItem = SortableElement(({ item }) => + + ); + + const SortableList = SortableContainer(({ children }) => + + ); let activeListCount = items.reduce(function (accum, item) { return item.completed ? accum : accum + 1; @@ -85,6 +108,9 @@ export default class ListItems extends React.Component { />; } + /* Include a second list tag that we can attach the cloned list items to + * so that it will have the same styling. + */ if (items.length) { main = (
@@ -93,9 +119,26 @@ export default class ListItems extends React.Component { checked={ activeListCount === 0 } change={ this.toggleAll } /> -
    - { listItems } -
+
    this.slidingList = ref } + className="item-list" + /> + this.slidingList } + > + { shownItems.map((item, index) => + + )} +
); } @@ -103,7 +146,10 @@ export default class ListItems extends React.Component { return (
- +
{ main } { footer } @@ -116,7 +162,8 @@ ListItems.propTypes = { items: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, - completed: PropTypes.bool.isRequired + completed: PropTypes.bool.isRequired, + order: PropTypes.number.isRequired, }).isRequired).isRequired, activeListID: PropTypes.number, itemActions: PropTypes.object.isRequired, diff --git a/modules/list/constants/ItemConstants.js b/modules/list/constants/ItemConstants.js index ef4a9687..5ea96bab 100644 --- a/modules/list/constants/ItemConstants.js +++ b/modules/list/constants/ItemConstants.js @@ -4,6 +4,7 @@ export default { ITEM_SAVE: 'LIST_ITEM_SAVE', ITEM_TOGGLE: 'LIST_ITEM_TOGGLE', ITEM_TOGGLE_ALL: 'LIST_ITEM_TOGGLE_ALL', + ITEM_ORDER_ALL: 'LIST_ITEM_ORDER_ALL', ITEM_DELETE: 'LIST_ITEM_DELETE', ITEM_DELETE_COMPLETED: 'LIST_ITEM_DELETE_COMPLETED', ITEM_INDEX: 'ITEM', diff --git a/modules/list/css/_list.scss b/modules/list/css/_list.scss index 20288a04..a60c6860 100644 --- a/modules/list/css/_list.scss +++ b/modules/list/css/_list.scss @@ -102,11 +102,9 @@ border-bottom: none; } &.editing { - padding: 0; + padding: 0 0 0 53px; .edit { display: block; - width: 506px; - margin: 0 0 0 43px; } .view { display: none; @@ -125,15 +123,23 @@ display: block; } .view { - margin-left: 50px; - line-height: 30px; + display: flex; + line-height: 40px; .toggle { - display: inline; + order: 1; + margin: auto 20px; + .check-container { + margin: 0; + } .checkmark { - margin-top: 3px; + display: block; + position: relative; } } .item { + order: 2; + flex-grow: 1; + margin: auto 0; white-space: pre-line; word-break: break-all; transition: color 0.4s; @@ -142,16 +148,10 @@ } .destroy { display: none; - position: absolute; - top: 0; - right: 10px; - bottom: 0; - width: 40px; - height: 25px; - margin: auto 0; + order: 3; + margin: auto 15px; font-size: 30px; color: #cc9a9a; - margin-bottom: 11px; transition: color 0.2s ease-out; &:hover { color: #af5b5e; @@ -160,6 +160,16 @@ content: '×'; } } + .drag-handle { + order: 4; + width: 18px; + height: 12px; + margin: auto 15px; + opacity: .25; + cursor: row-resize; + background: -webkit-linear-gradient(top,#000,#000 20%,#fff 0,#fff 40%,#000 0,#000 60%,#fff 0,#fff 80%,#000 0,#000); + background: linear-gradient(180deg,#000,#000 20%,#fff 0,#fff 40%,#000 0,#000 60%,#fff 0,#fff 80%,#000 0,#000); + } .edit { display: none; } diff --git a/modules/list/reducers/ItemReducer.js b/modules/list/reducers/ItemReducer.js index 2cab49f2..420af8e4 100644 --- a/modules/list/reducers/ItemReducer.js +++ b/modules/list/reducers/ItemReducer.js @@ -34,6 +34,19 @@ const items = (state = [], action) => { { ...item, completed: !item.completed } : item ); + case ItemConstants.ITEM_ORDER_ALL: + let order_ids = new Map(); + for (let i in action.ids) { + order_ids.set(action.ids[i].id, action.ids[i].order); + } + + return state.map(item => + order_ids.has(item.id) ? + { ...item, order: order_ids.get(item.id) } : + item + ).sort((first, second) => + first.order - second.order + ); case ItemConstants.ITEM_DELETE: return state.filter(t => t.id !== action.id); case ItemConstants.ITEM_DELETE_COMPLETED: diff --git a/package.json b/package.json index 50ded0d7..64259b52 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "react-dom": "^16.8.3", "react-intl": "^2.1.5", "react-redux": "^5.0.6", + "react-sortable-hoc": "^1.11.0", "react-router-bootstrap": "^0.24.4", "react-router-dom": "^4.2.2", "react-select": "^2.4.1", diff --git a/yarn.lock b/yarn.lock index b58f13db..53873ce3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,12 @@ dependencies: regenerator-runtime "^0.12.0" +"@babel/runtime@^7.2.0": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" @@ -6675,6 +6681,14 @@ react-select@^2.4.1: react-input-autosize "^2.2.1" react-transition-group "^2.2.1" +react-sortable-hoc@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-1.11.0.tgz#fe4022362bbafc4b836f5104b9676608a40a278f" + dependencies: + "@babel/runtime" "^7.2.0" + invariant "^2.2.4" + prop-types "^15.5.7" + react-spinkit@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/react-spinkit/-/react-spinkit-2.1.2.tgz#55c037bd73e99e4b69bf2e37c6227474e74a99f6" @@ -6841,6 +6855,10 @@ regenerator-runtime@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" +regenerator-runtime@^0.13.4: + version "0.13.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"