notifications
This commit is contained in:
parent
1f7856d6ae
commit
1230d16235
|
@ -21,6 +21,7 @@
|
||||||
"object-assign": "^4.0.1",
|
"object-assign": "^4.0.1",
|
||||||
"object-path": "^0.9.2",
|
"object-path": "^0.9.2",
|
||||||
"react": "^0.14.6",
|
"react": "^0.14.6",
|
||||||
|
"react-addons-css-transition-group": "^0.14.6",
|
||||||
"react-mini-router": "^2.0.0",
|
"react-mini-router": "^2.0.0",
|
||||||
"semver": "^5.1.0",
|
"semver": "^5.1.0",
|
||||||
"sortedindex-compare": "0.0.1",
|
"sortedindex-compare": "0.0.1",
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default React.createClass({
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let user;
|
let user;
|
||||||
if (!('uid' in this.props.user)) {
|
if (!(this.props.user != null && 'uid' in this.props.user)) {
|
||||||
user = (
|
user = (
|
||||||
<span><S />If you'd like to add a private GitHub repo,
|
<span><S />If you'd like to add a private GitHub repo,
|
||||||
<S /><a onClick={this._onSignIn}>Sign In</a> first.</span>
|
<S /><a onClick={this._onSignIn}>Sign In</a> first.</span>
|
||||||
|
|
|
@ -41,11 +41,13 @@ export default React.createClass({
|
||||||
let total = issues.open.size + issues.closed.size;
|
let total = issues.open.size + issues.closed.size;
|
||||||
|
|
||||||
// An issue may have been closed before the start of a milestone.
|
// An issue may have been closed before the start of a milestone.
|
||||||
|
if (issues.closed.size > 0) {
|
||||||
let head = issues.closed.list[0].closed_at;
|
let head = issues.closed.list[0].closed_at;
|
||||||
if (issues.length && milestone.created_at > head) {
|
if (issues.length && milestone.created_at > head) {
|
||||||
// This is the new start.
|
// This is the new start.
|
||||||
milestone.created_at = head;
|
milestone.created_at = head;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Actual, ideal & trend lines.
|
// Actual, ideal & trend lines.
|
||||||
let actual = lines.actual(issues.closed.list, milestone.created_at, total);
|
let actual = lines.actual(issues.closed.list, milestone.created_at, total);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
|
|
||||||
import actions from '../actions/appActions.js';
|
import actions from '../actions/appActions.js';
|
||||||
|
|
||||||
|
import Notify from './Notify.jsx';
|
||||||
import Icon from './Icon.jsx';
|
import Icon from './Icon.jsx';
|
||||||
import Link from './Link.jsx';
|
import Link from './Link.jsx';
|
||||||
|
|
||||||
|
@ -51,6 +52,8 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<Notify {...props.system.notification} />
|
||||||
<div id="head">
|
<div id="head">
|
||||||
{user}
|
{user}
|
||||||
|
|
||||||
|
@ -71,6 +74,7 @@ export default React.createClass({
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,69 @@
|
||||||
import React from 'react';
|
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() {
|
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() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
'system': {
|
'system': {
|
||||||
'loading': false
|
'loading': false,
|
||||||
|
'notification': null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,9 +67,12 @@ class AppStore extends Store {
|
||||||
this.set('system.loading', state);
|
this.set('system.loading', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement.
|
// Show a notification.
|
||||||
onSystemNotify() {
|
// 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, {
|
this.getMilestone(user, {
|
||||||
'owner': args.owner,
|
'owner': args.owner,
|
||||||
'name': args.name
|
'name': args.name
|
||||||
}, args.milestone);
|
}, args.milestone, true); // notify as well
|
||||||
} else {
|
} else {
|
||||||
// For a single project.
|
// For a single project.
|
||||||
_.find(this.get('list'), (obj) => {
|
_.find(this.get('list'), (obj) => {
|
||||||
|
@ -214,7 +214,7 @@ class ProjectsStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch a single milestone.
|
// Fetch a single milestone.
|
||||||
getMilestone(user, p, m) {
|
getMilestone(user, p, m, say) {
|
||||||
// Fetch the single milestone.
|
// Fetch the single milestone.
|
||||||
milestones.fetch(user, {
|
milestones.fetch(user, {
|
||||||
'owner': p.owner,
|
'owner': p.owner,
|
||||||
|
@ -222,33 +222,63 @@ class ProjectsStore extends Store {
|
||||||
'milestone': m
|
'milestone': m
|
||||||
}, this.cb((err, milestone) => { // async
|
}, this.cb((err, milestone) => { // async
|
||||||
// Save the error if project does not exist.
|
// 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.
|
// Now add in the issues.
|
||||||
this.getIssues(user, p, milestone);
|
this.getIssues(user, p, milestone, say);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all issues for a milestone.
|
// Fetch all issues for a milestone.
|
||||||
getIssues(user, p, m) {
|
getIssues(user, p, m, say) {
|
||||||
issues.fetchAll(user, {
|
issues.fetchAll(user, {
|
||||||
'owner': p.owner,
|
'owner': p.owner,
|
||||||
'name': p.name,
|
'name': p.name,
|
||||||
'milestone': m.number
|
'milestone': m.number
|
||||||
}, this.cb((err, obj) => { // async
|
}, this.cb((err, obj) => { // async
|
||||||
// Save any errors on the project.
|
// 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.
|
// Add in the issues to the milestone.
|
||||||
_.extend(m, { 'issues': obj });
|
_.extend(m, { 'issues': obj });
|
||||||
// Save the milestone.
|
// 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.
|
// Add a milestone for a project.
|
||||||
addMilestone(project, milestone) {
|
addMilestone(project, milestone, say) {
|
||||||
// Add in the stats.
|
// Add in the stats.
|
||||||
let i, j;
|
let i, j;
|
||||||
_.extend(milestone, { 'stats': stats(milestone) });
|
_.extend(milestone, { 'stats': stats(milestone) });
|
||||||
|
|
||||||
|
// Notify?
|
||||||
|
if (say) this.notify(milestone.stats);
|
||||||
|
|
||||||
// We are supposed to exist already.
|
// We are supposed to exist already.
|
||||||
if ((i = this.findIndex(project)) < 0) { throw 500; }
|
if ((i = this.findIndex(project)) < 0) { throw 500; }
|
||||||
|
|
||||||
|
@ -275,14 +305,23 @@ class ProjectsStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save an error from loading milestones or issues
|
// Save an error from loading milestones or issues
|
||||||
saveError(project, err) {
|
saveError(project, err, say=false) {
|
||||||
var idx;
|
var idx;
|
||||||
if ((idx = this.findIndex(project)) > -1) {
|
if ((idx = this.findIndex(project)) > -1) {
|
||||||
return this.push(`list.${idx}.errors`, err);
|
this.push(`list.${idx}.errors`, err);
|
||||||
} else {
|
} else {
|
||||||
// We are supposed to exist already.
|
// We are supposed to exist already.
|
||||||
throw 500;
|
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
|
// 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 {
|
#notify {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: -68px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fcfcfc;
|
background: #fcfcfc;
|
||||||
color: #aaafbf;
|
color: #aaafbf;
|
||||||
border-top: 3px solid #aaafbf;
|
border-top: 3px solid #aaafbf;
|
||||||
border-bottom: 1px solid #f3f4f8;
|
border-bottom: 1px solid #f3f4f8;
|
||||||
|
cursor: default;
|
||||||
|
.user-select(none);
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
float: right;
|
float: right;
|
||||||
|
@ -71,7 +72,7 @@ ul {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.system {
|
&.system {
|
||||||
top: 0%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
.transform(translateX(-50%) translateY(-50%));
|
.transform(translateX(-50%) translateY(-50%));
|
||||||
|
|
|
@ -8,4 +8,5 @@
|
||||||
@import "fonts.less";
|
@import "fonts.less";
|
||||||
@import "icons.less";
|
@import "icons.less";
|
||||||
@import "chart.less";
|
@import "chart.less";
|
||||||
|
@import "animations.less";
|
||||||
@import "app.less";
|
@import "app.less";
|
Loading…
Reference in New Issue