diff --git a/client/constants.js b/client/constants.js index 9cb3daa4..4c5d1bf1 100644 --- a/client/constants.js +++ b/client/constants.js @@ -118,6 +118,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 1369699c..496a2b1c 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 03a11212..dba205f1 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 } } }, @@ -14092,6 +14111,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 3d7c3f69..a231e155 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,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