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 }
/>
-
+ 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"