add a project form
This commit is contained in:
parent
0c8cadf38b
commit
3eaf4740be
|
@ -15,7 +15,7 @@ $ npm start
|
||||||
# Server started on port 8080
|
# Server started on port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
##CHANGELOG
|
##Changelog
|
||||||
|
|
||||||
3.0.0
|
###v3.0.0
|
||||||
- switch to React & Flux architecture
|
- switch to React & Flux architecture
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RouterMixin, navigate } from 'react-mini-router';
|
import { RouterMixin, navigate } from 'react-mini-router';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import lodash from './mixins/lodash.js';
|
||||||
|
|
||||||
import ProjectsPage from './pages/ProjectsPage.jsx';
|
import ProjectsPage from './pages/ProjectsPage.jsx';
|
||||||
import MilestonesPage from './pages/MilestonesPage.jsx';
|
import MilestonesPage from './pages/MilestonesPage.jsx';
|
||||||
|
@ -70,13 +71,14 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Route to a link.
|
// Route to a link.
|
||||||
|
// TODO: make this a named route.
|
||||||
navigate: navigate
|
navigate: navigate
|
||||||
},
|
},
|
||||||
|
|
||||||
// Show projects.
|
// Show projects.
|
||||||
projects() {
|
projects() {
|
||||||
document.title = 'Burnchart: GitHub Burndown Chart as a Service';
|
document.title = 'Burnchart: GitHub Burndown Chart as a Service';
|
||||||
actions.emit('projects.load');
|
process.nextTick(() => { actions.emit('projects.load'); });
|
||||||
return <ProjectsPage />;
|
return <ProjectsPage />;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -92,6 +94,7 @@ export default React.createClass({
|
||||||
|
|
||||||
// Add a project.
|
// Add a project.
|
||||||
addProject() {
|
addProject() {
|
||||||
|
document.title = 'Add a project';
|
||||||
return <AddProjectPage />;
|
return <AddProjectPage />;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -114,13 +117,7 @@ export default React.createClass({
|
||||||
return <div />;
|
return <div />;
|
||||||
} else {
|
} else {
|
||||||
blank = true;
|
blank = true;
|
||||||
|
actions.emit('system.loading', false);
|
||||||
// TODO: Hide any notifications.
|
|
||||||
// mediator.fire '!app/notify/hide'
|
|
||||||
|
|
||||||
// Each page is starting in a loading state.
|
|
||||||
actions.emit('system.loading', true);
|
|
||||||
|
|
||||||
return this.renderCurrentRoute();
|
return this.renderCurrentRoute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import App from '../App.jsx';
|
||||||
|
|
||||||
|
import actions from '../actions/appActions.js';
|
||||||
|
|
||||||
|
import Icon from './Icon.jsx';
|
||||||
|
import S from './Space.jsx';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
|
||||||
|
displayName: 'AddProjectForm.jsx',
|
||||||
|
|
||||||
|
// Sign user in.
|
||||||
|
_onSignIn() {
|
||||||
|
actions.emit('user.signin');
|
||||||
|
},
|
||||||
|
|
||||||
|
_onChange(evt) {
|
||||||
|
this.setState({ 'val': evt.target.value });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onKeyUp(evt) {
|
||||||
|
if (evt.key == 'Enter') {
|
||||||
|
this._onAdd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onAdd() {
|
||||||
|
let [ owner, name ] = this.state.val.split('/');
|
||||||
|
actions.emit('projects.add', { owner, name });
|
||||||
|
// Redirect to the dashboard.
|
||||||
|
App.navigate('/');
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState() {
|
||||||
|
return { 'val': '' };
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let user;
|
||||||
|
if (!('uid' in this.props.user)) {
|
||||||
|
user = (
|
||||||
|
<span><S />If you'd like to add a private GitHub repo,
|
||||||
|
<S /><a onClick={this._onSignIn}>Sign In</a> first.</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="add">
|
||||||
|
<div className="header">
|
||||||
|
<h2>Add a Project</h2>
|
||||||
|
<p>Type the name of a GitHub repository that has some
|
||||||
|
milestones with issues.{user}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="text" placeholder="user/repo" autoComplete="off"
|
||||||
|
onChange={this._onChange} value={this.state.val} onKeyUp={this._onKeyUp} />
|
||||||
|
</td>
|
||||||
|
<td><a onClick={this._onAdd}>Add</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="protip">
|
||||||
|
<Icon name="rocket"/> Protip: To see if a milestone is on track or not,
|
||||||
|
make sure it has a due date assigned to it.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
|
||||||
|
displayName: 'Space.jsx',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <span> </span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -54,7 +54,7 @@ export default class Store extends EventEmitter {
|
||||||
let obj = this.get(key);
|
let obj = this.get(key);
|
||||||
if (_.isArray(obj)) {
|
if (_.isArray(obj)) {
|
||||||
// TODO: Don't assume a string.
|
// TODO: Don't assume a string.
|
||||||
this.set(`${key}.${obj.length}`, val);
|
this.set(`${key}.${obj.length}`, val); // TODO: won't emit for root key
|
||||||
return obj.length - 1;
|
return obj.length - 1;
|
||||||
} else {
|
} else {
|
||||||
this.set(key, [ val ]);
|
this.set(key, [ val ]);
|
||||||
|
|
|
@ -27,8 +27,11 @@ export default {
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
_onChange(store, val, key) {;
|
_onChange(store, val, key) {
|
||||||
|
// TODO: this is not the right approach!
|
||||||
|
if (this.isMounted()) {
|
||||||
this.setState(this._getData(store));
|
this.setState(this._getData(store));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
_.mixin({
|
||||||
|
pluckMany: (source, keys) => {
|
||||||
|
if (!_.isArray(keys)) {
|
||||||
|
throw '`keys` needs to be an Array';
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.map(source, (item) => {
|
||||||
|
let obj = {};
|
||||||
|
for (let key of keys) {
|
||||||
|
obj[key] = item[key];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isInt: (val) => {
|
||||||
|
return !isNaN(val) && parseInt(Number(val)) === val &&
|
||||||
|
!isNaN(parseInt(val, 10));
|
||||||
|
}
|
||||||
|
});
|
|
@ -148,7 +148,7 @@ let isValid = (obj) => {
|
||||||
let rules = {
|
let rules = {
|
||||||
owner: (x) => { return (typeof x !== "undefined" && x !== null); },
|
owner: (x) => { return (typeof x !== "undefined" && x !== null); },
|
||||||
name: (x) => { return (typeof x !== "undefined" && x !== null); },
|
name: (x) => { return (typeof x !== "undefined" && x !== null); },
|
||||||
milestone: (x) => { return _.isFinite(x); }
|
milestone: (x) => { return _.isInt(x); } // mixin
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let key in obj) {
|
for (let key in obj) {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import actions from '../actions/appActions.js';
|
||||||
|
|
||||||
import Page from '../mixins/Page.js';
|
import Page from '../mixins/Page.js';
|
||||||
|
|
||||||
import Notify from '../components/Notify.jsx';
|
import Notify from '../components/Notify.jsx';
|
||||||
import Header from '../components/Header.jsx';
|
import Header from '../components/Header.jsx';
|
||||||
|
import AddProjectForm from '../components/AddProjectForm.jsx';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
|
|
||||||
|
@ -17,7 +20,11 @@ export default React.createClass({
|
||||||
<Notify />
|
<Notify />
|
||||||
<Header {...this.state} />
|
<Header {...this.state} />
|
||||||
|
|
||||||
<div id="page" />
|
<div id="page">
|
||||||
|
<div id="content" className="wrap">
|
||||||
|
<AddProjectForm user={this.state.app.user} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div className="wrap">
|
<div className="wrap">
|
||||||
|
|
|
@ -4,7 +4,6 @@ import Page from '../mixins/Page.js';
|
||||||
|
|
||||||
import Notify from '../components/Notify.jsx';
|
import Notify from '../components/Notify.jsx';
|
||||||
import Header from '../components/Header.jsx';
|
import Header from '../components/Header.jsx';
|
||||||
|
|
||||||
import Projects from '../components/Projects.jsx';
|
import Projects from '../components/Projects.jsx';
|
||||||
import Hero from '../components/Hero.jsx';
|
import Hero from '../components/Hero.jsx';
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class AppStore extends Store {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
system: {
|
system: {
|
||||||
loading: false
|
loading: true
|
||||||
},
|
},
|
||||||
user: {}
|
user: {}
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,9 +45,11 @@ class ProjectsStore extends Store {
|
||||||
this.set('user', user);
|
this.set('user', user);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Persist projects in local storage (sans milestones).
|
// Persist projects in local storage (sans milestones and issues).
|
||||||
this.on('list', (projects) => {
|
this.on('list.*', () => {
|
||||||
lscache.set('projects', _.pluckMany(projects, [ 'owner', 'name' ]));
|
if (process.browser) {
|
||||||
|
lscache.set('projects', _.pluckMany(this.get('list'), [ 'owner', 'name' ]));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset our index and re-sort.
|
// Reset our index and re-sort.
|
||||||
|
@ -62,13 +64,22 @@ class ProjectsStore extends Store {
|
||||||
onProjectsLoad() {
|
onProjectsLoad() {
|
||||||
let list = this.get('list');
|
let list = this.get('list');
|
||||||
|
|
||||||
|
let done = (err) => {
|
||||||
|
actions.emit('system.loading', false);
|
||||||
|
};
|
||||||
|
|
||||||
// Quit if we have no projects.
|
// Quit if we have no projects.
|
||||||
if (!list.length) return;
|
if (!list.length) return done();
|
||||||
|
|
||||||
|
actions.emit('system.loading', true);
|
||||||
|
|
||||||
// Wait for the user to get resolved.
|
// Wait for the user to get resolved.
|
||||||
this.get('user', (user) => {
|
this.get('user', (user) => {
|
||||||
// For all projects.
|
// For all projects.
|
||||||
async.map(list, (project, cb) => {
|
async.map(list, (project, cb) => {
|
||||||
|
// Skip any projects that already have milestones.
|
||||||
|
if ('milestones' in project) return cb();
|
||||||
|
|
||||||
// Fetch their milestones.
|
// Fetch their milestones.
|
||||||
milestones.fetchAll(user, project, (err, list) => {
|
milestones.fetchAll(user, project, (err, list) => {
|
||||||
// Save the error if project does not exist.
|
// Save the error if project does not exist.
|
||||||
|
@ -109,10 +120,7 @@ class ProjectsStore extends Store {
|
||||||
});
|
});
|
||||||
}, cb);
|
}, cb);
|
||||||
});
|
});
|
||||||
// All done, any errors are ignored as saved on projects.
|
}, done);
|
||||||
}, (err) => {
|
|
||||||
actions.emit('system.loading', false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { noCallThru } from 'proxyquire';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
let proxy = noCallThru();
|
|
||||||
|
|
||||||
import stats from '../src/js/modules/stats.js';
|
import stats from '../src/js/modules/stats.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
Loading…
Reference in New Issue