diff --git a/app/assets/stylesheets/_colors.scss b/app/assets/stylesheets/_colors.scss index 2e897247..89f4bf7a 100644 --- a/app/assets/stylesheets/_colors.scss +++ b/app/assets/stylesheets/_colors.scss @@ -2,6 +2,7 @@ $lighterGrey: #CCC; $lightGrey: #999; $grey: #666; $darkGrey: #444; +$navGrey: #F3F3F3; $red: red; diff --git a/app/assets/stylesheets/components/_flash-message.scss b/app/assets/stylesheets/components/_flash-message.scss index d4f7123a..f38d59d8 100644 --- a/app/assets/stylesheets/components/_flash-message.scss +++ b/app/assets/stylesheets/components/_flash-message.scss @@ -33,6 +33,12 @@ background: $hb-red; } + .progress.message { + background: $navGrey; + box-shadow: -1px 2px 2px rgba($navGrey, 0.5); + color: $hb-purple-dark; + } + .hb-spinner { position: relative; float: right; diff --git a/app/controllers/api/issues_controller.rb b/app/controllers/api/issues_controller.rb index 3de72da0..6ec89788 100644 --- a/app/controllers/api/issues_controller.rb +++ b/app/controllers/api/issues_controller.rb @@ -6,6 +6,11 @@ def issue render json: api.issue(params[:number]) end + def issues + api = huboard.board(params[:user], params[:repo]) + render json: api.issues(params[:label], params[:options]) + end + def details api = huboard.board(params[:user], params[:repo]) render json: api.issue(params[:number]).activities diff --git a/config/routes.rb b/config/routes.rb index b0438f61..a307891f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,6 +72,7 @@ #Issues get 'issues/:number' => 'issues#issue' + get 'issues' => 'issues#issues' get 'issues/:number/details' => 'issues#details' get 'issues/:number/status' => 'issues#status' post 'issues' => 'issues#open_issue' diff --git a/ember-app/app/controllers/application.js b/ember-app/app/controllers/application.js index 76089b50..33c410b2 100644 --- a/ember-app/app/controllers/application.js +++ b/ember-app/app/controllers/application.js @@ -1,6 +1,7 @@ import Ember from 'ember'; import BoardSubscriptions from "app/mixins/subscriptions/board"; import Messaging from "app/mixins/messaging"; +import { throttledObserver } from 'app/utilities/observers'; var ApplicationController = Ember.Controller.extend( BoardSubscriptions, Messaging, { @@ -24,6 +25,21 @@ var ApplicationController = Ember.Controller.extend( //Fix the need to delay event subscriptions subscribeDisabled: true, + + //Browser Session Checker + boardSyncing: Ember.inject.service(), + checkBrowserSession: throttledObserver(function(){ + var lastFocus = this.get('browser-session.lastFocus'); + var _self = this; + if(lastFocus >= 30000){ + var since = new Date(new Date().getTime() - lastFocus); + return this.get('boardSyncing').syncIssues(this.get('model.board'), {since: since.toISOString()}); + } + + if(lastFocus >= 8.64e+7){ //One Day + this.send('sessionErrorHandler'); + } + },'browser-session.lastFocus', 30000).on('init') }); export default ApplicationController; diff --git a/ember-app/app/initializers/browser-session.js b/ember-app/app/initializers/browser-session.js new file mode 100644 index 00000000..f79daa0d --- /dev/null +++ b/ember-app/app/initializers/browser-session.js @@ -0,0 +1,14 @@ +import BrowserSession from 'app/services/browser-session'; + +export function initialize(container, application){ + application.register('browser-session:main', BrowserSession); + application.inject('controller', 'browser-session', 'browser-session:main'); + application.inject('component', 'browser-session', 'browser-session:main'); +} + +export default { + name: 'browser-session', + after: 'advanceReadiness', + initialize: initialize +} + diff --git a/ember-app/app/models/model.js b/ember-app/app/models/model.js index 83e7d245..b5fe52af 100644 --- a/ember-app/app/models/model.js +++ b/ember-app/app/models/model.js @@ -10,6 +10,9 @@ var HuBoardModel = Ember.Object.extend( _onInit: function(){ this.set('content', this.get('data')); }.on('init'), + onDataChanged: function(){ + this.set('content', this.get('data')); + }.observes('data'), ajax: ajax }); diff --git a/ember-app/app/models/new/board.js b/ember-app/app/models/new/board.js index 52ab39d0..249022d3 100644 --- a/ember-app/app/models/new/board.js +++ b/ember-app/app/models/new/board.js @@ -112,7 +112,16 @@ var Board = Model.extend({ return issue.data.assignee && issue.data.assignee.login === assignee.login; }); }); - }).property("assignees.[]", "issues.@each.assignee") + }).property("assignees.[]", "issues.@each.assignee"), + fetchIssues: function(options){ + var promises = this.get('repos').map((repo)=>{ + return repo.fetchIssues(options); + }); + + return Ember.RSVP.all(promises).then((issues)=>{ + return _.flatten(issues); + }); + } }); Board.reopenClass({ diff --git a/ember-app/app/models/new/repo.js b/ember-app/app/models/new/repo.js index 793aaaee..eb03e097 100644 --- a/ember-app/app/models/new/repo.js +++ b/ember-app/app/models/new/repo.js @@ -222,7 +222,11 @@ var Repo = Model.extend({ }, assigneesLength: function(){ return this.get("assignees.length"); - }.property("assignees.[]") + }.property("assignees.[]"), + fetchIssues: function(options){ + var url = `/api/${this.get('data.repo.full_name')}/issues` + return Ember.$.getJSON(url,{ options: options }); + } }); export default Repo; diff --git a/ember-app/app/router.js b/ember-app/app/router.js index f5d10652..985fe077 100644 --- a/ember-app/app/router.js +++ b/ember-app/app/router.js @@ -30,6 +30,7 @@ Router.map(function() { }); + this.resource("sync-issues"); this.route("unauthorized"); }); diff --git a/ember-app/app/routes/sync-issues.js b/ember-app/app/routes/sync-issues.js new file mode 100644 index 00000000..3ad221ab --- /dev/null +++ b/ember-app/app/routes/sync-issues.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; + +var SyncIssuesRoute = Ember.Route.extend({ + boardSyncing: Ember.inject.service(), + + model: function(){ + var repo = this.modelFor("application"); + return repo; + }, + + afterModel: function (model){ + var since = new Date(new Date().getTime() - 3600000); + this.get('boardSyncing').syncIssues(model.get('board'), {since: since.toISOString()}); + this.transitionTo('application'); + } +}); + +export default SyncIssuesRoute; diff --git a/ember-app/app/services/board-syncing.js b/ember-app/app/services/board-syncing.js new file mode 100644 index 00000000..e067f77c --- /dev/null +++ b/ember-app/app/services/board-syncing.js @@ -0,0 +1,77 @@ +import Ember from 'ember'; + +var BoardSyncingService = Ember.Service.extend({ + + //Sync Notifier + flashMessages: Ember.inject.service(), + syncFlashNotifier: function(){ + if(this.get('syncInProgress')){ + this.get('flashMessages').add(this.messageData()); + } else { + var flash = this.get('flashMessages.queue').find((flash)=>{ + return flash.identifier === 'sync-message'; + }); + if(!flash){ return; } + Ember.set(flash.progress, 'status', false); + } + }.observes('syncInProgress'), + messageData: function(){ + return { + message: 'syncing your board, please wait...', + sticky: true, + type: 'progress', + identifier: 'sync-message', + progress: { + status: true, + callback: function(){ + setTimeout(()=>{ + this.set('message', 'sync complete!'); + this.get('flash')._setTimer('timer', 'destroyMessage', 2000); + }, 1000); + } + } + }; + }, + + //Issue Syncing + syncIssues: function(board, opts){ + if(this.get('syncInProgress')){ return; } + var _self = this; + _self.set('syncInProgress', true); + + board.fetchIssues(opts).then((issues)=>{ + _self.issueSuccess(board, issues); + _self.set('syncInProgress', false); + }, (error)=>{ + _self.issueFail(); + _self.set('syncInProgress', false); + }); + }, + issueSuccess: function(board, issues){ + if(!issues.length){ return; } + Ember.run.once(()=>{ + board.get('issues').forEach((issue)=>{ + issues.forEach((i)=>{ + if(i.id === issue.get('id')){ + Ember.set(issue, 'data', i); + }; + }); + }); + }); + }, + issueFail: function(error){ + var flash = this.get('flashMessages.queue').find((flash)=>{ + return flash.identifier === 'sync-message'; + }); + if(!flash){ return; } + flash.progress.callback = function(){ + setTimeout(()=>{ + this.set('flash.type', 'warning'); + this.set('message', 'unable to sync your board, try refreshing'); + this.set('flash.sticky', true); + }, 1000); + }; + } +}); + +export default BoardSyncingService; diff --git a/ember-app/app/services/browser-session.js b/ember-app/app/services/browser-session.js new file mode 100644 index 00000000..58a83f26 --- /dev/null +++ b/ember-app/app/services/browser-session.js @@ -0,0 +1,31 @@ +import Ember from 'ember'; + +var BrowserSessionService = Ember.Service.extend(Ember.Evented, { + initEventObservers: function(){ + var _self = this; + Ember.$(window).on('focus blur', (e)=>{ + _self[`${e.type}Handlers`].forEach((h) => _self[h]()); + }); + }.on('init'), + setLastFocus: function(){ + var before = this.get('lastBlur'); + var now = new Date().getTime(); + this.set('lastFocus', (now - before)); + }.on('didFocusBrowser'), + + //Focus Handlers + focusHandlers: ['sendFocusEvent'], + sendFocusEvent: function(){ + this.trigger('didFocusBrowser'); + }, + + //Blur Handlers + blurHandlers: ['updateLastBlur'], + lastBlur: new Date().getTime(), + updateLastBlur: function(){ + var time = new Date().getTime(); + this.set('lastBlur', time); + } +}); + +export default BrowserSessionService; diff --git a/ember-app/app/templates/components/flash/hb-message.hbs b/ember-app/app/templates/components/flash/hb-message.hbs index 816172e1..19b3b670 100644 --- a/ember-app/app/templates/components/flash/hb-message.hbs +++ b/ember-app/app/templates/components/flash/hb-message.hbs @@ -1,9 +1,12 @@ -{{#if flash.sticky}} - +{{#if progress}} + {{hb-spinner}} {{/if}} +{{#unless progress}} + {{#if flash.sticky}} + + {{/if}} +{{/unless}} +
{{truncate message 50}}
-{{#if progress}} - {{hb-spinner}} -{{/if}} diff --git a/ember-app/tests/unit/services/board-syncing-test.js b/ember-app/tests/unit/services/board-syncing-test.js new file mode 100644 index 00000000..9585a807 --- /dev/null +++ b/ember-app/tests/unit/services/board-syncing-test.js @@ -0,0 +1,64 @@ +import Ember from 'ember'; +import { + moduleFor, + test +} from 'ember-qunit'; + +var sut; +var fakeServer; +var fakeResponse; +moduleFor('service:board-syncing', { + setup: function(){ + sut = this.subject(); + } +}); + +test('syncs the boards issues successfuly', (assert)=>{ + var issues = ['issue1', 'issue2']; + var success = $.ajax().then(()=>{return issues}); + var board = { fetchIssues: sinon.stub().returns(success) }; + sut.syncFlashNotifier = sinon.stub(); + sut.issueSuccess = sinon.stub(); + + var done = assert.async(); + sut.syncIssues(board, {}); + setTimeout(()=>{ + assert.ok(board.fetchIssues.calledWith({})); + assert.ok(sut.issueSuccess.calledWith(board, issues)); + assert.ok(sut.get('syncInProgress') === false); + done(); + }, 10); +}); + +test('fails gracefully on syncing the boards issues', (assert)=>{ + var fail = $.ajax('fail'); + var board = { fetchIssues: sinon.stub().returns(fail) }; + sut.syncFlashNotifier = sinon.stub(); + sut.issueFail = sinon.stub(); + + var done = assert.async(); + sut.syncIssues(board, {}); + setTimeout(()=>{ + assert.ok(board.fetchIssues.calledWith({})); + assert.ok(sut.issueFail.called); + assert.ok(sut.get('syncInProgress') === false); + done(); + }, 10); +}); + +test('sends a flash notifier on sync', (assert)=> { + var flash = { add: sinon.stub() }; + sut.messageData = sinon.stub(); + sut.set('flashMessages', flash); + sut.set('syncInProgress', true); + + assert.ok(sut.get('flashMessages').add.calledWith(sut.messageData())); +}); + +test('clears flash when sync is finished', (assert)=> { + var flash = { queue: [sut.messageData()] }; + sut.set('flashMessages', flash); + sut.set('syncInProgress', false); + + assert.ok(flash.queue[0].progress.status === false); +}); diff --git a/ember-app/tests/unit/services/browser-session-test.js b/ember-app/tests/unit/services/browser-session-test.js new file mode 100644 index 00000000..f9e8eae6 --- /dev/null +++ b/ember-app/tests/unit/services/browser-session-test.js @@ -0,0 +1,65 @@ +import Ember from 'ember'; +import { + moduleFor, + test +} from 'ember-qunit'; + +var sut; +moduleFor('service:browser-session', { + setup: function(){ + sut = this.subject(); + sut.focusHandlers = ['focusHandler1', 'focusHandler2']; + sut.get('focusHandlers').forEach((handler) => { + sut[handler] = sinon.stub(); + }); + + sut.blurHandlers = ['blurHandler1', 'blurHandler2']; + sut.get('blurHandlers').forEach((handler) => { + sut[handler] = sinon.stub(); + }); + } +}); + +test('Runs Blur Handlers on window.blur', (assert)=>{ + $(window).trigger('blur'); + + assert.ok(sut.blurHandler1.called); + assert.ok(sut.blurHandler2.called); +}); + +test('sets last focus interval', (assert)=>{ + var done = assert.async(); + var interval; + var interval2; + + sut.trigger('didFocusBrowser'); + interval = sut.get('lastFocus'); + + setTimeout(()=>{ + sut.trigger('didFocusBrowser'); + interval2 = sut.get('lastFocus'); + assert.ok(interval < interval2); + done(); + }, 10); +}); + +//Focus Handlers +test('Send ember observable didFocusBrowser event', (assert)=>{ + sinon.stub(sut, 'trigger'); + + sut.sendFocusEvent(); + assert.ok(sut.trigger.calledWith('didFocusBrowser')); +}); + +//Blur Handlers +test('Set lastBlur with time of last blur', (assert)=>{ + var done = assert.async(); + var current = sut.get('lastBlur'); + + setTimeout(()=>{ + sut.updateLastBlur(); + var updated = sut.get('lastBlur'); + assert.ok((updated - current) >= 100); + done(); + }, 100); +}); diff --git a/lib/bridge/github/issues.rb b/lib/bridge/github/issues.rb index 4893517a..02452c08 100644 --- a/lib/bridge/github/issues.rb +++ b/lib/bridge/github/issues.rb @@ -10,8 +10,8 @@ def overridable(&blk) class Huboard module Issues - def issues(label = nil) - params = {direction: "asc"} + def issues(label = nil, opts={}) + params = {direction: "asc"}.merge(opts) params = params.merge(labels: label) if label gh.issues(params).all.each{