From 56f017f6059e9e520c7f0af357362421b8fe2404 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 5 Sep 2016 09:12:11 +0530 Subject: [PATCH 01/88] Route Query String --- README.md | 1 + server/server.js | 5 ++++- src/IssueFilter.jsx | 10 +++++++++- src/IssueList.jsx | 15 ++++++++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cecdb8b..b0c625e 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,4 @@ There are no code listings in this chapter. ### Chapter 8: React Router * Simple Routing: [Full source](../../tree/08-simple-routing) | [Diffs from previous step](../../compare/07-eslint...08-simple-routing) * Route Parameters: [Full source](../../tree/08-route-parameters) | [Diffs from previous step](../../compare/08-simple-routing...08-route-parameters) + * Route Query String: [Full source](../../tree/08-route-query-string) | [Diffs from previous step](../../compare/08-route-parameters...08-route-query-string) diff --git a/server/server.js b/server/server.js index d5b1e2d..5b07507 100644 --- a/server/server.js +++ b/server/server.js @@ -14,7 +14,10 @@ app.use(bodyParser.json()); let db; app.get('/api/issues', (req, res) => { - db.collection('issues').find().toArray() + const filter = {}; + if (req.query.status) filter.status = req.query.status; + + db.collection('issues').find(filter).toArray() .then(issues => { const metadata = { total_count: issues.length }; res.json({ _metadata: metadata, records: issues }); diff --git a/src/IssueFilter.jsx b/src/IssueFilter.jsx index a990d9c..bc1abe6 100644 --- a/src/IssueFilter.jsx +++ b/src/IssueFilter.jsx @@ -1,9 +1,17 @@ import React from 'react'; +import { Link } from 'react-router'; export default class IssueFilter extends React.Component { // eslint-disable-line render() { + const Separator = () => | ; return ( -
This is a placeholder for the Issue Filter.
+
+ All Issues + + Open Issues + + Assigned Issues +
); } } diff --git a/src/IssueList.jsx b/src/IssueList.jsx index 7f6df20..c9a2f75 100644 --- a/src/IssueList.jsx +++ b/src/IssueList.jsx @@ -57,8 +57,17 @@ export default class IssueList extends React.Component { this.loadData(); } + componentDidUpdate(prevProps) { + const oldQuery = prevProps.location.query; + const newQuery = this.props.location.query; + if (oldQuery.status === newQuery.status) { + return; + } + this.loadData(); + } + loadData() { - fetch('/api/issues').then(response => { + fetch(`/api/issues${this.props.location.search}`).then(response => { if (response.ok) { response.json().then(data => { data.records.forEach(issue => { @@ -117,3 +126,7 @@ export default class IssueList extends React.Component { ); } } + +IssueList.propTypes = { + location: React.PropTypes.object.isRequired, +}; From b263a381c05374ba88545e018b84ea54a9d95b38 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 5 Sep 2016 17:53:54 +0530 Subject: [PATCH 02/88] Programmatic Navigation --- README.md | 1 + src/App.jsx | 4 ++-- src/IssueFilter.jsx | 35 ++++++++++++++++++++++++++++++----- src/IssueList.jsx | 8 +++++++- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b0c625e..16e7b53 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,4 @@ There are no code listings in this chapter. * Simple Routing: [Full source](../../tree/08-simple-routing) | [Diffs from previous step](../../compare/07-eslint...08-simple-routing) * Route Parameters: [Full source](../../tree/08-route-parameters) | [Diffs from previous step](../../compare/08-simple-routing...08-route-parameters) * Route Query String: [Full source](../../tree/08-route-query-string) | [Diffs from previous step](../../compare/08-route-parameters...08-route-query-string) + * Programmatic Navigation: [Full source](../../tree/08-programmatic-navigation) | [Diffs from previous step](../../compare/08-route-query-string...08-programmatic-navigation) diff --git a/src/App.jsx b/src/App.jsx index e05bd70..54f27b6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,7 @@ import 'babel-polyfill'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Router, Route, Redirect, hashHistory } from 'react-router'; +import { Router, Route, Redirect, hashHistory, withRouter } from 'react-router'; import IssueList from './IssueList.jsx'; import IssueEdit from './IssueEdit.jsx'; @@ -12,7 +12,7 @@ const NoMatch = () =>

Page Not Found

; ReactDOM.render(( - + diff --git a/src/IssueFilter.jsx b/src/IssueFilter.jsx index bc1abe6..b097881 100644 --- a/src/IssueFilter.jsx +++ b/src/IssueFilter.jsx @@ -1,17 +1,42 @@ import React from 'react'; -import { Link } from 'react-router'; -export default class IssueFilter extends React.Component { // eslint-disable-line +export default class IssueFilter extends React.Component { + constructor() { + super(); + this.clearFilter = this.clearFilter.bind(this); + this.setFilterOpen = this.setFilterOpen.bind(this); + this.setFilterAssigned = this.setFilterAssigned.bind(this); + } + + setFilterOpen(e) { + e.preventDefault(); + this.props.setFilter({ status: 'Open' }); + } + + setFilterAssigned(e) { + e.preventDefault(); + this.props.setFilter({ status: 'Assigned' }); + } + + clearFilter(e) { + e.preventDefault(); + this.props.setFilter({}); + } + render() { const Separator = () => | ; return (
- All Issues + All Issues - Open Issues + Open Issues - Assigned Issues + Assigned Issues
); } } + +IssueFilter.propTypes = { + setFilter: React.PropTypes.func.isRequired, +}; diff --git a/src/IssueList.jsx b/src/IssueList.jsx index c9a2f75..63fca38 100644 --- a/src/IssueList.jsx +++ b/src/IssueList.jsx @@ -51,6 +51,7 @@ export default class IssueList extends React.Component { this.state = { issues: [] }; this.createIssue = this.createIssue.bind(this); + this.setFilter = this.setFilter.bind(this); } componentDidMount() { @@ -66,6 +67,10 @@ export default class IssueList extends React.Component { this.loadData(); } + setFilter(query) { + this.props.router.push({ query }); + } + loadData() { fetch(`/api/issues${this.props.location.search}`).then(response => { if (response.ok) { @@ -117,7 +122,7 @@ export default class IssueList extends React.Component { return (

Issue Tracker

- +

@@ -129,4 +134,5 @@ export default class IssueList extends React.Component { IssueList.propTypes = { location: React.PropTypes.object.isRequired, + router: React.PropTypes.object, }; From 29a452f62686b18280caeaa6b02fb05f7c1a6bb9 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 5 Sep 2016 20:02:34 +0530 Subject: [PATCH 03/88] Nested Routes --- README.md | 1 + src/App.jsx | 27 ++++++++++++++++++++++++--- src/IssueList.jsx | 1 - static/index.html | 7 ++++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 16e7b53..d3c3b4e 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,4 @@ There are no code listings in this chapter. * Route Parameters: [Full source](../../tree/08-route-parameters) | [Diffs from previous step](../../compare/08-simple-routing...08-route-parameters) * Route Query String: [Full source](../../tree/08-route-query-string) | [Diffs from previous step](../../compare/08-route-parameters...08-route-query-string) * Programmatic Navigation: [Full source](../../tree/08-programmatic-navigation) | [Diffs from previous step](../../compare/08-route-query-string...08-programmatic-navigation) + * Nested Routes: [Full source](../../tree/08-nested-routes) | [Diffs from previous step](../../compare/08-programmatic-navigation...08-nested-routes) diff --git a/src/App.jsx b/src/App.jsx index 54f27b6..ea98e9d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,12 +9,33 @@ import IssueEdit from './IssueEdit.jsx'; const contentNode = document.getElementById('contents'); const NoMatch = () =>

Page Not Found

; +const App = (props) => ( +
+
+

Issue Tracker

+
+
+ {props.children} +
+
+ Full source code available at this + GitHub repository. +
+
+); + +App.propTypes = { + children: React.PropTypes.object.isRequired, +}; + ReactDOM.render(( - - - + + + + + ), contentNode); diff --git a/src/IssueList.jsx b/src/IssueList.jsx index 63fca38..00c500f 100644 --- a/src/IssueList.jsx +++ b/src/IssueList.jsx @@ -121,7 +121,6 @@ export default class IssueList extends React.Component { render() { return (
-

Issue Tracker


diff --git a/static/index.html b/static/index.html index ff541fb..6423893 100644 --- a/static/index.html +++ b/static/index.html @@ -6,7 +6,12 @@ Pro MERN Stack From 79496e2101ee041edf2c8874ca1251573267916b Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Tue, 6 Sep 2016 12:08:49 +0530 Subject: [PATCH 04/88] Browser History --- README.md | 1 + server/server.js | 5 +++++ src/App.jsx | 4 ++-- webpack.config.js | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d3c3b4e..0d2faac 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,4 @@ There are no code listings in this chapter. * Route Query String: [Full source](../../tree/08-route-query-string) | [Diffs from previous step](../../compare/08-route-parameters...08-route-query-string) * Programmatic Navigation: [Full source](../../tree/08-programmatic-navigation) | [Diffs from previous step](../../compare/08-route-query-string...08-programmatic-navigation) * Nested Routes: [Full source](../../tree/08-nested-routes) | [Diffs from previous step](../../compare/08-programmatic-navigation...08-nested-routes) + * Browser History [Full source](../../tree/08-browser-history) | [Diffs from previous step](../../compare/08-nested-routes...08-browser-history) diff --git a/server/server.js b/server/server.js index 5b07507..53691a1 100644 --- a/server/server.js +++ b/server/server.js @@ -2,6 +2,7 @@ import SourceMapSupport from 'source-map-support'; SourceMapSupport.install(); import 'babel-polyfill'; +import path from 'path'; import express from 'express'; import bodyParser from 'body-parser'; import { MongoClient } from 'mongodb'; @@ -54,6 +55,10 @@ app.post('/api/issues', (req, res) => { }); }); +app.get('*', (req, res) => { + res.sendFile(path.resolve('static/index.html')); +}); + MongoClient.connect('mongodb://localhost/issuetracker').then(connection => { db = connection; app.listen(3000, () => { diff --git a/src/App.jsx b/src/App.jsx index ea98e9d..6633267 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,7 @@ import 'babel-polyfill'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Router, Route, Redirect, hashHistory, withRouter } from 'react-router'; +import { Router, Route, Redirect, browserHistory, withRouter } from 'react-router'; import IssueList from './IssueList.jsx'; import IssueEdit from './IssueEdit.jsx'; @@ -29,7 +29,7 @@ App.propTypes = { }; ReactDOM.render(( - + diff --git a/webpack.config.js b/webpack.config.js index 8a0463f..bb23092 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -31,6 +31,7 @@ module.exports = { target: 'http://localhost:3000', }, }, + historyApiFallback: true, }, devtool: 'source-map', }; From 208e5c3623f48646c9e608af6ba1d80abbe762ce Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sun, 18 Sep 2016 17:27:45 +0530 Subject: [PATCH 05/88] Browser History: corrected minor typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3d210b..874a13d 100644 --- a/README.md +++ b/README.md @@ -61,4 +61,4 @@ There are no code listings in this chapter. * Route Query String: [Full source](../../tree/08-route-query-string) | [Diffs from previous step](../../compare/08-route-parameters...08-route-query-string) * Programmatic Navigation: [Full source](../../tree/08-programmatic-navigation) | [Diffs from previous step](../../compare/08-route-query-string...08-programmatic-navigation) * Nested Routes: [Full source](../../tree/08-nested-routes) | [Diffs from previous step](../../compare/08-programmatic-navigation...08-nested-routes) - * Browser History [Full source](../../tree/08-browser-history) | [Diffs from previous step](../../compare/08-nested-routes...08-browser-history) + * Browser History: [Full source](../../tree/08-browser-history) | [Diffs from previous step](../../compare/08-nested-routes...08-browser-history) From f1944cca420d900b89c7e74b886806cbfa1e1864 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sun, 18 Sep 2016 17:30:25 +0530 Subject: [PATCH 06/88] More Filters in List API --- README.md | 3 +++ server/server.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 874a13d..6c25e43 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,6 @@ There are no code listings in this chapter. * Programmatic Navigation: [Full source](../../tree/08-programmatic-navigation) | [Diffs from previous step](../../compare/08-route-query-string...08-programmatic-navigation) * Nested Routes: [Full source](../../tree/08-nested-routes) | [Diffs from previous step](../../compare/08-programmatic-navigation...08-nested-routes) * Browser History: [Full source](../../tree/08-browser-history) | [Diffs from previous step](../../compare/08-nested-routes...08-browser-history) + +### Chapter 9: Forms + * More Filters in List API: [Full source](../../tree/09-more-filters-in-list-api) | [Diffs from previous step](../../compare/08-browser-history...09-more-filters-in-list-api) diff --git a/server/server.js b/server/server.js index 53691a1..516713d 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,9 @@ let db; app.get('/api/issues', (req, res) => { const filter = {}; if (req.query.status) filter.status = req.query.status; + if (req.query.effort_lte || req.query.effort_gte) filter.effort = {}; + if (req.query.effort_lte) filter.effort.$lte = parseInt(req.query.effort_lte, 10); + if (req.query.effort_gte) filter.effort.$gte = parseInt(req.query.effort_gte, 10); db.collection('issues').find(filter).toArray() .then(issues => { From 4ac87f7b28fd4e0b1414706fd39c1ab4ddf64249 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sun, 25 Sep 2016 17:40:43 +0530 Subject: [PATCH 07/88] Filter Form --- README.md | 1 + src/IssueFilter.jsx | 90 ++++++++++++++++++++++++++++++++++++--------- src/IssueList.jsx | 6 ++- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6c25e43..eae3348 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,4 @@ There are no code listings in this chapter. ### Chapter 9: Forms * More Filters in List API: [Full source](../../tree/09-more-filters-in-list-api) | [Diffs from previous step](../../compare/08-browser-history...09-more-filters-in-list-api) + * Filter Form: [Full source](../../tree/09-filter-form) | [Diffs from previous step](../../compare/09-more-filters-in-list-api...09-filter-form) diff --git a/src/IssueFilter.jsx b/src/IssueFilter.jsx index b097881..63557fb 100644 --- a/src/IssueFilter.jsx +++ b/src/IssueFilter.jsx @@ -1,37 +1,90 @@ import React from 'react'; export default class IssueFilter extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); + this.state = { + status: props.initFilter.status || '', + effort_gte: props.initFilter.effort_gte || '', + effort_lte: props.initFilter.effort_lte || '', + changed: false, + }; + this.onChangeStatus = this.onChangeStatus.bind(this); + this.onChangeEffortGte = this.onChangeEffortGte.bind(this); + this.onChangeEffortLte = this.onChangeEffortLte.bind(this); + this.applyFilter = this.applyFilter.bind(this); + this.resetFilter = this.resetFilter.bind(this); this.clearFilter = this.clearFilter.bind(this); - this.setFilterOpen = this.setFilterOpen.bind(this); - this.setFilterAssigned = this.setFilterAssigned.bind(this); } - setFilterOpen(e) { - e.preventDefault(); - this.props.setFilter({ status: 'Open' }); + componentWillReceiveProps(newProps) { + this.setState({ + status: newProps.initFilter.status || '', + effort_gte: newProps.initFilter.effort_gte || '', + effort_lte: newProps.initFilter.effort_lte || '', + changed: false, + }); } - setFilterAssigned(e) { - e.preventDefault(); - this.props.setFilter({ status: 'Assigned' }); + onChangeStatus(e) { + this.setState({ status: e.target.value, changed: true }); } - clearFilter(e) { - e.preventDefault(); + onChangeEffortGte(e) { + const effortString = e.target.value; + if (effortString.match(/^\d*$/)) { + this.setState({ effort_gte: e.target.value, changed: true }); + } + } + + onChangeEffortLte(e) { + const effortString = e.target.value; + if (effortString.match(/^\d*$/)) { + this.setState({ effort_lte: e.target.value, changed: true }); + } + } + + applyFilter() { + const newFilter = {}; + if (this.state.status) newFilter.status = this.state.status; + if (this.state.effort_gte) newFilter.effort_gte = this.state.effort_gte; + if (this.state.effort_lte) newFilter.effort_lte = this.state.effort_lte; + this.props.setFilter(newFilter); + } + + clearFilter() { this.props.setFilter({}); } + resetFilter() { + this.setState({ + status: this.props.initFilter.status || '', + effort_gte: this.props.initFilter.effort_gte || '', + effort_lte: this.props.initFilter.effort_lte || '', + changed: false, + }); + } + render() { - const Separator = () => | ; return (
- All Issues - - Open Issues - - Assigned Issues + Status: + +  Effort between: + +  -  + + + +
); } @@ -39,4 +92,5 @@ export default class IssueFilter extends React.Component { IssueFilter.propTypes = { setFilter: React.PropTypes.func.isRequired, + initFilter: React.PropTypes.object.isRequired, }; diff --git a/src/IssueList.jsx b/src/IssueList.jsx index 00c500f..bc32b86 100644 --- a/src/IssueList.jsx +++ b/src/IssueList.jsx @@ -61,7 +61,9 @@ export default class IssueList extends React.Component { componentDidUpdate(prevProps) { const oldQuery = prevProps.location.query; const newQuery = this.props.location.query; - if (oldQuery.status === newQuery.status) { + if (oldQuery.status === newQuery.status + && oldQuery.effort_gte === newQuery.effort_gte + && oldQuery.effort_lte === newQuery.effort_lte) { return; } this.loadData(); @@ -121,7 +123,7 @@ export default class IssueList extends React.Component { render() { return (
- +

From 05fdeb4e0790d2efdc0126530b6468fb8965fa70 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 26 Sep 2016 21:00:26 +0530 Subject: [PATCH 08/88] Get API --- README.md | 1 + server/server.js | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eae3348..bae7d59 100644 --- a/README.md +++ b/README.md @@ -66,3 +66,4 @@ There are no code listings in this chapter. ### Chapter 9: Forms * More Filters in List API: [Full source](../../tree/09-more-filters-in-list-api) | [Diffs from previous step](../../compare/08-browser-history...09-more-filters-in-list-api) * Filter Form: [Full source](../../tree/09-filter-form) | [Diffs from previous step](../../compare/09-more-filters-in-list-api...09-filter-form) + * Get API: [Full source](../../tree/09-get-api | [Diffs from previous step](../../compare/09-filter-form...09-get-api) diff --git a/server/server.js b/server/server.js index 516713d..539a818 100644 --- a/server/server.js +++ b/server/server.js @@ -5,7 +5,7 @@ import 'babel-polyfill'; import path from 'path'; import express from 'express'; import bodyParser from 'body-parser'; -import { MongoClient } from 'mongodb'; +import { MongoClient, ObjectId } from 'mongodb'; import Issue from './issue.js'; const app = express(); @@ -58,6 +58,27 @@ app.post('/api/issues', (req, res) => { }); }); +app.get('/api/issues/:id', (req, res) => { + let issueId; + try { + issueId = new ObjectId(req.params.id); + } catch (error) { + res.status(422).json({ message: `Invalid issue ID format: ${error}` }); + return; + } + + db.collection('issues').find({ _id: issueId }).limit(1) + .next() + .then(issue => { + if (!issue) res.status(404).json({ message: `No such issue: ${issueId}` }); + else res.json(issue); + }) + .catch(error => { + console.log(error); + res.status(422).json({ message: `Internal Server Error: ${error}` }); + }); +}); + app.get('*', (req, res) => { res.sendFile(path.resolve('static/index.html')); }); From 21c8ac2e7bbb6b4f0c512df71ff4d5c5fbb1da9e Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 26 Sep 2016 21:01:26 +0530 Subject: [PATCH 09/88] Get API: corrected README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bae7d59..6f72fc1 100644 --- a/README.md +++ b/README.md @@ -66,4 +66,4 @@ There are no code listings in this chapter. ### Chapter 9: Forms * More Filters in List API: [Full source](../../tree/09-more-filters-in-list-api) | [Diffs from previous step](../../compare/08-browser-history...09-more-filters-in-list-api) * Filter Form: [Full source](../../tree/09-filter-form) | [Diffs from previous step](../../compare/09-more-filters-in-list-api...09-filter-form) - * Get API: [Full source](../../tree/09-get-api | [Diffs from previous step](../../compare/09-filter-form...09-get-api) + * Get API: [Full source](../../tree/09-get-api) | [Diffs from previous step](../../compare/09-filter-form...09-get-api) From 89e9be4fcec7a6129fd3ca7ec464e0d40383b259 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Tue, 1 Nov 2016 19:52:00 +0530 Subject: [PATCH 10/88] Edit Page --- README.md | 1 + src/IssueEdit.jsx | 81 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f72fc1..5c379ca 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,4 @@ There are no code listings in this chapter. * More Filters in List API: [Full source](../../tree/09-more-filters-in-list-api) | [Diffs from previous step](../../compare/08-browser-history...09-more-filters-in-list-api) * Filter Form: [Full source](../../tree/09-filter-form) | [Diffs from previous step](../../compare/09-more-filters-in-list-api...09-filter-form) * Get API: [Full source](../../tree/09-get-api) | [Diffs from previous step](../../compare/09-filter-form...09-get-api) + * Edit Page: [Full source](../../tree/09-edit-page) | [Diffs from previous step](../../compare/09-get-api...09-edit-page) diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index 47982a2..66a98eb 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -1,12 +1,87 @@ import React from 'react'; import { Link } from 'react-router'; -export default class IssueEdit extends React.Component { // eslint-disable-line +export default class IssueEdit extends React.Component { + constructor() { + super(); + this.state = { + issue: { + _id: '', title: '', status: '', owner: '', effort: '', + completionDate: '', created: '', + }, + }; + + this.onChange = this.onChange.bind(this); + } + + componentDidMount() { + this.loadData(); + } + + componentDidUpdate(prevProps) { + if (prevProps.params.id !== this.props.params.id) { + this.loadData(); + } + } + + onChange(event) { + const issue = Object.assign({}, this.state.issue); + const value = event.target.value; + issue[event.target.name] = value; + this.setState({ issue }); + } + + loadData() { + fetch(`/api/issues/${this.props.params.id}`).then(response => { + if (response.ok) { + response.json().then(issue => { + issue.created = new Date(issue.created).toDateString(); + issue.effort = issue.effort != null ? issue.effort.toString() : ''; + issue.completionDate = issue.completionDate != null ? + new Date(issue.completionDate).toDateString() : ''; + this.setState({ issue }); + }); + } else { + response.json().then(error => { + alert(`Failed to fetch issue: ${error.message}`); + }); + } + }).catch(err => { + alert(`Error in fetching data from server: ${err.message}`); + }); + } + render() { + const issue = this.state.issue; return (
-

This is a placeholder for editing issue {this.props.params.id}.

- Back to issue list +
+ ID: {issue._id} +
+ Created: {issue.created} +
+ Status: +
+ Owner: +
+ Effort: +
+ Completion Date: +
+ Title: +
+ + Back to issue list +
); } From c06fde0a46c6e00e37ad27ebccf230cc425be9de Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Tue, 1 Nov 2016 20:02:26 +0530 Subject: [PATCH 11/88] Edit Page: corrections --- src/IssueEdit.jsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index 66a98eb..68a76ed 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -10,7 +10,6 @@ export default class IssueEdit extends React.Component { completionDate: '', created: '', }, }; - this.onChange = this.onChange.bind(this); } @@ -26,8 +25,7 @@ export default class IssueEdit extends React.Component { onChange(event) { const issue = Object.assign({}, this.state.issue); - const value = event.target.value; - issue[event.target.name] = value; + issue[event.target.name] = event.target.value; this.setState({ issue }); } @@ -36,9 +34,9 @@ export default class IssueEdit extends React.Component { if (response.ok) { response.json().then(issue => { issue.created = new Date(issue.created).toDateString(); - issue.effort = issue.effort != null ? issue.effort.toString() : ''; issue.completionDate = issue.completionDate != null ? new Date(issue.completionDate).toDateString() : ''; + issue.effort = issue.effort != null ? issue.effort.toString() : ''; this.setState({ issue }); }); } else { From 73884aeed78a65ebf4d54526d86626002b9db838 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Thu, 3 Nov 2016 11:44:22 +0530 Subject: [PATCH 12/88] UI Components - Number Input --- README.md | 1 + src/IssueEdit.jsx | 12 +++++++----- src/NumInput.jsx | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 src/NumInput.jsx diff --git a/README.md b/README.md index 5c379ca..25918ae 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,4 @@ There are no code listings in this chapter. * Filter Form: [Full source](../../tree/09-filter-form) | [Diffs from previous step](../../compare/09-more-filters-in-list-api...09-filter-form) * Get API: [Full source](../../tree/09-get-api) | [Diffs from previous step](../../compare/09-filter-form...09-get-api) * Edit Page: [Full source](../../tree/09-edit-page) | [Diffs from previous step](../../compare/09-get-api...09-edit-page) + * UI Components - Number Input [Full source](../../tree/09-ui-components--number-input) | [Diffs from previous step](../../compare/09-edit-page...09-ui-components--number-input) diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index 68a76ed..4bbef04 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -1,12 +1,14 @@ import React from 'react'; import { Link } from 'react-router'; +import NumInput from './NumInput.jsx'; + export default class IssueEdit extends React.Component { constructor() { super(); this.state = { issue: { - _id: '', title: '', status: '', owner: '', effort: '', + _id: '', title: '', status: '', owner: '', effort: null, completionDate: '', created: '', }, }; @@ -23,9 +25,10 @@ export default class IssueEdit extends React.Component { } } - onChange(event) { + onChange(event, convertedValue) { const issue = Object.assign({}, this.state.issue); - issue[event.target.name] = event.target.value; + const value = (convertedValue !== undefined) ? convertedValue : event.target.value; + issue[event.target.name] = value; this.setState({ issue }); } @@ -36,7 +39,6 @@ export default class IssueEdit extends React.Component { issue.created = new Date(issue.created).toDateString(); issue.completionDate = issue.completionDate != null ? new Date(issue.completionDate).toDateString() : ''; - issue.effort = issue.effort != null ? issue.effort.toString() : ''; this.setState({ issue }); }); } else { @@ -69,7 +71,7 @@ export default class IssueEdit extends React.Component {
Owner:
- Effort: + Effort:
Completion Date: + ); + } +} + +NumInput.propTypes = { + value: React.PropTypes.number, + onChange: React.PropTypes.func.isRequired, +}; From de060d68dfa63830145cb2de6667b67ca42a6e9d Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Thu, 3 Nov 2016 18:19:07 +0530 Subject: [PATCH 13/88] UI Components - Date Input --- README.md | 1 + src/DateInput.jsx | 70 +++++++++++++++++++++++++++++++++++++++++++++++ src/IssueEdit.jsx | 23 ++++++++++++++-- static/index.html | 2 ++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/DateInput.jsx diff --git a/README.md b/README.md index 25918ae..0b7513e 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,4 @@ There are no code listings in this chapter. * Get API: [Full source](../../tree/09-get-api) | [Diffs from previous step](../../compare/09-filter-form...09-get-api) * Edit Page: [Full source](../../tree/09-edit-page) | [Diffs from previous step](../../compare/09-get-api...09-edit-page) * UI Components - Number Input [Full source](../../tree/09-ui-components--number-input) | [Diffs from previous step](../../compare/09-edit-page...09-ui-components--number-input) + * UI Components - Date Input [Full source](../../tree/09-ui-components--date-input) | [Diffs from previous step](../../compare/09-ui-components--number-input...09-ui-components--date-input) diff --git a/src/DateInput.jsx b/src/DateInput.jsx new file mode 100644 index 0000000..f81f1b9 --- /dev/null +++ b/src/DateInput.jsx @@ -0,0 +1,70 @@ +import React from 'react'; + +export default class DateInput extends React.Component { + constructor(props) { + super(props); + this.state = { value: this.editFormat(props.value), focused: false, valid: true }; + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onChange = this.onChange.bind(this); + } + + componentWillReceiveProps(newProps) { + if (newProps.value !== this.props.value) { + this.setState({ value: this.editFormat(newProps.value) }); + } + } + + onFocus() { + this.setState({ focused: true }); + } + + onBlur(e) { + const value = this.unformat(this.state.value); + const valid = this.state.value === '' || value != null; + if (valid !== this.state.valid && this.props.onValidityChange) { + this.props.onValidityChange(e, valid); + } + this.setState({ focused: false, valid }); + if (valid) this.props.onChange(e, value); + } + + onChange(e) { + if (e.target.value.match(/^[\d-]*$/)) { + this.setState({ value: e.target.value }); + } + } + + displayFormat(date) { + return (date != null) ? date.toDateString() : ''; + } + + editFormat(date) { + return (date != null) ? date.toISOString().substr(0, 10) : ''; + } + + unformat(str) { + const val = new Date(str); + return isNaN(val.getTime()) ? null : val; + } + + render() { + const className = (!this.state.valid && !this.state.focused) ? 'invalid' : null; + const value = (this.state.focused || !this.state.valid) ? this.state.value + : this.displayFormat(this.props.value); + return ( + + ); + } +} + +DateInput.propTypes = { + value: React.PropTypes.object, + onChange: React.PropTypes.func.isRequired, + onValidityChange: React.PropTypes.func, + name: React.PropTypes.string.isRequired, +}; diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index 4bbef04..f8e38c3 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Link } from 'react-router'; import NumInput from './NumInput.jsx'; +import DateInput from './DateInput.jsx'; export default class IssueEdit extends React.Component { constructor() { @@ -9,10 +10,12 @@ export default class IssueEdit extends React.Component { this.state = { issue: { _id: '', title: '', status: '', owner: '', effort: null, - completionDate: '', created: '', + completionDate: null, created: '', }, + invalidFields: {}, }; this.onChange = this.onChange.bind(this); + this.onValidityChange = this.onValidityChange.bind(this); } componentDidMount() { @@ -32,13 +35,23 @@ export default class IssueEdit extends React.Component { this.setState({ issue }); } + onValidityChange(event, valid) { + const invalidFields = Object.assign({}, this.state.invalidFields); + if (!valid) { + invalidFields[event.target.name] = true; + } else { + delete invalidFields[event.target.name]; + } + this.setState({ invalidFields }); + } + loadData() { fetch(`/api/issues/${this.props.params.id}`).then(response => { if (response.ok) { response.json().then(issue => { issue.created = new Date(issue.created).toDateString(); issue.completionDate = issue.completionDate != null ? - new Date(issue.completionDate).toDateString() : ''; + new Date(issue.completionDate) : null; this.setState({ issue }); }); } else { @@ -53,6 +66,8 @@ export default class IssueEdit extends React.Component { render() { const issue = this.state.issue; + const validationMessage = Object.keys(this.state.invalidFields).length === 0 ? null + : (
Please correct invalid fields before submitting.
); return (
@@ -73,12 +88,14 @@ export default class IssueEdit extends React.Component {
Effort:
- Completion Date:
Title:
+ {validationMessage} Back to issue list diff --git a/static/index.html b/static/index.html index 6423893..f7433b6 100644 --- a/static/index.html +++ b/static/index.html @@ -12,6 +12,8 @@ border-top: 1px solid silver; padding-top: 5px; margin-top: 20px; font-family: Helvetica; font-size: 10px; color: grey; } + input.invalid {border-color: red;} + div.error {color: red;} From 72ad77f9f314c10958f2b256601cc93475a98e8f Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sat, 5 Nov 2016 13:32:33 +0530 Subject: [PATCH 14/88] Get API: corrected http status for error --- server/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server.js b/server/server.js index 539a818..43c5a50 100644 --- a/server/server.js +++ b/server/server.js @@ -75,7 +75,7 @@ app.get('/api/issues/:id', (req, res) => { }) .catch(error => { console.log(error); - res.status(422).json({ message: `Internal Server Error: ${error}` }); + res.status(500).json({ message: `Internal Server Error: ${error}` }); }); }); From d8701950367a0f6dca4a66c2a8226bffc9954832 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sat, 5 Nov 2016 17:40:51 +0530 Subject: [PATCH 15/88] Update API --- README.md | 1 + server/issue.js | 7 +++++++ server/server.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/README.md b/README.md index 0b7513e..c9da25b 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,4 @@ There are no code listings in this chapter. * Edit Page: [Full source](../../tree/09-edit-page) | [Diffs from previous step](../../compare/09-get-api...09-edit-page) * UI Components - Number Input [Full source](../../tree/09-ui-components--number-input) | [Diffs from previous step](../../compare/09-edit-page...09-ui-components--number-input) * UI Components - Date Input [Full source](../../tree/09-ui-components--date-input) | [Diffs from previous step](../../compare/09-ui-components--number-input...09-ui-components--date-input) + * Update API [Full source](../../tree/09-update-api) | [Diffs from previous step](../../compare/09-ui-components--date-input...09-update-api) diff --git a/server/issue.js b/server/issue.js index c327e47..2b964f6 100644 --- a/server/issue.js +++ b/server/issue.js @@ -24,6 +24,12 @@ function cleanupIssue(issue) { return cleanedUpIssue; } +function convertIssue(issue) { + if (issue.created) issue.created = new Date(issue.created); + if (issue.completionDate) issue.completionDate = new Date(issue.completionDate); + return cleanupIssue(issue); +} + function validateIssue(issue) { const errors = []; Object.keys(issueFieldType).forEach(field => { @@ -42,4 +48,5 @@ function validateIssue(issue) { export default { validateIssue, cleanupIssue, + convertIssue, }; diff --git a/server/server.js b/server/server.js index 43c5a50..a6c28da 100644 --- a/server/server.js +++ b/server/server.js @@ -79,6 +79,37 @@ app.get('/api/issues/:id', (req, res) => { }); }); +app.put('/api/issues/:id', (req, res) => { + let issueId; + try { + issueId = new ObjectId(req.params.id); + } catch (error) { + res.status(422).json({ message: `Invalid issue ID format: ${error}` }); + return; + } + + const issue = req.body; + delete issue._id; + + const err = Issue.validateIssue(issue); + if (err) { + res.status(422).json({ message: `Invalid request: ${err}` }); + return; + } + + db.collection('issues').update({ _id: issueId }, Issue.convertIssue(issue)).then(() => + db.collection('issues').find({ _id: issueId }).limit(1) + .next() + ) + .then(savedIssue => { + res.json(savedIssue); + }) + .catch(error => { + console.log(error); + res.status(500).json({ message: `Internal Server Error: ${error}` }); + }); +}); + app.get('*', (req, res) => { res.sendFile(path.resolve('static/index.html')); }); From 6a0056dbc00498b581be16b351a303fded644980 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sat, 5 Nov 2016 18:38:40 +0530 Subject: [PATCH 16/88] Using Update API --- README.md | 1 + src/IssueEdit.jsx | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c9da25b..9c28da6 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,4 @@ There are no code listings in this chapter. * UI Components - Number Input [Full source](../../tree/09-ui-components--number-input) | [Diffs from previous step](../../compare/09-edit-page...09-ui-components--number-input) * UI Components - Date Input [Full source](../../tree/09-ui-components--date-input) | [Diffs from previous step](../../compare/09-ui-components--number-input...09-ui-components--date-input) * Update API [Full source](../../tree/09-update-api) | [Diffs from previous step](../../compare/09-ui-components--date-input...09-update-api) + * Using Update API [Full source](../../tree/09-using-update-api) | [Diffs from previous step](../../compare/09-update-api...09-using-update-api) diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index f8e38c3..0849a60 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -10,12 +10,13 @@ export default class IssueEdit extends React.Component { this.state = { issue: { _id: '', title: '', status: '', owner: '', effort: null, - completionDate: null, created: '', + completionDate: null, created: null, }, invalidFields: {}, }; this.onChange = this.onChange.bind(this); this.onValidityChange = this.onValidityChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); } componentDidMount() { @@ -45,11 +46,42 @@ export default class IssueEdit extends React.Component { this.setState({ invalidFields }); } + onSubmit(event) { + event.preventDefault(); + + if (Object.keys(this.state.invalidFields).length !== 0) { + return; + } + + fetch(`/api/issues/${this.props.params.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.state.issue), + }).then(response => { + if (response.ok) { + response.json().then(updatedIssue => { + updatedIssue.created = new Date(updatedIssue.created); + if (updatedIssue.completionDate) { + updatedIssue.completionDate = new Date(updatedIssue.completionDate); + } + this.setState({ issue: updatedIssue }); + alert('Updated issue successfully.'); + }); + } else { + response.json().then(error => { + alert(`Failed to update issue: ${error.message}`); + }); + } + }).catch(err => { + alert(`Error in sending data to server: ${err.message}`); + }); + } + loadData() { fetch(`/api/issues/${this.props.params.id}`).then(response => { if (response.ok) { response.json().then(issue => { - issue.created = new Date(issue.created).toDateString(); + issue.created = new Date(issue.created); issue.completionDate = issue.completionDate != null ? new Date(issue.completionDate) : null; this.setState({ issue }); @@ -70,10 +102,10 @@ export default class IssueEdit extends React.Component { : (
Please correct invalid fields before submitting.
); return (
-
+ ID: {issue._id}
- Created: {issue.created} + Created: {issue.created ? issue.created.toDateString() : ''}
Status: - - - - - - - - -  Effort between: - -  -  - - - - -
+ + + + Status + + + + + + + + + + + + + + Effort + + + - + + + + + + +   + + + + + + + + ); } } From 3dcf2777bd81d0b41ecb3d7a04743d9f2bbc7621 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Thu, 17 Nov 2016 15:20:54 +0530 Subject: [PATCH 30/88] Forms - Inline Forms --- README.md | 1 + src/IssueAdd.jsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 305b69f..b18a267 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,4 @@ There are no code listings in this chapter. * Navigation [Full source](../../tree/10-navigation) | [Diffs from previous step](../../compare/10-bootstrap-installation...10-navigation) * Table and Panel [Full source](../../tree/10-table-and-panel) | [Diffs from previous step](../../compare/10-navigation...10-table-and-panel) * Forms - Grid Based Forms [Full source](../../tree/10-forms--grid-based-forms) | [Diffs from previous step](../../compare/10-table-and-panel...10-forms--grid-based-forms) + * Forms - Inline Forms [Full source](../../tree/10-forms--inline-forms) | [Diffs from previous step](../../compare/10-forms--grid-based-forms...10-forms--inline-forms) diff --git a/src/IssueAdd.jsx b/src/IssueAdd.jsx index 534e7fe..71604d1 100644 --- a/src/IssueAdd.jsx +++ b/src/IssueAdd.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Form, FormControl, Button } from 'react-bootstrap'; export default class IssueAdd extends React.Component { constructor() { @@ -22,11 +23,13 @@ export default class IssueAdd extends React.Component { render() { return (
- - - - - +
+ + {' '} + + {' '} + +
); } From d05bc942140f485dce7db389d17c13bc4c596ebf Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Fri, 18 Nov 2016 14:10:50 +0530 Subject: [PATCH 31/88] Forms -- Horizontal Forms --- README.md | 1 + src/DateInput.jsx | 5 +- src/IssueEdit.jsx | 114 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index b18a267..b76dce2 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,4 @@ There are no code listings in this chapter. * Table and Panel [Full source](../../tree/10-table-and-panel) | [Diffs from previous step](../../compare/10-navigation...10-table-and-panel) * Forms - Grid Based Forms [Full source](../../tree/10-forms--grid-based-forms) | [Diffs from previous step](../../compare/10-table-and-panel...10-forms--grid-based-forms) * Forms - Inline Forms [Full source](../../tree/10-forms--inline-forms) | [Diffs from previous step](../../compare/10-forms--grid-based-forms...10-forms--inline-forms) + * Forms - Horizontal Forms [Full source](../../tree/10-horizontal-forms) | [Diffs from previous step](../../compare/10-forms--inline-forms...10-horizontal-forms) diff --git a/src/DateInput.jsx b/src/DateInput.jsx index f81f1b9..32570a4 100644 --- a/src/DateInput.jsx +++ b/src/DateInput.jsx @@ -49,12 +49,13 @@ export default class DateInput extends React.Component { } render() { - const className = (!this.state.valid && !this.state.focused) ? 'invalid' : null; const value = (this.state.focused || !this.state.valid) ? this.state.value : this.displayFormat(this.props.value); + const childProps = Object.assign({}, this.props); + delete childProps.onValidityChange; return ( diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index 0849a60..5902b85 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -1,7 +1,9 @@ import React from 'react'; -import { Link } from 'react-router'; - +import { FormGroup, FormControl, ControlLabel, ButtonToolbar, Button, + Panel, Form, Col } from 'react-bootstrap'; +import { LinkContainer } from 'react-router-bootstrap'; import NumInput from './NumInput.jsx'; + import DateInput from './DateInput.jsx'; export default class IssueEdit extends React.Component { @@ -101,37 +103,83 @@ export default class IssueEdit extends React.Component { const validationMessage = Object.keys(this.state.invalidFields).length === 0 ? null : (
Please correct invalid fields before submitting.
); return ( -
-
- ID: {issue._id} -
- Created: {issue.created ? issue.created.toDateString() : ''} -
- Status: -
- Owner: -
- Effort: -
- Completion Date: -
- Title: -
- {validationMessage} - - Back to issue list - -
+ +
+ + ID + + {issue._id} + + + + Created + + + {issue.created ? issue.created.toDateString() : ''} + + + + + Status + + + + + + + + + + + + + Owner + + + + + + Effort + + + + + + Completion Date + + + + + + + Title + + + + + + + + + + + + + + +
+ {validationMessage} +
); } } From 67501436712abfe4f4631b04c6c2c927a322e5a2 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Fri, 18 Nov 2016 14:11:51 +0530 Subject: [PATCH 32/88] Forms -- Horizontal Forms: fixed typo in branch name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b76dce2..bc43a4c 100644 --- a/README.md +++ b/README.md @@ -81,4 +81,4 @@ There are no code listings in this chapter. * Table and Panel [Full source](../../tree/10-table-and-panel) | [Diffs from previous step](../../compare/10-navigation...10-table-and-panel) * Forms - Grid Based Forms [Full source](../../tree/10-forms--grid-based-forms) | [Diffs from previous step](../../compare/10-table-and-panel...10-forms--grid-based-forms) * Forms - Inline Forms [Full source](../../tree/10-forms--inline-forms) | [Diffs from previous step](../../compare/10-forms--grid-based-forms...10-forms--inline-forms) - * Forms - Horizontal Forms [Full source](../../tree/10-horizontal-forms) | [Diffs from previous step](../../compare/10-forms--inline-forms...10-horizontal-forms) + * Forms - Horizontal Forms [Full source](../../tree/10-forms--horizontal-forms) | [Diffs from previous step](../../compare/10-forms--inline-forms...10-forms--horizontal-forms) From 8062770eeef3687d9648cb9f1526a32954972ee3 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sat, 19 Nov 2016 20:16:41 +0530 Subject: [PATCH 33/88] Alerts - Validations --- README.md | 1 + src/IssueEdit.jsx | 29 ++++++++++++++++++++++++----- static/index.html | 2 -- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bc43a4c..8976eed 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,4 @@ There are no code listings in this chapter. * Forms - Grid Based Forms [Full source](../../tree/10-forms--grid-based-forms) | [Diffs from previous step](../../compare/10-table-and-panel...10-forms--grid-based-forms) * Forms - Inline Forms [Full source](../../tree/10-forms--inline-forms) | [Diffs from previous step](../../compare/10-forms--grid-based-forms...10-forms--inline-forms) * Forms - Horizontal Forms [Full source](../../tree/10-forms--horizontal-forms) | [Diffs from previous step](../../compare/10-forms--inline-forms...10-forms--horizontal-forms) + * Alerts - validations [Full source](../../tree/10-alerts--validations) | [Diffs from previous step](../../compare/10-forms--horizontal-forms...10-alerts--validations) diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index 5902b85..b44e090 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { FormGroup, FormControl, ControlLabel, ButtonToolbar, Button, - Panel, Form, Col } from 'react-bootstrap'; + Panel, Form, Col, Alert } from 'react-bootstrap'; import { LinkContainer } from 'react-router-bootstrap'; import NumInput from './NumInput.jsx'; @@ -14,8 +14,10 @@ export default class IssueEdit extends React.Component { _id: '', title: '', status: '', owner: '', effort: null, completionDate: null, created: null, }, - invalidFields: {}, + invalidFields: {}, showingValidation: false, }; + this.dismissValidation = this.dismissValidation.bind(this); + this.showValidation = this.showValidation.bind(this); this.onChange = this.onChange.bind(this); this.onValidityChange = this.onValidityChange.bind(this); this.onSubmit = this.onSubmit.bind(this); @@ -50,6 +52,7 @@ export default class IssueEdit extends React.Component { onSubmit(event) { event.preventDefault(); + this.showValidation(); if (Object.keys(this.state.invalidFields).length !== 0) { return; @@ -98,10 +101,24 @@ export default class IssueEdit extends React.Component { }); } + showValidation() { + this.setState({ showingValidation: true }); + } + + dismissValidation() { + this.setState({ showingValidation: false }); + } + render() { const issue = this.state.issue; - const validationMessage = Object.keys(this.state.invalidFields).length === 0 ? null - : (
Please correct invalid fields before submitting.
); + let validationMessage = null; + if (Object.keys(this.state.invalidFields).length !== 0 && this.state.showingValidation) { + validationMessage = ( + + Please correct invalid fields before submitting. + + ); + } return (
@@ -177,8 +194,10 @@ export default class IssueEdit extends React.Component { + + {validationMessage} +
- {validationMessage}
); } diff --git a/static/index.html b/static/index.html index 517a9f1..5c7b212 100644 --- a/static/index.html +++ b/static/index.html @@ -7,8 +7,6 @@ From 0f364080a66e5e2a6e4797db0e2822d2b3825621 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Sun, 20 Nov 2016 19:25:14 +0530 Subject: [PATCH 34/88] Alerts - Results --- README.md | 1 + src/.eslintrc | 3 --- src/IssueEdit.jsx | 31 ++++++++++++++++++++++++++----- src/IssueList.jsx | 30 ++++++++++++++++++++++++------ src/Toast.jsx | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 src/Toast.jsx diff --git a/README.md b/README.md index 8976eed..742cf82 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,4 @@ There are no code listings in this chapter. * Forms - Inline Forms [Full source](../../tree/10-forms--inline-forms) | [Diffs from previous step](../../compare/10-forms--grid-based-forms...10-forms--inline-forms) * Forms - Horizontal Forms [Full source](../../tree/10-forms--horizontal-forms) | [Diffs from previous step](../../compare/10-forms--inline-forms...10-forms--horizontal-forms) * Alerts - validations [Full source](../../tree/10-alerts--validations) | [Diffs from previous step](../../compare/10-forms--horizontal-forms...10-alerts--validations) + * Alerts - Results [Full source](../../tree/10-alerts--results) | [Diffs from previous step](../../compare/10-alerts--validations...10-alerts--results) diff --git a/src/.eslintrc b/src/.eslintrc index cb6392f..e5a34ae 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -1,8 +1,5 @@ { "env": { "browser": true - }, - "rules": { - "no-alert": ["off"] } } diff --git a/src/IssueEdit.jsx b/src/IssueEdit.jsx index b44e090..4726ad9 100644 --- a/src/IssueEdit.jsx +++ b/src/IssueEdit.jsx @@ -5,6 +5,7 @@ import { LinkContainer } from 'react-router-bootstrap'; import NumInput from './NumInput.jsx'; import DateInput from './DateInput.jsx'; +import Toast from './Toast.jsx'; export default class IssueEdit extends React.Component { constructor() { @@ -15,9 +16,13 @@ export default class IssueEdit extends React.Component { completionDate: null, created: null, }, invalidFields: {}, showingValidation: false, + toastVisible: false, toastMessage: '', toastType: 'success', }; this.dismissValidation = this.dismissValidation.bind(this); this.showValidation = this.showValidation.bind(this); + this.showSuccess = this.showSuccess.bind(this); + this.showError = this.showError.bind(this); + this.dismissToast = this.dismissToast.bind(this); this.onChange = this.onChange.bind(this); this.onValidityChange = this.onValidityChange.bind(this); this.onSubmit = this.onSubmit.bind(this); @@ -70,15 +75,15 @@ export default class IssueEdit extends React.Component { updatedIssue.completionDate = new Date(updatedIssue.completionDate); } this.setState({ issue: updatedIssue }); - alert('Updated issue successfully.'); + this.showSuccess('Updated issue successfully.'); }); } else { response.json().then(error => { - alert(`Failed to update issue: ${error.message}`); + this.showError(`Failed to update issue: ${error.message}`); }); } }).catch(err => { - alert(`Error in sending data to server: ${err.message}`); + this.showError(`Error in sending data to server: ${err.message}`); }); } @@ -93,11 +98,11 @@ export default class IssueEdit extends React.Component { }); } else { response.json().then(error => { - alert(`Failed to fetch issue: ${error.message}`); + this.showError(`Failed to fetch issue: ${error.message}`); }); } }).catch(err => { - alert(`Error in fetching data from server: ${err.message}`); + this.showError(`Error in fetching data from server: ${err.message}`); }); } @@ -109,6 +114,18 @@ export default class IssueEdit extends React.Component { this.setState({ showingValidation: false }); } + showSuccess(message) { + this.setState({ toastVisible: true, toastMessage: message, toastType: 'success' }); + } + + showError(message) { + this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); + } + + dismissToast() { + this.setState({ toastVisible: false }); + } + render() { const issue = this.state.issue; let validationMessage = null; @@ -198,6 +215,10 @@ export default class IssueEdit extends React.Component { {validationMessage} + ); } diff --git a/src/IssueList.jsx b/src/IssueList.jsx index 95c8139..3c008ef 100644 --- a/src/IssueList.jsx +++ b/src/IssueList.jsx @@ -5,6 +5,7 @@ import { Button, Glyphicon, Table, Panel } from 'react-bootstrap'; import IssueAdd from './IssueAdd.jsx'; import IssueFilter from './IssueFilter.jsx'; +import Toast from './Toast.jsx'; const IssueRow = (props) => { function onDeleteClick() { @@ -63,11 +64,16 @@ IssueTable.propTypes = { export default class IssueList extends React.Component { constructor() { super(); - this.state = { issues: [] }; + this.state = { + issues: [], + toastVisible: false, toastMessage: '', toastType: 'success', + }; this.createIssue = this.createIssue.bind(this); this.setFilter = this.setFilter.bind(this); this.deleteIssue = this.deleteIssue.bind(this); + this.showError = this.showError.bind(this); + this.dismissToast = this.dismissToast.bind(this); } componentDidMount() { @@ -89,6 +95,14 @@ export default class IssueList extends React.Component { this.props.router.push({ query }); } + showError(message) { + this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); + } + + dismissToast() { + this.setState({ toastVisible: false }); + } + loadData() { fetch(`/api/issues${this.props.location.search}`).then(response => { if (response.ok) { @@ -103,11 +117,11 @@ export default class IssueList extends React.Component { }); } else { response.json().then(error => { - alert(`Failed to fetch issues ${error.message}`); + this.showError(`Failed to fetch issues ${error.message}`); }); } }).catch(err => { - alert(`Error in fetching data from server: ${err}`); + this.showError(`Error in fetching data from server: ${err}`); }); } @@ -128,17 +142,17 @@ export default class IssueList extends React.Component { }); } else { response.json().then(error => { - alert(`Failed to add issue: ${error.message}`); + this.showError(`Failed to add issue: ${error.message}`); }); } }).catch(err => { - alert(`Error in sending data to server: ${err.message}`); + this.showError(`Error in sending data to server: ${err.message}`); }); } deleteIssue(id) { fetch(`/api/issues/${id}`, { method: 'DELETE' }).then(response => { - if (!response.ok) alert('Failed to delete issue'); + if (!response.ok) this.showError('Failed to delete issue'); else this.loadData(); }); } @@ -151,6 +165,10 @@ export default class IssueList extends React.Component { +
); } diff --git a/src/Toast.jsx b/src/Toast.jsx new file mode 100644 index 0000000..82386db --- /dev/null +++ b/src/Toast.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Alert, Collapse } from 'react-bootstrap'; + +export default class UndoToast extends React.Component { + componentDidUpdate() { + if (this.props.showing) { + clearTimeout(this.dismissTimer); + this.dismissTimer = setTimeout(this.props.onDismiss, 5000); + } + } + + componentWillUnmount() { + clearTimeout(this.dismissTimer); + } + + render() { + return ( + +
+ + {this.props.message} + +
+
+ ); + } +} + +UndoToast.propTypes = { + showing: React.PropTypes.bool.isRequired, + onDismiss: React.PropTypes.func.isRequired, + bsStyle: React.PropTypes.string, + message: React.PropTypes.any.isRequired, +}; + +UndoToast.defaultProps = { + bsStyle: 'success', +}; From 40d437c9c1384dc0f0038426576ccb12f36ff12a Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 21 Nov 2016 12:23:09 +0530 Subject: [PATCH 35/88] Alerts - Results: corrected class name for Toast --- src/Toast.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Toast.jsx b/src/Toast.jsx index 82386db..3fb6ac4 100644 --- a/src/Toast.jsx +++ b/src/Toast.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Alert, Collapse } from 'react-bootstrap'; -export default class UndoToast extends React.Component { +export default class Toast extends React.Component { componentDidUpdate() { if (this.props.showing) { clearTimeout(this.dismissTimer); @@ -29,13 +29,13 @@ export default class UndoToast extends React.Component { } } -UndoToast.propTypes = { +Toast.propTypes = { showing: React.PropTypes.bool.isRequired, onDismiss: React.PropTypes.func.isRequired, bsStyle: React.PropTypes.string, message: React.PropTypes.any.isRequired, }; -UndoToast.defaultProps = { +Toast.defaultProps = { bsStyle: 'success', }; From b9234eaefdd2c187f43f8b8936675579e1440590 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Mon, 21 Nov 2016 12:37:55 +0530 Subject: [PATCH 36/88] Modals --- README.md | 1 + src/App.jsx | 3 +- src/IssueAdd.jsx | 40 ---------------- src/IssueAddNavItem.jsx | 104 ++++++++++++++++++++++++++++++++++++++++ src/IssueList.jsx | 28 ----------- 5 files changed, 107 insertions(+), 69 deletions(-) delete mode 100644 src/IssueAdd.jsx create mode 100644 src/IssueAddNavItem.jsx diff --git a/README.md b/README.md index 742cf82..608fdfb 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,4 @@ There are no code listings in this chapter. * Forms - Horizontal Forms [Full source](../../tree/10-forms--horizontal-forms) | [Diffs from previous step](../../compare/10-forms--inline-forms...10-forms--horizontal-forms) * Alerts - validations [Full source](../../tree/10-alerts--validations) | [Diffs from previous step](../../compare/10-forms--horizontal-forms...10-alerts--validations) * Alerts - Results [Full source](../../tree/10-alerts--results) | [Diffs from previous step](../../compare/10-alerts--validations...10-alerts--results) + * Modals [Full source](../../tree/10-modals) | [Diffs from previous step](../../compare/10-alerts--results...10-modals) diff --git a/src/App.jsx b/src/App.jsx index c2f94c5..e2f45d7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ import { LinkContainer } from 'react-router-bootstrap'; import IssueList from './IssueList.jsx'; import IssueEdit from './IssueEdit.jsx'; +import IssueAddNavItem from './IssueAddNavItem.jsx'; const contentNode = document.getElementById('contents'); const NoMatch = () =>

Page Not Found

; @@ -25,7 +26,7 @@ const Header = () => (
); } @@ -182,8 +167,11 @@ export default class IssueList extends React.Component { IssueList.propTypes = { location: React.PropTypes.object.isRequired, router: React.PropTypes.object, + showError: React.PropTypes.func.isRequired, }; IssueList.contextTypes = { initialState: React.PropTypes.object, }; + +export default withToast(IssueList); diff --git a/src/IssueReport.jsx b/src/IssueReport.jsx index 74c3527..61d3210 100644 --- a/src/IssueReport.jsx +++ b/src/IssueReport.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Panel, Table } from 'react-bootstrap'; import IssueFilter from './IssueFilter.jsx'; -import Toast from './Toast.jsx'; +import withToast from './withToast.jsx'; const statuses = ['New', 'Open', 'Assigned', 'Fixed', 'Verified', 'Closed']; @@ -18,7 +18,7 @@ StatRow.propTypes = { counts: React.PropTypes.object.isRequired, }; -export default class IssueReport extends React.Component { +class IssueReport extends React.Component { static dataFetcher({ urlBase, location }) { const search = location.search ? `${location.search}&_summary` : '?_summary'; return fetch(`${urlBase || ''}/api/issues${search}`).then(response => { @@ -32,11 +32,8 @@ export default class IssueReport extends React.Component { const stats = context.initialState.IssueReport ? context.initialState.IssueReport : {}; this.state = { stats, - toastVisible: false, toastMessage: '', toastType: 'success', }; this.setFilter = this.setFilter.bind(this); - this.showError = this.showError.bind(this); - this.dismissToast = this.dismissToast.bind(this); } componentDidMount() { @@ -58,20 +55,12 @@ export default class IssueReport extends React.Component { this.props.router.push({ pathname: this.props.location.pathname, query }); } - showError(message) { - this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); - } - - dismissToast() { - this.setState({ toastVisible: false }); - } - loadData() { IssueReport.dataFetcher({ location: this.props.location }) .then(data => { this.setState({ stats: data.IssueReport }); }).catch(err => { - this.showError(`Error in fetching data from server: ${err}`); + this.props.showError(`Error in fetching data from server: ${err}`); }); } @@ -94,10 +83,6 @@ export default class IssueReport extends React.Component { )} -
); } @@ -106,8 +91,11 @@ export default class IssueReport extends React.Component { IssueReport.propTypes = { location: React.PropTypes.object.isRequired, router: React.PropTypes.object, + showError: React.PropTypes.func.isRequired, }; IssueReport.contextTypes = { initialState: React.PropTypes.object, }; + +export default withToast(IssueReport); diff --git a/src/withToast.jsx b/src/withToast.jsx new file mode 100644 index 0000000..999886a --- /dev/null +++ b/src/withToast.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import Toast from './Toast.jsx'; + +export default function withToast(OriginalComponent) { + return class WithToast extends React.Component { + constructor(props) { + super(props); + this.state = { + toastVisible: false, toastMessage: '', toastType: 'success', + }; + this.showSuccess = this.showSuccess.bind(this); + this.showError = this.showError.bind(this); + this.dismissToast = this.dismissToast.bind(this); + } + + showSuccess(message) { + this.setState({ toastVisible: true, toastMessage: message, toastType: 'success' }); + } + + showError(message) { + this.setState({ toastVisible: true, toastMessage: message, toastType: 'danger' }); + } + + dismissToast() { + this.setState({ toastVisible: false }); + } + + render() { + return ( +
+ + +
+ ); + } + }; +} From 7a15442fda5b9f6806b7fdb18ad707b1d7dad3b0 Mon Sep 17 00:00:00 2001 From: Vasan Subramanian Date: Thu, 1 Dec 2016 15:41:48 +0530 Subject: [PATCH 62/88] Higher Order Components: include Create Issue in re-use --- src/App.jsx | 13 ++++++++++--- src/IssueAddNavItem.jsx | 21 +++------------------ 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 6a5acfc..412ae11 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,8 +4,9 @@ import { Navbar, Nav, NavItem, NavDropdown, MenuItem, Glyphicon } from 'react-bo import { LinkContainer } from 'react-router-bootstrap'; import IssueAddNavItem from './IssueAddNavItem.jsx'; +import withToast from './withToast.jsx'; -const Header = () => ( +const Header = (props) => ( Issue Tracker @@ -19,7 +20,7 @@ const Header = () => (