Add sorting to sketches #789 (#910)

* reselect added

* Added Reselect Sorting

* Refactor App

* added svgs

* Refactor

* Fixed Issues

* re: #789, update sorting styling, create sorting actions and reducers, add sort by sketch name

* re #789, change names of svg icons

* re: #789, use orderBy instead of sortBy, fix styling jumps
This commit is contained in:
siddhant 2019-06-07 02:47:33 +05:30 committed by Cassie Tarakajian
parent 6f1b6fd51c
commit 8caeb0d439
12 changed files with 219 additions and 22 deletions

View file

@ -121,6 +121,9 @@ export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING'; export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
export const SET_ASSETS = 'SET_ASSETS'; export const SET_ASSETS = 'SET_ASSETS';
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
export const SET_SORTING = 'SET_SORTING';
export const START_LOADING = 'START_LOADING'; export const START_LOADING = 'START_LOADING';
export const STOP_LOADING = 'STOP_LOADING'; export const STOP_LOADING = 'STOP_LOADING';

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M49.761,67.969l-17.36-30.241h34.561L49.761,67.969z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 568 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M49.761,37.728l17.36,30.241H32.561L49.761,37.728z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View file

@ -13,6 +13,7 @@ require('./images/p5js-square-logo.png');
const history = browserHistory; const history = browserHistory;
const initialState = window.__INITIAL_STATE__; const initialState = window.__INITIAL_STATE__;
const store = configureStore(initialState); const store = configureStore(initialState);
const App = () => ( const App = () => (

View file

@ -0,0 +1,27 @@
import * as ActionTypes from '../../../constants';
export const DIRECTION = {
ASC: 'ASCENDING',
DESC: 'DESCENDING'
};
export function setSorting(field, direction) {
return {
type: ActionTypes.SET_SORTING,
payload: {
field,
direction
}
};
}
export function resetSorting() {
return setSorting('createdAt', DIRECTION.DESC);
}
export function toggleDirectionForField(field) {
return {
type: ActionTypes.TOGGLE_DIRECTION,
field
};
}

View file

@ -6,17 +6,24 @@ import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { browserHistory, Link } from 'react-router'; import { browserHistory, Link } from 'react-router';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import classNames from 'classnames';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import * as SketchActions from '../actions/projects'; import * as SketchActions from '../actions/projects';
import * as ToastActions from '../actions/toast'; import * as ToastActions from '../actions/toast';
import * as SortingActions from '../actions/sorting';
import getSortedSketches from '../selectors/projects';
import Loader from '../../App/components/loader'; import Loader from '../../App/components/loader';
const trashCan = require('../../../images/trash-can.svg'); const trashCan = require('../../../images/trash-can.svg');
const arrowUp = require('../../../images/sort-arrow-up.svg');
const arrowDown = require('../../../images/sort-arrow-down.svg');
class SketchList extends React.Component { class SketchList extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.props.getProjects(this.props.username); this.props.getProjects(this.props.username);
this.props.resetSorting();
this._renderFieldHeader = this._renderFieldHeader.bind(this);
} }
getSketchesTitle() { getSketchesTitle() {
@ -30,18 +37,39 @@ class SketchList extends React.Component {
return !this.props.loading && this.props.sketches.length > 0; return !this.props.loading && this.props.sketches.length > 0;
} }
renderLoader() { _renderLoader() {
if (this.props.loading) return <Loader />; if (this.props.loading) return <Loader />;
return null; return null;
} }
renderEmptyTable() { _renderEmptyTable() {
if (!this.props.loading && this.props.sketches.length === 0) { if (!this.props.loading && this.props.sketches.length === 0) {
return (<p className="sketches-table__empty">No sketches.</p>); return (<p className="sketches-table__empty">No sketches.</p>);
} }
return null; return null;
} }
_renderFieldHeader(fieldName, displayName) {
const { field, direction } = this.props.sorting;
const headerClass = classNames({
'sketches-table__header': true,
'sketches-table__header--selected': field === fieldName
});
return (
<th scope="col">
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
<span className={headerClass}>{displayName}</span>
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
<InlineSVG src={arrowUp} />
}
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
<InlineSVG src={arrowDown} />
}
</button>
</th>
);
}
render() { render() {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username; const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
return ( return (
@ -49,16 +77,16 @@ class SketchList extends React.Component {
<Helmet> <Helmet>
<title>{this.getSketchesTitle()}</title> <title>{this.getSketchesTitle()}</title>
</Helmet> </Helmet>
{this.renderLoader()} {this._renderLoader()}
{this.renderEmptyTable()} {this._renderEmptyTable()}
{this.hasSketches() && {this.hasSketches() &&
<table className="sketches-table" summary="table containing all saved projects"> <table className="sketches-table" summary="table containing all saved projects">
<thead> <thead>
<tr> <tr>
<th className="sketch-list__trash-column" scope="col"></th> <th className="sketch-list__trash-column" scope="col"></th>
<th scope="col">Sketch</th> {this._renderFieldHeader('name', 'Sketch')}
<th scope="col">Date created</th> {this._renderFieldHeader('createdAt', 'Date Created')}
<th scope="col">Date updated</th> {this._renderFieldHeader('updatedAt', 'Date Updated')}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -112,7 +140,13 @@ SketchList.propTypes = {
})).isRequired, })).isRequired,
username: PropTypes.string, username: PropTypes.string,
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
deleteProject: PropTypes.func.isRequired deleteProject: PropTypes.func.isRequired,
toggleDirectionForField: PropTypes.func.isRequired,
resetSorting: PropTypes.func.isRequired,
sorting: PropTypes.shape({
field: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired
}).isRequired,
}; };
SketchList.defaultProps = { SketchList.defaultProps = {
@ -122,13 +156,14 @@ SketchList.defaultProps = {
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
user: state.user, user: state.user,
sketches: state.sketches, sketches: getSortedSketches(state),
loading: state.loading, sorting: state.sorting,
loading: state.loading
}; };
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return bindActionCreators(Object.assign({}, SketchActions, ProjectActions, ToastActions), dispatch); return bindActionCreators(Object.assign({}, SketchActions, ProjectActions, ToastActions, SortingActions), dispatch);
} }
export default connect(mapStateToProps, mapDispatchToProps)(SketchList); export default connect(mapStateToProps, mapDispatchToProps)(SketchList);

