notifications
This commit is contained in:
parent
1f7856d6ae
commit
1230d16235
|
@ -21,6 +21,7 @@
|
|||
"object-assign": "^4.0.1",
|
||||
"object-path": "^0.9.2",
|
||||
"react": "^0.14.6",
|
||||
"react-addons-css-transition-group": "^0.14.6",
|
||||
"react-mini-router": "^2.0.0",
|
||||
"semver": "^5.1.0",
|
||||
"sortedindex-compare": "0.0.1",
|
||||
|
|
|
@ -39,7 +39,7 @@ export default React.createClass({
|
|||
|
||||
render() {
|
||||
let user;
|
||||
if (!('uid' in this.props.user)) {
|
||||
if (!(this.props.user != null && '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>
|
||||
|
|
|
@ -41,10 +41,12 @@ export default React.createClass({
|
|||
let total = issues.open.size + issues.closed.size;
|
||||
|
||||
// An issue may have been closed before the start of a milestone.
|
||||
let head = issues.closed.list[0].closed_at;
|
||||
if (issues.length && milestone.created_at > head) {
|
||||
// This is the new start.
|
||||
milestone.created_at = head;
|
||||
if (issues.closed.size > 0) {
|
||||
let head = issues.closed.list[0].closed_at;
|
||||
if (issues.length && milestone.created_at > head) {
|
||||
// This is the new start.
|
||||
milestone.created_at = head;
|
||||
}
|
||||
}
|
||||
|
||||
// Actual, ideal & trend lines.
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
|
||||
import actions from '../actions/appActions.js';
|
||||
|
||||
import Notify from './Notify.jsx';
|
||||
import Icon from './Icon.jsx';
|
||||
import Link from './Link.jsx';
|
||||
|
||||
|
@ -51,25 +52,28 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div id="head">
|
||||
{user}
|
||||
<div>
|
||||
<Notify {...props.system.notification} />
|
||||
<div id="head">
|
||||
{user}
|
||||
|
||||
<Link route={{ to: 'projects' }} id="icon">
|
||||
<Icon name={icon} />
|
||||
</Link>
|
||||
<Link route={{ to: 'projects' }} id="icon">
|
||||
<Icon name={icon} />
|
||||
</Link>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<Link route={{ to: 'addProject' }}>
|
||||
<Icon name="plus" /> Add a Project
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link route={{ to: 'demo' }}>
|
||||
<Icon name="computer" /> See Examples
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<Link route={{ to: 'addProject' }}>
|
||||
<Icon name="plus" /> Add a Project
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link route={{ to: 'demo' }}>
|
||||
<Icon name="computer" /> See Examples
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,69 @@
|
|||
import React from 'react';
|
||||
import Transition from 'react-addons-css-transition-group';
|
||||
|
||||
export default React.createClass({
|
||||
import actions from '../actions/appActions.js';
|
||||
|
||||
displayName: 'Header.jsx',
|
||||
import Icon from './Icon.jsx';
|
||||
|
||||
let Notify = React.createClass({
|
||||
|
||||
displayName: 'Notify.jsx',
|
||||
|
||||
_onClose() {
|
||||
actions.emit('system.notify');
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
'text': null,
|
||||
'type': '',
|
||||
'system': false,
|
||||
'icon': 'megaphone'
|
||||
};
|
||||
},
|
||||
|
||||
// TODO.
|
||||
render() {
|
||||
return false;
|
||||
let { text, system, type, icon, ttl } = this.props;
|
||||
|
||||
if (!text) return false;
|
||||
|
||||
if (system) {
|
||||
return (
|
||||
<div id="notify" className={`system ${type}`}>
|
||||
<Icon name={icon} />
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div id="notify" className={type}>
|
||||
<span className="close" onClick={this._onClose} />
|
||||
<Icon name={icon} />
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
export default React.createClass({
|
||||
|
||||
// TODO: animate in
|
||||
render() {
|
||||
if (!this.props.id) return false; // TODO: fix ghost
|
||||
|
||||
let name = (this.props.system) ? 'animCenter' : 'animTop';
|
||||
|
||||
return (
|
||||
<Transition transitionName={name}
|
||||
transitionEnterTimeout={2000}
|
||||
transitionLeaveTimeout={1000}
|
||||
component="div"
|
||||
>
|
||||
<Notify {...this.props} key={this.props.id} />
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -16,7 +16,8 @@ class AppStore extends Store {
|
|||
constructor() {
|
||||
super({
|
||||
'system': {
|
||||
'loading': false
|
||||
'loading': false,
|
||||
'notification': null
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -66,9 +67,12 @@ class AppStore extends Store {
|
|||
this.set('system.loading', state);
|
||||
}
|
||||
|
||||
// TODO: implement.
|
||||
onSystemNotify() {
|
||||
|
||||
// Show a notification.
|
||||
// TODO: multiple notifications & ttl
|
||||
onSystemNotify(args) {
|
||||
if (!_.isObject(args)) args = { 'text': args };
|
||||
args.id = _.uniqueId('m-');
|
||||
this.set('system.notification', args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ class ProjectsStore extends Store {
|
|||
this.getMilestone(user, {
|
||||
'owner': args.owner,
|
||||
'name': args.name
|
||||
}, args.milestone);
|
||||
}, args.milestone, true); // notify as well
|
||||
} else {
|
||||
// For a single project.
|
||||
_.find(this.get('list'), (obj) => {
|
||||
|
@ -214,7 +214,7 @@ class ProjectsStore extends Store {
|
|||
}
|
||||
|
||||
// Fetch a single milestone.
|
||||
getMilestone(user, p, m) {
|
||||
getMilestone(user, p, m, say) {
|
||||
// Fetch the single milestone.
|
||||
milestones.fetch(user, {
|
||||
'owner': p.owner,
|
||||
|
@ -222,33 +222,63 @@ class ProjectsStore extends Store {
|
|||
'milestone': m
|
||||
}, this.cb((err, milestone) => { // async
|
||||
// Save the error if project does not exist.
|
||||
if (err) return this.saveError(p, err);
|
||||
if (err) return this.saveError(p, err, say);
|
||||
// Now add in the issues.
|
||||
this.getIssues(user, p, milestone);
|
||||
this.getIssues(user, p, milestone, say);
|
||||
}));
|
||||
}
|
||||
|
||||
// Fetch all issues for a milestone.
|
||||
getIssues(user, p, m) {
|
||||
getIssues(user, p, m, say) {
|
||||
issues.fetchAll(user, {
|
||||
'owner': p.owner,
|
||||
'name': p.name,
|
||||
'milestone': m.number
|
||||
}, this.cb((err, obj) => { // async
|
||||
// Save any errors on the project.
|
||||
if (err) return this.saveError(p, err);
|
||||
if (err) return this.saveError(p, err, say);
|
||||
// Add in the issues to the milestone.
|
||||
_.extend(m, { 'issues': obj });
|
||||
// Save the milestone.
|
||||
this.addMilestone(p, m);
|
||||
this.addMilestone(p, m, say);
|
||||
}));
|
||||
}
|
||||
|
||||
// Talk about the stats of a milestone.
|
||||
notify(stats) {
|
||||
if (stats.isEmpty) {
|
||||
return actions.emit('system.notify', {
|
||||
'text': 'This milestone has no issues',
|
||||
'type': 'warn',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
|
||||
if (stats.isDone) {
|
||||
actions.emit('system.notify', {
|
||||
'text': 'This milestone is complete',
|
||||
'type': 'success'
|
||||
});
|
||||
}
|
||||
|
||||
if (stats.isOverdue) {
|
||||
actions.emit('system.notify', {
|
||||
'text': 'This milestone is overdue',
|
||||
'type': 'warn'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add a milestone for a project.
|
||||
addMilestone(project, milestone) {
|
||||
addMilestone(project, milestone, say) {
|
||||
// Add in the stats.
|
||||
let i, j;
|
||||
_.extend(milestone, { 'stats': stats(milestone) });
|
||||
|
||||
// Notify?
|
||||
if (say) this.notify(milestone.stats);
|
||||
|
||||
// We are supposed to exist already.
|
||||
if ((i = this.findIndex(project)) < 0) { throw 500; }
|
||||
|
||||
|
@ -275,14 +305,23 @@ class ProjectsStore extends Store {
|
|||
}
|
||||
|
||||
// Save an error from loading milestones or issues
|
||||
saveError(project, err) {
|
||||
saveError(project, err, say=false) {
|
||||
var idx;
|
||||
if ((idx = this.findIndex(project)) > -1) {
|
||||
return this.push(`list.${idx}.errors`, err);
|
||||
this.push(`list.${idx}.errors`, err);
|
||||
} else {
|
||||
// We are supposed to exist already.
|
||||
throw 500;
|
||||
}
|
||||
|
||||
// Notify?
|
||||
if (!say) return;
|
||||
actions.emit('system.notify', {
|
||||
'text': err,
|
||||
'type': 'alert',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
});
|
||||
}
|
||||
|
||||
// Sort projects (update the index). Can pass reference to the
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
.easeInOutBack(@time) {
|
||||
.transition(top @time cubic-bezier(0.68, -0.55, 0.265, 1.55));
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
.animTop-enter {
|
||||
top: -68px;
|
||||
}
|
||||
|
||||
.animTop-enter-active {
|
||||
top: 0px;
|
||||
.easeInOutBack(2000ms);
|
||||
}
|
||||
|
||||
.animTop-leave {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.animTop-leave-active {
|
||||
top: -68px;
|
||||
.easeInOutBack(1000ms);
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
.animCenter-enter {
|
||||
top: 0%;
|
||||
}
|
||||
|
||||
.animCenter-enter-active {
|
||||
top: 50%;
|
||||
.easeInOutBack(2000ms);
|
||||
}
|
||||
|
||||
.animCenter-leave {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.animCenter-leave-active {
|
||||
top: 0%;
|
||||
.easeInOutBack(1000ms);
|
||||
}
|
|
@ -50,13 +50,14 @@ ul {
|
|||
|
||||
#notify {
|
||||
position: fixed;
|
||||
top: -68px;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background: #fcfcfc;
|
||||
color: #aaafbf;
|
||||
border-top: 3px solid #aaafbf;
|
||||
border-bottom: 1px solid #f3f4f8;
|
||||
cursor: default;
|
||||
.user-select(none);
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
|
@ -71,7 +72,7 @@ ul {
|
|||
}
|
||||
|
||||
&.system {
|
||||
top: 0%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 500px;
|
||||
.transform(translateX(-50%) translateY(-50%));
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
@import "fonts.less";
|
||||
@import "icons.less";
|
||||
@import "chart.less";
|
||||
@import "animations.less";
|
||||
@import "app.less";
|
Loading…
Reference in New Issue