add a project form

This commit is contained in:
Radek Stepan 2016-01-16 20:27:57 +01:00
parent 0c8cadf38b
commit 3eaf4740be
13 changed files with 152 additions and 29 deletions

View File

@ -15,7 +15,7 @@ $ npm start
# Server started on port 8080
```
##CHANGELOG
##Changelog
3.0.0
###v3.0.0
- switch to React & Flux architecture

View File

@ -1,6 +1,7 @@
import React from 'react';
import { RouterMixin, navigate } from 'react-mini-router';
import _ from 'lodash';
import lodash from './mixins/lodash.js';
import ProjectsPage from './pages/ProjectsPage.jsx';
import MilestonesPage from './pages/MilestonesPage.jsx';
@ -70,13 +71,14 @@ export default React.createClass({
},
// Route to a link.
// TODO: make this a named route.
navigate: navigate
},
// Show projects.
projects() {
document.title = 'Burnchart: GitHub Burndown Chart as a Service';
actions.emit('projects.load');
process.nextTick(() => { actions.emit('projects.load'); });
return <ProjectsPage />;
},
@ -92,6 +94,7 @@ export default React.createClass({
// Add a project.
addProject() {
document.title = 'Add a project';
return <AddProjectPage />;
},
@ -114,13 +117,7 @@ export default React.createClass({
return <div />;
} else {
blank = true;
// TODO: Hide any notifications.
// mediator.fire '!app/notify/hide'
// Each page is starting in a loading state.
actions.emit('system.loading', true);
actions.emit('system.loading', false);
return this.renderCurrentRoute();
}
}

View File

@ -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>
);
}
});

View File

@ -0,0 +1,11 @@
import React from 'react';
export default React.createClass({
displayName: 'Space.jsx',
render() {
return <span>&nbsp;</span>;
}
});

View File

@ -54,7 +54,7 @@ export default class Store extends EventEmitter {
let obj = this.get(key);
if (_.isArray(obj)) {
// 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;
} else {
this.set(key, [ val ]);

View File

@ -27,8 +27,11 @@ export default {
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));
}
},
getInitialState() {

22
src/js/mixins/lodash.js Normal file
View File

@ -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));
}
});

View File

@ -148,7 +148,7 @@ let isValid = (obj) => {
let rules = {
owner: (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) {

View File

@ -1,9 +1,12 @@
import React from 'react';
import actions from '../actions/appActions.js';
import Page from '../mixins/Page.js';
import Notify from '../components/Notify.jsx';
import Header from '../components/Header.jsx';
import AddProjectForm from '../components/AddProjectForm.jsx';
export default React.createClass({
@ -17,7 +20,11 @@ export default React.createClass({
<Notify />
<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 className="wrap">

View File

@ -4,7 +4,6 @@ import Page from '../mixins/Page.js';
import Notify from '../components/Notify.jsx';
import Header from '../components/Header.jsx';
import Projects from '../components/Projects.jsx';
import Hero from '../components/Hero.jsx';

View File

@ -16,7 +16,7 @@ class AppStore extends Store {
constructor() {
super({
system: {
loading: false
loading: true
},
user: {}
});

View File

@ -45,9 +45,11 @@ class ProjectsStore extends Store {
this.set('user', user);
});
// Persist projects in local storage (sans milestones).
this.on('list', (projects) => {
lscache.set('projects', _.pluckMany(projects, [ 'owner', 'name' ]));
// Persist projects in local storage (sans milestones and issues).
this.on('list.*', () => {
if (process.browser) {
lscache.set('projects', _.pluckMany(this.get('list'), [ 'owner', 'name' ]));
}
});
// Reset our index and re-sort.
@ -62,13 +64,22 @@ class ProjectsStore extends Store {
onProjectsLoad() {
let list = this.get('list');
let done = (err) => {
actions.emit('system.loading', false);
};
// 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.
this.get('user', (user) => {
// For all projects.
async.map(list, (project, cb) => {
// Skip any projects that already have milestones.
if ('milestones' in project) return cb();
// Fetch their milestones.
milestones.fetchAll(user, project, (err, list) => {
// Save the error if project does not exist.
@ -109,10 +120,7 @@ class ProjectsStore extends Store {
});
}, cb);
});
// All done, any errors are ignored as saved on projects.
}, (err) => {
actions.emit('system.loading', false);
});
}, done);
});
}

View File

@ -1,10 +1,7 @@
import { assert } from 'chai';
import { noCallThru } from 'proxyquire';
import path from 'path';
import moment from 'moment';
let proxy = noCallThru();
import stats from '../src/js/modules/stats.js';
export default {