View file

@ -0,0 +1,29 @@
import * as ActionTypes from '../../../constants';
import { DIRECTION } from '../actions/sorting';
const initialState = {
field: 'createdAt',
direction: DIRECTION.DESC
};
const sorting = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.TOGGLE_DIRECTION:
if (action.field && action.field !== state.field) {
if (action.field === 'name') {
return { ...state, field: action.field, direction: DIRECTION.ASC };
}
return { ...state, field: action.field, direction: DIRECTION.DESC };
}
if (state.direction === DIRECTION.ASC) {
return { ...state, direction: DIRECTION.DESC };
}
return { ...state, direction: DIRECTION.ASC };
case ActionTypes.SET_SORTING:
return { ...state, field: action.payload.field, direction: action.payload.direction };
default:
return state;
}
};
export default sorting;

View file

@ -0,0 +1,32 @@
import { createSelector } from 'reselect';
import differenceInMilliseconds from 'date-fns/difference_in_milliseconds';
import orderBy from 'lodash/orderBy';
import { DIRECTION } from '../actions/sorting';
const getSketches = state => state.sketches;
const getField = state => state.sorting.field;
const getDirection = state => state.sorting.direction;
const getSortedSketches = createSelector(
getSketches,
getField,
getDirection,
(sketches, field, direction) => {
if (field === 'name') {
if (direction === DIRECTION.DESC) {
return orderBy(sketches, 'name', 'desc');
}
return orderBy(sketches, 'name', 'asc');
}
const sortedSketches = [...sketches].sort((a, b) => {
const result =
direction === DIRECTION.ASC
? differenceInMilliseconds(new Date(a[field]), new Date(b[field]))
: differenceInMilliseconds(new Date(b[field]), new Date(a[field]));
return result;
});
return sortedSketches;
}
);
export default getSortedSketches;

View file

@ -10,6 +10,7 @@ import sketches from './modules/IDE/reducers/projects';
import toast from './modules/IDE/reducers/toast'; import toast from './modules/IDE/reducers/toast';
import console from './modules/IDE/reducers/console'; import console from './modules/IDE/reducers/console';
import assets from './modules/IDE/reducers/assets'; import assets from './modules/IDE/reducers/assets';
import sorting from './modules/IDE/reducers/sorting';
import loading from './modules/IDE/reducers/loading'; import loading from './modules/IDE/reducers/loading';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
@ -20,6 +21,7 @@ const rootReducer = combineReducers({
user, user,
project, project,
sketches, sketches,
sorting,
editorAccessibility, editorAccessibility,
toast, toast,
console, console,

View file

@ -19,6 +19,31 @@
height: #{32 / $base-font-size}rem; height: #{32 / $base-font-size}rem;
} }
.sketch-list__sort-button {
display: flex;
align-items: center;
height: #{35 / $base-font-size}rem;
& svg {
@include themify() {
fill: getThemifyVariable('inactive-text-color')
}
}
}
.sketches-table__header {
border-bottom: 2px dashed transparent;
padding: #{3 / $base-font-size}rem 0;
@include themify() {
color: getThemifyVariable('inactive-text-color')
}
}
.sketches-table__header--selected {
@include themify() {
border-color: getThemifyVariable('logo-color');
}
}
.sketches-table__row { .sketches-table__row {
margin: #{10 / $base-font-size}rem; margin: #{10 / $base-font-size}rem;
height: #{72 / $base-font-size}rem; height: #{72 / $base-font-size}rem;

46
package-lock.json generated
View file

@ -5462,7 +5462,8 @@
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true "bundled": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -5480,11 +5481,13 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true "bundled": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -5497,15 +5500,18 @@
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true "bundled": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true "bundled": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true "bundled": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -5608,7 +5614,8 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true "bundled": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -5618,6 +5625,7 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -5630,17 +5638,20 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true "bundled": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -5657,6 +5668,7 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -5729,7 +5741,8 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true "bundled": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -5739,6 +5752,7 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -5814,7 +5828,8 @@
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true "bundled": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -5844,6 +5859,7 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -5861,6 +5877,7 @@
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -5899,11 +5916,13 @@
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true "bundled": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true "bundled": true,
"optional": true
} }
} }
}, },
@ -14100,6 +14119,11 @@
"semver": "^5.1.0" "semver": "^5.1.0"
} }
}, },
"reselect": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
},
"resolve": { "resolve": {
"version": "1.11.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",

View file

@ -161,6 +161,7 @@
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"request": "^2.88.0", "request": "^2.88.0",
"request-promise": "^4.1.1", "request-promise": "^4.1.1",
"reselect": "^4.0.0",
"s3": "^4.4.0", "s3": "^4.4.0",
"s3-policy": "^0.2.0", "s3-policy": "^0.2.0",
"sass-extract": "^2.1.0", "sass-extract": "^2.1.0",