repos store
This commit is contained in:
parent
516c2af417
commit
cb5d6ea712
|
@ -56,45 +56,6 @@ export default {
|
|||
request(data, cb);
|
||||
},
|
||||
|
||||
// Get all open milestones.
|
||||
allMilestones: (user, { owner, name }, cb) => {
|
||||
let token = (user && user.credential != null) ? user.credential.accessToken : null;
|
||||
let data = _.defaults({
|
||||
'path': `/repos/${owner}/${name}/milestones`,
|
||||
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' },
|
||||
'headers': headers(token)
|
||||
}, defaults.github);
|
||||
|
||||
request(data, cb);
|
||||
},
|
||||
|
||||
// Get one open milestone.
|
||||
oneMilestone: (user, { owner, name, milestone }, cb) => {
|
||||
let token = (user && user.credential != null) ? user.credential.accessToken : null;
|
||||
let data = _.defaults({
|
||||
'path': `/repos/${owner}/${name}/milestones/${milestone}`,
|
||||
'query': { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' },
|
||||
'headers': headers(token)
|
||||
}, defaults.github);
|
||||
|
||||
request(data, function(err, data) {
|
||||
console.log('oneMilestone', data);
|
||||
cb(err, data);
|
||||
});
|
||||
},
|
||||
|
||||
// Get all issues for a state..
|
||||
allIssues: (user, { owner, name, milestone }, query, cb) => {
|
||||
let token = (user && user.credential != null) ? user.credential.accessToken : null;
|
||||
let data = _.defaults({
|
||||
'path': `/repos/${owner}/${name}/issues`,
|
||||
'query': _.extend(query, { milestone, 'per_page': '100' }),
|
||||
'headers': headers(token)
|
||||
}, defaults.github);
|
||||
|
||||
return request(data, cb);
|
||||
},
|
||||
|
||||
allProjects: (user, { owner, name }, cb) => {
|
||||
let token = (user && user.credential != null) ? user.credential.accessToken : null;
|
||||
|
||||
|
@ -117,10 +78,8 @@ export default {
|
|||
},
|
||||
method: 'POST',
|
||||
}, defaults.github);
|
||||
request(data, (err, result) => {
|
||||
console.log('graphql response!', err, result);
|
||||
cb(err, result);
|
||||
});
|
||||
|
||||
request(data, cb);
|
||||
},
|
||||
|
||||
oneProject: (user, { owner, name, project }, cb) => {
|
||||
|
@ -147,11 +106,8 @@ export default {
|
|||
},
|
||||
method: 'POST',
|
||||
}, defaults.github);
|
||||
request(data, (err, result) => {
|
||||
console.log('graphql response!', err, result);
|
||||
// Filter down to cards that are issues (as opposed to notes) here?
|
||||
cb(err, result);
|
||||
});
|
||||
|
||||
request(data, cb);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -246,6 +202,3 @@ let error = (err) => {
|
|||
|
||||
return text;
|
||||
};
|
||||
|
||||
// oneMilestone:
|
||||
// - returns lists of open and closed issues.
|
|
@ -1,8 +1,8 @@
|
|||
// The app store needs to go last because it loads user.
|
||||
import projectsStore from './projectsStore.js';
|
||||
import reposStore from './reposStore.js';
|
||||
import appStore from './appStore.js';
|
||||
|
||||
export default {
|
||||
'app': appStore,
|
||||
'projects': projectsStore
|
||||
'repos': reposStore
|
||||
};
|
||||
|
|
|
@ -12,17 +12,17 @@ import stats from '../modules/stats.js';
|
|||
import request from '../modules/github/request.js';
|
||||
import issues from '../modules/github/issues.js';
|
||||
|
||||
class ProjectsStore extends Store {
|
||||
class ReposStore extends Store {
|
||||
|
||||
// Initial payload.
|
||||
constructor() {
|
||||
// Init the projects from local storage.
|
||||
let list = lscache.get('projects') || [];
|
||||
// Init the repos from local storage.
|
||||
const list = lscache.get('repos') || [];
|
||||
|
||||
super({
|
||||
// A stack of projects.
|
||||
list: list,
|
||||
// A sorted projects and milestones index.
|
||||
// A stack of repos.
|
||||
list,
|
||||
// A sorted repos and projects index.
|
||||
'index': [],
|
||||
// The default sort order.
|
||||
'sortBy': 'priority',
|
||||
|
@ -30,20 +30,20 @@ class ProjectsStore extends Store {
|
|||
'sortFns': [ 'progress', 'priority', 'name' ]
|
||||
});
|
||||
|
||||
// Listen to only projects actions.
|
||||
actions.on('projects.*', (obj, event) => {
|
||||
// Listen to only repos actions.
|
||||
actions.on('repos.*', (obj, event) => {
|
||||
let fn = `on.${event}`.replace(/[.]+(\w|$)/g, (m, p) => p.toUpperCase());
|
||||
// Run?
|
||||
(fn in this) && this[fn](obj);
|
||||
});
|
||||
|
||||
// Listen to when user is ready and save info on us.
|
||||
actions.on('user.ready', (user) => this.set('user', user));
|
||||
actions.on('user.ready', user => this.set('user', user));
|
||||
|
||||
// Persist projects in local storage (sans milestones and issues).
|
||||
// Persist repos in local storage (sans projects and issues).
|
||||
this.on('list.*', () => {
|
||||
if (process.browser) {
|
||||
lscache.set('projects', _.pluckMany(this.get('list'), [ 'owner', 'name' ]));
|
||||
lscache.set('repos', _.pluckMany(this.get('list'), [ 'owner', 'name' ]));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -56,56 +56,54 @@ class ProjectsStore extends Store {
|
|||
|
||||
// Debounce.
|
||||
if (process.browser) { // easier to test
|
||||
this.onProjectsSearch = _.debounce(this.onProjectsSearch, 500);
|
||||
this.onReposSearch = _.debounce(this.onReposSearch, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch milestone(s) and issues for a project(s).
|
||||
onProjectsLoad(args) {
|
||||
let projects = this.get('list');
|
||||
// Fetch issues for a repo(s).
|
||||
onReposLoad(args) {
|
||||
let repos = this.get('list');
|
||||
|
||||
// Reset first.
|
||||
projects = _.each(projects, (p) => delete p.errors);
|
||||
repos = _.each(repos, r => delete r.errors);
|
||||
|
||||
// Wait for the user to get resolved.
|
||||
this.get('user', this.cb((user) => { // async
|
||||
this.get('user', this.cb(user => { // async
|
||||
if (args) {
|
||||
if ('milestone' in args) {
|
||||
// For a single milestone.
|
||||
this.getMilestone(user, {
|
||||
if ('project' in args) {
|
||||
// For a single project.
|
||||
this.getProject(user, {
|
||||
'owner': args.owner,
|
||||
'name': args.name
|
||||
}, args.milestone, true); // notify as well
|
||||
} else if ('project' in args) {
|
||||
// ...
|
||||
}, args.project, true); // notify as well
|
||||
} else {
|
||||
// For a single project.
|
||||
_.find(this.get('list'), (obj) => {
|
||||
// For a single repo.
|
||||
_.find(this.get('list'), obj => {
|
||||
if (args.owner == obj.owner && args.name == obj.name) {
|
||||
args = obj; // expand by saved properties
|
||||
return true;
|
||||
};
|
||||
return false;
|
||||
});
|
||||
this.getProject(user, args);
|
||||
this.getRepo(user, args);
|
||||
}
|
||||
} else {
|
||||
// For all projects.
|
||||
_.each(projects, project => this.getProject(user, project));
|
||||
// For all repos.
|
||||
_.each(repos, r => this.getRepo(user, r));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Push to the stack unless it exists already.
|
||||
onProjectsAdd(project) {
|
||||
if (!_.find(this.get('list'), project)) {
|
||||
this.push('list', project);
|
||||
onRepoAdd(repo) {
|
||||
if (!_.find(this.get('list'), repo)) {
|
||||
this.push('list', repo);
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through projects sort order.
|
||||
onProjectsSort() {
|
||||
let { sortBy, sortFns } = this.get();
|
||||
// Cycle through repos sort order.
|
||||
onReposSort() {
|
||||
const { sortBy, sortFns } = this.get();
|
||||
|
||||
let idx = 1 + sortFns.indexOf(sortBy);
|
||||
if (idx === sortFns.length) idx = 0;
|
||||
|
@ -113,8 +111,8 @@ class ProjectsStore extends Store {
|
|||
this.set('sortBy', sortFns[idx]);
|
||||
}
|
||||
|
||||
// Demonstration projects.
|
||||
onProjectsDemo() {
|
||||
// Demonstration repos.
|
||||
onReposDemo() {
|
||||
this.set({
|
||||
'list': [
|
||||
{ 'owner': 'd3', 'name': 'd3' },
|
||||
|
@ -125,8 +123,8 @@ class ProjectsStore extends Store {
|
|||
});
|
||||
}
|
||||
|
||||
// Search for projects.
|
||||
onProjectsSearch(text) {
|
||||
// Search for repos.
|
||||
onRepoSearch(text) {
|
||||
if (!text || !text.length) return;
|
||||
|
||||
// Wait for the user to get resolved.
|
||||
|
@ -169,10 +167,10 @@ class ProjectsStore extends Store {
|
|||
}));
|
||||
}
|
||||
|
||||
// Delete a project.
|
||||
onProjectsDelete(project) {
|
||||
let i = this.findIndex(project);
|
||||
// Delete the project.
|
||||
// Delete a repo.
|
||||
onRepoDelete(repo) {
|
||||
const i = this.findIndex(repo);
|
||||
// Delete the repo.
|
||||
this.del(`list.${i}`);
|
||||
// And the index, sorting again.
|
||||
this.set('index', []);
|
||||
|
@ -183,10 +181,10 @@ class ProjectsStore extends Store {
|
|||
comparator() {
|
||||
let { list, sortBy } = this.get();
|
||||
|
||||
// Convert existing index into actual project milestone.
|
||||
// Convert existing index into actual repo project.
|
||||
let deIdx = (fn) => {
|
||||
return ([ i, j ], ...rest) => {
|
||||
return fn.apply(this, [ [ list[i], list[i].milestones[j] ] ].concat(rest));
|
||||
return fn.apply(this, [ [ list[i], list[i].projects[j] ] ].concat(rest));
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -205,43 +203,43 @@ class ProjectsStore extends Store {
|
|||
switch (sortBy) {
|
||||
// From highest progress points.
|
||||
case 'progress':
|
||||
return deIdx(([ , aM ], [ , bM ]) => {
|
||||
defaults([ aM, bM ], { 'stats.progress.points': 0 });
|
||||
return deIdx(([ , aP ], [ , bP ]) => {
|
||||
defaults([ aP, bP ], { 'stats.progress.points': 0 });
|
||||
// Simple points difference.
|
||||
return aM.stats.progress.points - bM.stats.progress.points;
|
||||
return aP.stats.progress.points - bP.stats.progress.points;
|
||||
});
|
||||
|
||||
// From most delayed in days.
|
||||
case 'priority':
|
||||
return deIdx(([ , aM ], [ , bM ]) => {
|
||||
// Milestones with no deadline are always at the "beginning".
|
||||
defaults([ aM, bM ], { 'stats.progress.time': 0, 'stats.days': 1e3 });
|
||||
return deIdx(([ , aP ], [ , bP ]) => {
|
||||
// Projects with no deadline are always at the "beginning".
|
||||
defaults([ aP, bP ], { 'stats.progress.time': 0, 'stats.days': 1e3 });
|
||||
// % difference in progress times the number of days ahead or behind.
|
||||
let [ $a, $b ] = _.map([ aM, bM ], ({ stats }) => {
|
||||
let [ $a, $b ] = _.map([ aP, bP ], ({ stats }) => {
|
||||
return (stats.progress.points - stats.progress.time) * stats.days;
|
||||
});
|
||||
|
||||
return $b - $a;
|
||||
});
|
||||
|
||||
// Based on project then milestone name including semver.
|
||||
// Based on repo then project name including semver.
|
||||
case 'name':
|
||||
return deIdx(([ aP, aM ], [ bP, bM ]) => {
|
||||
return deIdx(([ aR, aP ], [ bR, bP ]) => {
|
||||
let owner, name;
|
||||
|
||||
if (owner = bP.owner.localeCompare(aP.owner)) {
|
||||
if (owner = bR.owner.localeCompare(aR.owner)) {
|
||||
return owner;
|
||||
}
|
||||
if (name = bP.name.localeCompare(aP.name)) {
|
||||
if (name = bR.name.localeCompare(aR.name)) {
|
||||
return name;
|
||||
}
|
||||
|
||||
// Try semver.
|
||||
if (semver.valid(bM.title) && semver.valid(aM.title)) {
|
||||
return semver.gt(bM.title, aM.title);
|
||||
if (semver.valid(bP.title) && semver.valid(aP.title)) {
|
||||
return semver.gt(bP.title, aP.title);
|
||||
// Back to string compare.
|
||||
} else {
|
||||
return bM.title.localeCompare(aM.title);
|
||||
return bP.title.localeCompare(aP.title);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -251,61 +249,39 @@ class ProjectsStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch milestones and issues for a project.
|
||||
getProject(user, p) {
|
||||
// Fetch their milestones.
|
||||
request.allMilestones(user, p, this.cb((err, milestones) => { // async
|
||||
// Save the error if project does not exist.
|
||||
if (err) return this.saveError(p, err);
|
||||
// Now add in the issues.
|
||||
milestones.forEach((milestone) => {
|
||||
// Do we have this milestone already? Skip fetching issues then.
|
||||
if (!_.find(p.milestones, ({ number }) => {
|
||||
return milestone.number === number;
|
||||
})) {
|
||||
// Fetch all the issues for this milestone.
|
||||
this.getIssues(user, p, milestone);
|
||||
}
|
||||
// Fetch projects in a repo.
|
||||
getProjects(user, r) {
|
||||
request.allProjects(user, r, this.cb((err, projects) => { // async
|
||||
// Save the error if repo does not exist.
|
||||
if (err) return this.saveError(r, err);
|
||||
projects.forEach(obj => {
|
||||
// Save the projet.
|
||||
// TODO do something with obj and projects
|
||||
this.addProject(r, obj, say);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// Fetch a single milestone.
|
||||
getMilestone(user, p, m, say) {
|
||||
// Fetch the single milestone.
|
||||
request.oneMilestone(user, {
|
||||
'owner': p.owner,
|
||||
'name': p.name,
|
||||
'milestone': m
|
||||
}, this.cb((err, milestone) => { // async
|
||||
// Save the error if project does not exist.
|
||||
if (err) return this.saveError(p, err, say);
|
||||
// Now add in the issues.
|
||||
this.getIssues(user, p, milestone, say);
|
||||
}));
|
||||
}
|
||||
|
||||
// Fetch all issues for a milestone.
|
||||
getIssues(user, p, m, say) {
|
||||
issues.fetchAll(user, {
|
||||
'owner': p.owner,
|
||||
'name': p.name,
|
||||
'milestone': m.number
|
||||
// Fetch a single project.
|
||||
getProject(user, r, p, say) {
|
||||
request.oneProject(user, {
|
||||
'owner': r.owner,
|
||||
'name': r.name,
|
||||
'project': p
|
||||
}, this.cb((err, obj) => { // async
|
||||
// Save any errors on the project.
|
||||
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, say);
|
||||
// Save the error if repo does not exist.
|
||||
if (err) return this.saveError(r, err, say);
|
||||
// Save the projet.
|
||||
// TODO do something with obj and p
|
||||
this.addProject(r, obj, say);
|
||||
}));
|
||||
}
|
||||
|
||||
// Talk about the stats of a milestone.
|
||||
notify(milestone) {
|
||||
if (milestone.stats.isEmpty) {
|
||||
// Talk about the stats of a project.
|
||||
notify(project) {
|
||||
if (project.stats.isEmpty) {
|
||||
let left;
|
||||
if (left = milestone.issues.open.size) {
|
||||
if (left = project.issues.open.size) {
|
||||
return actions.emit('system.notify', {
|
||||
'text': `No progress has been made, ${left} point${(left > 1) ? 's' : ''} left`,
|
||||
'system': true,
|
||||
|
@ -313,7 +289,7 @@ class ProjectsStore extends Store {
|
|||
});
|
||||
} else {
|
||||
return actions.emit('system.notify', {
|
||||
'text': 'This milestone has no issues',
|
||||
'text': 'This project has no issues',
|
||||
'type': 'warn',
|
||||
'system': true,
|
||||
'ttl': null
|
||||
|
@ -321,66 +297,66 @@ class ProjectsStore extends Store {
|
|||
}
|
||||
}
|
||||
|
||||
if (milestone.stats.isDone) {
|
||||
if (project.stats.isDone) {
|
||||
actions.emit('system.notify', {
|
||||
'text': 'This milestone is complete',
|
||||
'text': 'This project is complete',
|
||||
'type': 'success'
|
||||
});
|
||||
}
|
||||
|
||||
if (milestone.stats.isOverdue) {
|
||||
if (project.stats.isOverdue) {
|
||||
actions.emit('system.notify', {
|
||||
'text': 'This milestone is overdue',
|
||||
'text': 'This project is overdue',
|
||||
'type': 'warn'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add a milestone for a project.
|
||||
addMilestone(project, milestone, say) {
|
||||
// Add a project for a repo.
|
||||
addProject(repo, project, say) {
|
||||
// Add in the stats.
|
||||
let i, j;
|
||||
_.extend(milestone, { 'stats': stats(milestone) });
|
||||
_.extend(project, { 'stats': stats(project) });
|
||||
|
||||
// Notify?
|
||||
if (say) this.notify(milestone);
|
||||
if (say) this.notify(project);
|
||||
|
||||
// If project hasn't been found, add it behind the scenes.
|
||||
if ((i = this.findIndex(project)) < 0) {
|
||||
i = this.push('list', project);
|
||||
// If repo hasn't been found, add it behind the scenes.
|
||||
if ((i = this.findIndex(repo)) < 0) {
|
||||
i = this.push('list', repo);
|
||||
}
|
||||
|
||||
// Does the milestone exist already?
|
||||
let milestones;
|
||||
if (milestones = this.get(`list.${i}.milestones`)) {
|
||||
j = _.findIndex(milestones, { 'number': milestone.number });
|
||||
// Does the project exist already?
|
||||
let projects;
|
||||
if (projects = this.get(`list.${i}.projects`)) {
|
||||
j = _.findIndex(projects, { 'number': project.number });
|
||||
// Just make an update then.
|
||||
if (j != -1) {
|
||||
return this.set(`list.${i}.milestones.${j}`, milestone);
|
||||
return this.set(`list.${i}.projects.${j}`, project);
|
||||
}
|
||||
}
|
||||
|
||||
// Push the milestone and return the index.
|
||||
j = this.push(`list.${i}.milestones`, milestone);
|
||||
// Push the project and return the index.
|
||||
j = this.push(`list.${i}.projects`, project);
|
||||
|
||||
// Now index this milestone.
|
||||
this.sort([ i, j ], [ project, milestone ]);
|
||||
// Now index this project.
|
||||
this.sort([ i, j ], [ repo, project ]);
|
||||
}
|
||||
|
||||
// Find index of a project.
|
||||
// Find index of a repo.
|
||||
findIndex({ owner, name }) {
|
||||
return _.findIndex(this.get('list'), { owner, name });
|
||||
}
|
||||
|
||||
// Save an error from loading milestones or issues.
|
||||
// TODO: clear these when we fetch all projects anew.
|
||||
saveError(project, err, say=false) {
|
||||
// Save an error from loading projects.
|
||||
// TODO: clear these when we fetch all repos anew.
|
||||
saveError(repo, err, say=false) {
|
||||
var idx;
|
||||
if ((idx = this.findIndex(project)) > -1) {
|
||||
if ((idx = this.findIndex(repo)) > -1) {
|
||||
this.push(`list.${idx}.errors`, err);
|
||||
} else {
|
||||
// Create the stub project behind the scenes.
|
||||
this.push('list', _.extend({}, project, { 'errors': [ err ] }));
|
||||
// Create the stub repo behind the scenes.
|
||||
this.push('list', _.extend({}, repo, { 'errors': [ err ] }));
|
||||
}
|
||||
|
||||
// Notify?
|
||||
|
@ -393,29 +369,29 @@ class ProjectsStore extends Store {
|
|||
});
|
||||
}
|
||||
|
||||
// Sort projects (update the index). Can pass reference to the
|
||||
// project and milestone index in the stack.
|
||||
// Sort repos (update the index). Can pass reference to the
|
||||
// repo and project index in the stack.
|
||||
sort(ref, data) {
|
||||
let idx;
|
||||
// Get the existing index.
|
||||
let index = this.get('index');
|
||||
const index = this.get('index');
|
||||
|
||||
// Index one milestone in an already sorted index.
|
||||
// Index one project in an already sorted index.
|
||||
if (ref) {
|
||||
idx = sortedIndex(index, data, this.comparator());
|
||||
index.splice(idx, 0, ref);
|
||||
// Sort them all.
|
||||
} else {
|
||||
let list = this.get('list');
|
||||
const list = this.get('list');
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let p = list[i];
|
||||
// TODO: need to show projects that failed too...
|
||||
if (p.milestones == null) continue;
|
||||
// Walk the milestones.
|
||||
for (let j = 0; j < p.milestones.length; j++) {
|
||||
let m = p.milestones[j];
|
||||
const r = list[i];
|
||||
// TODO: need to show repos that failed too...
|
||||
if (r.projects == null) continue;
|
||||
// Walk the projects.
|
||||
for (let j = 0; j < r.projects.length; j++) {
|
||||
const p = r.projects[j];
|
||||
// Run a comparator here inserting into index.
|
||||
idx = sortedIndex(index, [ p, m ], this.comparator());
|
||||
idx = sortedIndex(index, [ r, p ], this.comparator());
|
||||
index.splice(idx, 0, [ i, j ]);
|
||||
}
|
||||
}
|
||||
|
@ -424,7 +400,7 @@ class ProjectsStore extends Store {
|
|||
this.set('index', index);
|
||||
}
|
||||
|
||||
// Do we have this project? Case-insensitive.
|
||||
// Do we have this repo? Case-insensitive.
|
||||
has(o, n) {
|
||||
o = o.toUpperCase() ; n = n.toUpperCase();
|
||||
return !!_.find(this.get('list'), ({ owner, name }) => {
|
||||
|
@ -434,4 +410,4 @@ class ProjectsStore extends Store {
|
|||
|
||||
}
|
||||
|
||||
export default new ProjectsStore();
|
||||
export default new ReposStore();
|
Loading…
Reference in New Issue