-
Notifications
You must be signed in to change notification settings - Fork 25
Syncs board after a period of inactivity #278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
f1cede4
dc2bce4
275b732
40c2205
1952937
9b0f528
51aba7a
f072790
04c9148
5829e45
224d420
251b281
cb862ac
5e9bbc1
0edf225
e43f3e1
8b69116
533e684
3ac1cad
87ca4b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,13 @@ | ||
| module Api | ||
| class ApiController < ApplicationController | ||
| skip_before_action :check_account | ||
|
|
||
| def logged_in | ||
| if logged_in? | ||
| return render json: {success: true} | ||
| else | ||
| return render json: {success: false, status: 403} | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |
|
|
||
|
|
||
| namespace :api do | ||
| get 'logged_in' => 'api#logged_in' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that a common convention (honest question) ? Easy change to make
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, it is for me. It goes back a pretty long ways to HTML webpage dev days - imagine http://www.bob.com/this_is_awesome where the default link underlining is in play vs
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this url is a query, for a pure RESTafarian- 💩 head - should it be |
||
| get 'uploads/asset' => 'uploads#asset_uploader' | ||
| #Webhooks | ||
| post '/site/webhook/issue' => 'webhooks#legacy' | ||
|
|
@@ -72,6 +73,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' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,25 @@ var ApplicationController = Ember.Controller.extend( | |
|
|
||
| //Fix the need to delay event subscriptions | ||
| subscribeDisabled: true, | ||
|
|
||
| //Browser Session Checker | ||
| boardSyncing: Ember.inject.service(), | ||
| checkBrowserSession: function(){ | ||
| var lastFocus = this.get('browser-session.lastFocus'); | ||
| var _self = this; | ||
| if(lastFocus >= _self.browserCheckInterval){ | ||
| var since = new Date(new Date().getTime() - _self.browserCheckInterval); | ||
| this.validateCredentials().success((response)=>{ | ||
| _self.get('boardSyncing').syncIssues(_self.get('model.board'), {since: since.toISOString()}); | ||
| }).fail((error)=>{ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than separately/explicitly check if the user is logged in here, it seems like we should just handle a 403 when we attempt to fetch latest. More generally, could we use $.ajaxSetup({
statusCode: {
403: function() {
alert( "Access denied! Do you need to [login|grant private access]?" );
},
500: function() {
alert( "Oh noes, something went wrong! Try again?" );
}
}
});(With better UX than alerts, of course.)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have a global ajax handler picking up failed requests and transition to a "unauthorized" modal. User login may be the wrong criteria, I honestly didn't think that part of it through too much. I would like to avoid using ajax catch-alls as much as possible, I see this feature as a way to "latch" on focus and fail before any unauthorized tasks are performed to begin with.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not sure what that actually gains us, though. Server-side, we're checking I'm curious what scenarios we have (or might have) in the app that would require different experiences when a 403 or 500 are encountered?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not entirely clear on the question. A session token can get stale, in which case all actions on the board will cause 403's. We handle this with the unauthorized modal right now, but it would be nice to let users know they need to "relogin" before they attempt to even do something that will fail
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. My question is: is there any situation in the app where we wouldn't want to handle a 403 with a unified UX to give a chance to restore authorization? |
||
|
|
||
| }); | ||
| } | ||
| }.observes('browser-session.lastFocus').on('init'), | ||
| validateCredentials: function(){ | ||
| return Ember.$.getJSON('/api/logged_in'); | ||
| }, | ||
| browserCheckInterval: 3600000//One Hour | ||
| }); | ||
|
|
||
| export default ApplicationController; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| 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 message = this.messageData().message; | ||
| var flash = this.get('flashMessages.queue').find((flash)=>{ | ||
| return flash.message === message; | ||
| }); | ||
| Ember.set(flash.progress, 'status', false); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any need to guard against |
||
| } | ||
| }.observes('syncInProgress'), | ||
| messageData: function(){ | ||
| return { | ||
| message: 'syncing your board, please wait...', | ||
| sticky: true, | ||
| type: 'info', | ||
| progress: { | ||
| status: true, | ||
| callback: function(){ | ||
| this.set('message', 'sync complete!'); | ||
| this.get('flash')._setTimer('timer', 'destroyMessage', 3000); | ||
| } | ||
| } | ||
| }; | ||
| }, | ||
|
|
||
| //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){ | ||
|
|
||
| } | ||
| }); | ||
|
|
||
| export default BoardSyncingService; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,12 @@ | ||
| {{#if flash.sticky}} | ||
| <i class='ui-icon ui-icon-x-thin'></i> | ||
| {{#if progress}} | ||
| {{hb-spinner}} | ||
| {{/if}} | ||
|
|
||
| {{#unless progress}} | ||
| {{#if flash.sticky}} | ||
| <i class='ui-icon ui-icon-x-thin'></i> | ||
| {{/if}} | ||
| {{/unless}} | ||
|
|
||
| <div class='message-copy'>{{truncate message 50}}</div> | ||
|
|
||
| {{#if progress}} | ||
| {{hb-spinner}} | ||
| {{/if}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); |


Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, this is just a discussion point. you request was technically successful 200 - you are returning the correct payload etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im rethinking this right now... maybe logged_in isn't the best option.