diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb
new file mode 100644
index 00000000..189eb15f
--- /dev/null
+++ b/app/controllers/api/projects_controller.rb
@@ -0,0 +1,39 @@
+module Api
+ class ProjectsController < ApiController
+
+ def index
+ path_prefix = "./repos/#{params[:user]}/#{params[:repo]}/projects"
+ projects = Project.new(gh.connection, path_prefix).all do |request|
+ request.headers["Accept"] = "application/vnd.github.inertia-preview.full+json"
+ end
+ render json: projects
+ end
+
+ def show
+ path_prefix = "./repos/#{params[:user]}/#{params[:repo]}/projects/#{params[:id]}"
+ project = Project.new(gh.connection, path_prefix).all do |request|
+ request.headers["Accept"] = "application/vnd.github.inertia-preview.full+json"
+ end
+
+ path_prefix = "./repos/#{params[:user]}/#{params[:repo]}/projects/#{params[:id]}/columns"
+ columns = Project.new(gh.connection, path_prefix).all do |request|
+ request.headers["Accept"] = "application/vnd.github.inertia-preview.full+json"
+ end
+
+ columns.each do |column|
+ path_prefix = "./repos/#{params[:user]}/#{params[:repo]}/projects/columns/#{column['id']}/cards"
+ column["cards"] = Project.new(gh.connection, path_prefix).all do |request|
+ request.headers["Accept"] = "application/vnd.github.inertia-preview.full+json"
+ end
+ end
+
+ render json: { project: project, columns: columns }
+ end
+
+ class Project < Ghee::ResourceProxy
+ accept_header "application/vnd.github.inertia-preview.full+json"
+ end
+
+ end
+end
+
diff --git a/config/routes.rb b/config/routes.rb
index 51117bbd..977b7217 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -66,6 +66,7 @@
resources :integrations, only: [:index, :create, :destroy]
resources :milestones, only: [:create, :update]
resources :links, only: [:index, :create]
+ resources :projects, only: [:index, :show]
delete 'links' => 'links#destroy'
post 'links/validate' => 'links#validate'
put 'links/update' => 'links#update'
diff --git a/ember-app/app/components/columns/hb-project.js b/ember-app/app/components/columns/hb-project.js
new file mode 100644
index 00000000..c55ef155
--- /dev/null
+++ b/ember-app/app/components/columns/hb-project.js
@@ -0,0 +1,69 @@
+import Ember from "ember";
+import HbColumn from "../columns/hb-column";
+import Messaging from 'app/mixins/messaging';
+
+var HbProjectComponent = HbColumn.extend(
+ Messaging, {
+ classNames: ["column"],
+ isTaskColumn: true,
+
+ //Scrolling Columns Tolerance
+ _toleranceDown: 59,
+ _toleranceUp: 70,
+
+ sortedIssues: function () {
+ var cards = this.get('model.cards');
+ var issues = this.get("issues")
+ .filter(function(issue){
+ return cards.find(function(c){
+ return Ember.get(c, 'content_url') == Ember.get(issue, 'url');
+ }) != null;
+ })
+ return issues || [];
+ }.property("issues.[]"),
+ sortStrategy: function(a,b){
+ },
+ moveIssue: function(issue, order, cancelMove){
+ // no-op
+ },
+ isCreateVisible: false,
+ topOrderNumber: function(){
+ var issues = this.get("issues")
+ .filter(function(i) { return !i.get("isArchived");})
+ .sort(this.sortStrategy);
+ var first = this.get("issues")
+ .filter(function(i) { return !i.get("isArchived");})
+ .sort(function (a, b){
+ return a.data._data.order - b.data._data.order;
+ }).get("firstObject");
+ if(issues.length){
+ var milestone_order = this.cardMover.moveToTop(issues.get("firstObject.data"));
+ var order = { milestone_order: milestone_order};
+ if(first){
+ order.order = this.cardMover.moveToTop(first.data, 'order');
+ }
+ return order;
+ } else {
+ if(first){
+ return { order: this.cardMover.moveToTop(first.data, 'order') };
+ }
+ return {};
+ }
+ }.property("sortedIssues.[]"),
+ isCollapsed: Ember.computed({
+ get: function(){
+ return this.get("settings.projectColumn" + this.get("model.number") + "Collapsed");
+ },
+ set: function(key, value){
+ this.set("settings.projectColumn" + this.get("model.number") + "Collapsed", value);
+ return value;
+ }
+ }).property(),
+ actions: {
+ toggleCollapsed(){
+ this.toggleProperty('isCollapsed');
+ }
+ }
+});
+
+export default HbProjectComponent;
diff --git a/ember-app/app/controllers/projects.js b/ember-app/app/controllers/projects.js
new file mode 100644
index 00000000..1b4abe2c
--- /dev/null
+++ b/ember-app/app/controllers/projects.js
@@ -0,0 +1,29 @@
+import Ember from 'ember';
+
+var ProjectsController = Ember.Controller.extend({
+ application: Ember.inject.controller(),
+
+ qps: Ember.inject.service("query-params"),
+ queryParams: [
+ {"qps.searchParams": "search"},
+ {"qps.repoParams": "repo"},
+ {"qps.assigneeParams": "assignee"},
+ {"qps.milestoneParams": "milestone"},
+ {"qps.labelParams": "label"},
+ {"qps.cardParams": "card"}
+ ],
+
+ filters: Ember.inject.service(),
+ filtersActive: Ember.computed.alias("filters.active"),
+
+ isCollaborator: function(){
+ return this.get("model.repo.isCollaborator");
+ }.property('model.repo.isCollaborator'),
+
+ isSidebarOpen: Ember.computed.alias("application.isSidebarOpen"),
+
+ actions: {
+ }
+});
+
+export default ProjectsController;
diff --git a/ember-app/app/controllers/projects/project.js b/ember-app/app/controllers/projects/project.js
new file mode 100644
index 00000000..17d7fe23
--- /dev/null
+++ b/ember-app/app/controllers/projects/project.js
@@ -0,0 +1,18 @@
+import Ember from 'ember';
+var ProjectController = Ember.Controller.extend({
+ application: Ember.inject.controller(),
+ registeredColumns: Ember.A(),
+ actions: {
+ registerColumn: function(column_component){
+ this.get("registeredColumns").pushObject(column_component);
+ },
+ unregisterColumn: function(column_component){
+ this.get("registeredColumns").removeObject(column_component);
+ },
+ openFullscreenIssue(issue){
+ this.get("target").send("openFullscreenIssue", issue);
+ }
+ }
+});
+
+export default ProjectController;
diff --git a/ember-app/app/models/new/board.js b/ember-app/app/models/new/board.js
index b08d4a26..1c2c89ab 100644
--- a/ember-app/app/models/new/board.js
+++ b/ember-app/app/models/new/board.js
@@ -133,6 +133,13 @@ var Board = Model.extend({
return Ember.RSVP.all(promises).then((issues)=>{
return _.flatten(issues);
});
+ },
+ fetchProjects: function(repo) {
+ var board = this;
+ return repo.fetchProjects().then(function(projects){
+ repo.set('projects', projects);
+ return board;
+ });
}
});
diff --git a/ember-app/app/models/new/project-column.js b/ember-app/app/models/new/project-column.js
new file mode 100644
index 00000000..f3cf781f
--- /dev/null
+++ b/ember-app/app/models/new/project-column.js
@@ -0,0 +1,32 @@
+import Ember from 'ember';
+import Model from '../model';
+
+var ProjectColumn = Model.extend({
+ issueNumberRegex: /\d+$/,
+ isLastColumn: Ember.computed('project.columns.[]', {
+ get() {
+ return this.get('project.columns.lastObject.data.id') === this.get('data.id');
+ }
+ }),
+ sortedIssues: function(){
+ var issues = this.get('project.repo.issues');
+ return this.get("cards").map((card)=>{
+ if(card.content_url){
+ var match = card.content_url.match(this.get('issueNumberRegex'));
+ if(match){
+ var issue = issues.findBy('number', parseInt(match[0]));
+ if(issue){
+ Ember.set(card, 'issue', issue);
+ } else {
+ Ember.set(card, 'note', `Issue #${match[0]} has been archived`);
+ }
+ }
+ }
+ return card;
+ }).filter((card) => {
+ return Ember.get(card, 'note') || !Ember.get(card, 'issue.isArchived');
+ });
+ }.property("data.cards.[]", 'project.repo.issues.[]'),
+});
+
+export default ProjectColumn;
diff --git a/ember-app/app/models/new/project.js b/ember-app/app/models/new/project.js
new file mode 100644
index 00000000..59f18ad8
--- /dev/null
+++ b/ember-app/app/models/new/project.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+import Model from '../model';
+import ProjectColumn from './project-column';
+
+var Project = Model.extend({
+ columns: Ember.computed('data.columns', {
+ get: function(){
+ var project = this;
+ return this.get('data.columns').map((c) =>{
+ return ProjectColumn.create({ data: c, project: this })
+ });
+ }
+ })
+});
+
+export default Project;
diff --git a/ember-app/app/models/new/repo.js b/ember-app/app/models/new/repo.js
index 857d9e9c..b1c4b18a 100644
--- a/ember-app/app/models/new/repo.js
+++ b/ember-app/app/models/new/repo.js
@@ -2,6 +2,7 @@ import Ember from 'ember';
import Model from '../model';
import Board from './board';
import Issue from './issue';
+import Project from './project';
import Milestone from './milestone';
import Integration from 'app/models/integration';
import Health from 'app/models/health';
@@ -230,6 +231,17 @@ var Repo = Model.extend({
fetchIssues: function(options){
var url = `/api/${this.get('data.repo.full_name')}/issues`;
return Ember.$.getJSON(url,{ options: options });
+ },
+ fetchProjects: function(){
+ var url = `/api/${this.get('data.repo.full_name')}/projects`;
+ return Ember.$.getJSON(url);
+ },
+ fetchProject: function(number){
+ var repo = this;
+ var url = `/api/${this.get('data.repo.full_name')}/projects/${number}`;
+ return Ember.$.getJSON(url).then((response) => {
+ return Project.create({data: response, repo: repo});
+ });
}
});
diff --git a/ember-app/app/router.js b/ember-app/app/router.js
index fcf73c3c..89ddc6ce 100644
--- a/ember-app/app/router.js
+++ b/ember-app/app/router.js
@@ -15,6 +15,12 @@ Router.map(function() {
});
this.route("milestones.missing");
+ this.resource("projects", function(){
+ this.resource("projects.project", {path:"/:project_id"}, function(){
+ this.resource("projects.project.issue", {path:"/issues/:issue_id"});
+ });
+ });
+
this.resource("settings", function(){
this.resource('settings.integrations', {path: '/integrations'}, function(){
diff --git a/ember-app/app/routes/projects.js b/ember-app/app/routes/projects.js
new file mode 100644
index 00000000..74dc3aa8
--- /dev/null
+++ b/ember-app/app/routes/projects.js
@@ -0,0 +1,44 @@
+import Board from 'app/models/new/board';
+import Ember from 'ember';
+import CreateIssue from 'app/models/forms/create-issue';
+import animateModalClose from 'app/config/animate-modal-close';
+
+var ProjectsRoute = Ember.Route.extend({
+ qps: Ember.inject.service("query-params"),
+
+ model: function(){
+ var repo = this.modelFor("application");
+ return Board.fetch(repo).then(function(board){
+ return board.fetchProjects(repo);
+ });
+ },
+ afterModel: function (model){
+ if (model.get("isLoaded")) {
+ return;
+ }
+ },
+ renderTemplate: function() {
+ this._super.apply(this, arguments);
+
+ var assignee = this.controllerFor("assignee");
+ assignee.set("model", this.currentModel);
+ this.render('assignee', {
+ into: 'projects',
+ outlet: 'sidebarTop',
+ controller: assignee
+ });
+
+ this.render('filters', {into: 'projects', outlet: 'sidebarMiddle'});
+ },
+ setupController: function(controller, model){
+ this._super(controller, model);
+ this.get("qps").applyFilterBuffer();
+ this.get("qps").applySearchBuffer();
+ },
+
+ actions : {
+ }
+});
+
+export default ProjectsRoute;
+
diff --git a/ember-app/app/routes/projects/project.js b/ember-app/app/routes/projects/project.js
new file mode 100644
index 00000000..d110fd23
--- /dev/null
+++ b/ember-app/app/routes/projects/project.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+
+var ProjectRoute = Ember.Route.extend({
+ model: function(params) {
+ var repo = this.modelFor("application");
+ var project = repo.get('projects').findBy('number', parseInt(params.project_id));
+ return repo.fetchProject(project.number);
+ },
+ actions: {
+ openFullscreenIssue: function(model) {
+ this.transitionTo("projects.project.issue", this.currentModel, model);
+ },
+ }
+});
+
+export default ProjectRoute;
diff --git a/ember-app/app/routes/projects/project/issue.js b/ember-app/app/routes/projects/project/issue.js
new file mode 100644
index 00000000..e00002dd
--- /dev/null
+++ b/ember-app/app/routes/projects/project/issue.js
@@ -0,0 +1,24 @@
+import Route from 'app/routes/issue';
+
+
+var ProjectIssueRoute = Route.extend({
+ model : function (params, transition){
+ // hacks!
+ var issue = this.modelFor("application")
+ .get("board.issues")
+ .findBy('id', parseInt(params.issue_id));
+ if(issue) { return issue; }
+
+ transition.abort();
+ this.transitionTo("projects.project", params.project_id);
+ },
+ actions: {
+ closeModal: function () {
+ this.transitionTo("projects.project");
+ return true;
+ }
+ }
+});
+
+export default ProjectIssueRoute;
+
diff --git a/ember-app/app/templates/application.hbs b/ember-app/app/templates/application.hbs
index 727a8fe5..3537e9c5 100644
--- a/ember-app/app/templates/application.hbs
+++ b/ember-app/app/templates/application.hbs
@@ -11,6 +11,11 @@
{{flash/hb-flash-message}}
+ {{#link-to 'projects' tagName="li" href=false classNames="hover-border-top"}}
+ {{#link-to 'projects' bubbles=false}}
+ Projects
+ {{/link-to}}
+ {{/link-to}}
{{#link-to 'milestones' tagName="li" href=false classNames="hover-border-top"}}
{{#link-to 'milestones' bubbles=false}}
Milestones
diff --git a/ember-app/app/templates/components/columns/hb-column.hbs b/ember-app/app/templates/components/columns/hb-column.hbs
index 520a225e..52f709fd 100644
--- a/ember-app/app/templates/components/columns/hb-column.hbs
+++ b/ember-app/app/templates/components/columns/hb-column.hbs
@@ -1,5 +1,4 @@
- {{columns/hb-task-header column=model issues=sortedIssues isCollapsed=isCollapsed}}
{{#if isCreateVisible}}
{{columns/hb-quick-issue
diff --git a/ember-app/app/templates/components/columns/hb-project.hbs b/ember-app/app/templates/components/columns/hb-project.hbs
new file mode 100644
index 00000000..0b516e63
--- /dev/null
+++ b/ember-app/app/templates/components/columns/hb-project.hbs
@@ -0,0 +1,38 @@
+
+
+
+
+ {{model.name}}
+
+
+ {{#if isCreateVisible}}
+ Create one
+ {{/if}}
+
+ {{#each visibleIssues as |issue|}}
+ {{#if issue.issue}}
+ {{
+ hb-task-card
+ issue=issue.issue
+ issues=model.project.repo.issues
+ repos=repos
+ isLastColumn=model.isLastColumn
+ cardClick=(action attrs.openFullscreenIssue)
+ }}
+ {{else}}
+
+ {{/if}}
+ {{/each}}
+
+
+
diff --git a/ember-app/app/templates/projects.hbs b/ember-app/app/templates/projects.hbs
new file mode 100644
index 00000000..8f164dee
--- /dev/null
+++ b/ember-app/app/templates/projects.hbs
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ {{#if filtersActive}}
+
+ {{/if}}
+
+ {{#if isCollaborator}}
+
+ {{#if model.isUnhealthy}}
+ {{#link-to 'settings.health' class='hb-icon-link' }}
+
+ {{/link-to}}
+ {{else}}
+ {{#link-to 'settings.index' class='hb-icon-link' }}
+
+ {{/link-to}}
+ {{/if}}
+
+ {{/if}}
+
+
+
+
+
+
+
diff --git a/ember-app/app/templates/projects/index.hbs b/ember-app/app/templates/projects/index.hbs
new file mode 100644
index 00000000..c4c902e0
--- /dev/null
+++ b/ember-app/app/templates/projects/index.hbs
@@ -0,0 +1,21 @@
+
+
+
+
Projects
+
+
+
+ {{#each model.repo.projects as |project|}}
+ -
+ {{#link-to 'projects.project' project.number}}
+ {{project.name}}
+ {{/link-to}}
+
+ {{/each}}
+
+
+
+
+
diff --git a/ember-app/app/templates/projects/project.hbs b/ember-app/app/templates/projects/project.hbs
new file mode 100644
index 00000000..1aa7d129
--- /dev/null
+++ b/ember-app/app/templates/projects/project.hbs
@@ -0,0 +1,10 @@
+{{#each model.columns as |column|}}
+ {{columns/hb-project
+ model=column
+ issues=column.sortedIssues
+ newSortable=true
+ registerColumn=(action "registerColumn")
+ unregisterColumn=(action "unregisterColumn")
+ openFullscreenIssue=(action "openFullscreenIssue")
+ }}
+{{/each}}