notifications

This commit is contained in:
Radek Stepan 2016-01-20 23:55:49 +01:00
parent 1f7856d6ae
commit 1230d16235
10 changed files with 194 additions and 42 deletions

View File

@ -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",

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@ -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

43
src/less/animations.less Normal file
View File

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

View File

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

View File

@ -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";