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() &&
|
- Sketch |
- Date created |
- Date updated |
+ {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",