Skip to content

Commit

Permalink
Add sortablity to the Grocery List (#44)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Rycieos and RyanNoelk authored Jul 4, 2020
1 parent f4865f2 commit f744037
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 39 deletions.
43 changes: 40 additions & 3 deletions modules/list/actions/ItemActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,25 @@ 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) {
dispatch({
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());
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 3 additions & 2 deletions modules/list/components/AddItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''});
}
};
Expand Down Expand Up @@ -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)
13 changes: 12 additions & 1 deletion modules/list/components/ListItem.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { SortableHandle } from 'react-sortable-hoc'

import {
ENTER_KEY,
ESCAPE_KEY
} from '../constants/ListStatus'
import { Checkbox } from '../../common/components/FormComponents'

const DragHandle = SortableHandle(({sortable}) =>
<div
className="drag-handle"
style={ sortable ? null : {display: 'none'} }
tabIndex="0"
/>
);

export default class ListItem extends React.Component {
constructor(props) {
super(props);
Expand Down Expand Up @@ -76,6 +85,7 @@ export default class ListItem extends React.Component {
{ this.props.item.title }
</label>
<button className="destroy" onClick={ this.handleDestroy } />
<DragHandle sortable={this.props.sortable} />
</div>
<input
ref="editField"
Expand All @@ -95,8 +105,9 @@ ListItem.propTypes = {
item: PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
completed: PropTypes.bool.isRequired,
}).isRequired,
sortable: PropTypes.bool.isRequired,
onSave: PropTypes.func.isRequired,
onDestroy: PropTypes.func.isRequired,
onToggleEdit: PropTypes.func.isRequired,
Expand Down
83 changes: 65 additions & 18 deletions modules/list/components/ListItems.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import React from 'react'
import {
SortableContainer,
SortableElement,
} from 'react-sortable-hoc'
import PropTypes from 'prop-types'

import { Checkbox } from '../../common/components/FormComponents'
Expand All @@ -13,6 +17,9 @@ import {
} from '../constants/ListStatus'

export default class ListItems extends React.Component {

slidingList: ?HTMLElement;

constructor(props) {
super(props);

Expand All @@ -34,6 +41,17 @@ export default class ListItems extends React.Component {
)
};

onSortEnd = ({oldIndex, newIndex}) => {
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});
};
Expand All @@ -54,19 +72,24 @@ export default class ListItems extends React.Component {
}
}, this);

let listItems = shownItems.map(function (item) {
return (
<ListItem
key={ item.id }
item={ item }
editing={ this.state.editing === item.id }
onToggleEdit={ this.toggleEdit }
onToggle={ this.props.itemActions.toggle }
onDestroy={ this.props.itemActions.destroy }
onSave={ this.props.itemActions.save }
/>
);
}, this);
const SortableItem = SortableElement(({ item }) =>
<ListItem
key={ item.id }
item={ item }
sortable={ this.state.nowShowing === ALL_ITEMS }
editing={ this.state.editing === item.id }
onToggleEdit={ this.toggleEdit }
onToggle={ this.props.itemActions.toggle }
onDestroy={ this.props.itemActions.destroy }
onSave={ this.props.itemActions.save }
/>
);

const SortableList = SortableContainer(({ children }) =>
<ul className="item-list">
{ children }
</ul>
);

let activeListCount = items.reduce(function (accum, item) {
return item.completed ? accum : accum + 1;
Expand All @@ -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 = (
<section className="main">
Expand All @@ -93,17 +119,37 @@ export default class ListItems extends React.Component {
checked={ activeListCount === 0 }
change={ this.toggleAll }
/>
<ul className="item-list">
{ listItems }
</ul>
<ul
ref={ ref => this.slidingList = ref }
className="item-list"
/>
<SortableList
onSortEnd={ this.onSortEnd }
axis="y"
lockAxis="y"
useDragHandle
lockToContainerEdges
helperContainer={ () => this.slidingList }
>
{ shownItems.map((item, index) =>
<SortableItem
key={ item.id }
index={ index }
item={ item }
/>
)}
</SortableList>
</section>
);
}

return (
<div>
<header className="header">
<AddItem addItem={ this.props.itemActions.add }/>
<AddItem
addItem={ this.props.itemActions.add }
listLength={ this.props.items.length }
/>
</header>
{ main }
{ footer }
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions modules/list/constants/ItemConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
40 changes: 25 additions & 15 deletions modules/list/css/_list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
13 changes: 13 additions & 0 deletions modules/list/reducers/ItemReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit f744037

Please sign in to comment.