From 8caeb0d439ad2cf4fec34c89af112645c7917270 Mon Sep 17 00:00:00 2001 From: siddhant <30566406+siddhant1@users.noreply.github.com> Date: Fri, 7 Jun 2019 02:47:33 +0530 Subject: [PATCH] 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 --- client/constants.js | 3 ++ client/images/sort-arrow-down.svg | 9 ++++ client/images/sort-arrow-up.svg | 9 ++++ client/index.jsx | 1 + client/modules/IDE/actions/sorting.js | 27 ++++++++++ client/modules/IDE/components/SketchList.jsx | 57 ++++++++++++++++---- client/modules/IDE/reducers/sorting.js | 29 ++++++++++ client/modules/IDE/selectors/projects.js | 32 +++++++++++ client/reducers.js | 2 + client/styles/components/_sketch-list.scss | 25 +++++++++ package-lock.json | 46 ++++++++++++---- package.json | 1 + 12 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 client/images/sort-arrow-down.svg create mode 100644 client/images/sort-arrow-up.svg create mode 100644 client/modules/IDE/actions/sorting.js create mode 100644 client/modules/IDE/reducers/sorting.js create mode 100644 client/modules/IDE/selectors/projects.js diff --git a/client/constants.js b/client/constants.js index 5facb1f3..c4dc80ae 100644 --- a/client/constants.js +++ b/client/constants.js @@ -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 SET_ASSETS = 'SET_ASSETS'; +export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION'; +export const SET_SORTING = 'SET_SORTING'; + export const START_LOADING = 'START_LOADING'; export const STOP_LOADING = 'STOP_LOADING'; diff --git a/client/images/sort-arrow-down.svg b/client/images/sort-arrow-down.svg new file mode 100644 index 00000000..f9a1fc8e --- /dev/null +++ b/client/images/sort-arrow-down.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/client/images/sort-arrow-up.svg b/client/images/sort-arrow-up.svg new file mode 100644 index 00000000..4fc7ca5d --- /dev/null +++ b/client/images/sort-arrow-up.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/client/index.jsx b/client/index.jsx index 0cecf42b..980b4ec1 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -13,6 +13,7 @@ require('./images/p5js-square-logo.png'); const history = browserHistory; const initialState = window.__INITIAL_STATE__; + const store = configureStore(initialState); const App = () => ( diff --git a/client/modules/IDE/actions/sorting.js b/client/modules/IDE/actions/sorting.js new file mode 100644 index 00000000..0e306b46 --- /dev/null +++ b/client/modules/IDE/actions/sorting.js @@ -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 + }; +} diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 79d5394f..f51a815a 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -6,17 +6,24 @@ import InlineSVG from 'react-inlinesvg'; import { connect } from 'react-redux'; import { browserHistory, Link } from 'react-router'; import { bindActionCreators } from 'redux'; +import classNames from 'classnames'; import * as ProjectActions from '../actions/project'; import * as SketchActions from '../actions/projects'; import * as ToastActions from '../actions/toast'; +import * as SortingActions from '../actions/sorting'; +import getSortedSketches from '../selectors/projects'; import Loader from '../../App/components/loader'; 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 { constructor(props) { super(props); this.props.getProjects(this.props.username); + this.props.resetSorting(); + this._renderFieldHeader = this._renderFieldHeader.bind(this); } getSketchesTitle() { @@ -30,18 +37,39 @@ class SketchList extends React.Component { return !this.props.loading && this.props.sketches.length > 0; } - renderLoader() { + _renderLoader() { if (this.props.loading) return ; return null; } - renderEmptyTable() { + _renderEmptyTable() { if (!this.props.loading && this.props.sketches.length === 0) { return (

No sketches.

); } 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 ( + + + + ); + } + render() { const username = this.props.username !== undefined ? this.props.username : this.props.user.username; return ( @@ -49,16 +77,16 @@ class SketchList extends React.Component { {this.getSketchesTitle()} - {this.renderLoader()} - {this.renderEmptyTable()} + {this._renderLoader()} + {this._renderEmptyTable()} {this.hasSketches() && - - - + {this._renderFieldHeader('name', 'Sketch')} + {this._renderFieldHeader('createdAt', 'Date Created')} + {this._renderFieldHeader('updatedAt', 'Date Updated')} @@ -112,7 +140,13 @@ SketchList.propTypes = { })).isRequired, username: PropTypes.string, 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 = { @@ -122,13 +156,14 @@ SketchList.defaultProps = { function mapStateToProps(state) { return { user: state.user, - sketches: state.sketches, - loading: state.loading, + sketches: getSortedSketches(state), + sorting: state.sorting, + loading: state.loading }; } 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); diff --git a/client/modules/IDE/reducers/sorting.js b/client/modules/IDE/reducers/sorting.js new file mode 100644 index 00000000..91c7addd --- /dev/null +++ b/client/modules/IDE/reducers/sorting.js @@ -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; diff --git a/client/modules/IDE/selectors/projects.js b/client/modules/IDE/selectors/projects.js new file mode 100644 index 00000000..9ff655d3 --- /dev/null +++ b/client/modules/IDE/selectors/projects.js @@ -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; diff --git a/client/reducers.js b/client/reducers.js index ef4d39c3..057dbd62 100644 --- a/client/reducers.js +++ b/client/reducers.js @@ -10,6 +10,7 @@ import sketches from './modules/IDE/reducers/projects'; import toast from './modules/IDE/reducers/toast'; import console from './modules/IDE/reducers/console'; import assets from './modules/IDE/reducers/assets'; +import sorting from './modules/IDE/reducers/sorting'; import loading from './modules/IDE/reducers/loading'; const rootReducer = combineReducers({ @@ -20,6 +21,7 @@ const rootReducer = combineReducers({ user, project, sketches, + sorting, editorAccessibility, toast, console, diff --git a/client/styles/components/_sketch-list.scss b/client/styles/components/_sketch-list.scss index 2735bb25..328b07b2 100644 --- a/client/styles/components/_sketch-list.scss +++ b/client/styles/components/_sketch-list.scss @@ -19,6 +19,31 @@ 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 { margin: #{10 / $base-font-size}rem; height: #{72 / $base-font-size}rem; diff --git a/package-lock.json b/package-lock.json index ae4a04e1..6e776b69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5462,7 +5462,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5480,11 +5481,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5497,15 +5500,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5608,7 +5614,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5618,6 +5625,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5630,17 +5638,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5657,6 +5668,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5729,7 +5741,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5739,6 +5752,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5814,7 +5828,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5844,6 +5859,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5861,6 +5877,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5899,11 +5916,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -14100,6 +14119,11 @@ "semver": "^5.1.0" } }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", diff --git a/package.json b/package.json index 59af6f6a..f7c3275f 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "redux-thunk": "^2.3.0", "request": "^2.88.0", "request-promise": "^4.1.1", + "reselect": "^4.0.0", "s3": "^4.4.0", "s3-policy": "^0.2.0", "sass-extract": "^2.1.0",
SketchDate createdDate updated