From f744037baf6a02c60cc3366ed1c01ab2eb22beec Mon Sep 17 00:00:00 2001 From: Mark Vander Stel Date: Sat, 4 Jul 2020 15:20:28 -0400 Subject: [PATCH] Add sortablity to the Grocery List (#44) * Add sortablity to the Grocery List Extend ListItems to hold and render a list of items that will slide. Add sorting function to sort the list after a user has sorted an element, and send that to the API, so that the whole list can be updated with the new order in the DB. When adding a new item, add an order index to it so that it will end up at the end of the list (like the default behaviour from before), and send that to the API so that it will stay there if the page is refreshed. Add a handle to the ListItem to grab for sorting. It's a bit buggy on mobile interfaces right now. Make the sorting handle not display when on a tab other than "All Items". Sorting only part of the list would be someone of an undefined function, so we just won't allow it. Rework the CSS layout so that the destroy button correctly flexes around the sorting handle. This CSS rework means we can cleanup a bunch of the margin and absolute distance hacks to allow for much simpler floating of objects vertically center and margin distances from each other. Also fix editing box being a fixed width, now it fits the list size, while still having a left buffer so that the text lines up with other lines. * updates on sortable lists * Change CSS class to drag-handle This is to be less confusing, as there could be other handles that this CSS probably shouldn't apply to. Co-authored-by: ryan --- modules/list/actions/ItemActions.js | 43 ++++++++++++- modules/list/components/AddItem.js | 5 +- modules/list/components/ListItem.js | 13 +++- modules/list/components/ListItems.js | 83 +++++++++++++++++++------ modules/list/constants/ItemConstants.js | 1 + modules/list/css/_list.scss | 40 +++++++----- modules/list/reducers/ItemReducer.js | 13 ++++ package.json | 1 + yarn.lock | 18 ++++++ 9 files changed, 178 insertions(+), 39 deletions(-) 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 }) => +
    + { 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"