Commit 4cc7f716 authored by Robert Knight's avatar Robert Knight

Extract timestamp out into its own component

This makes it easier to test the refresh logic directly and decouples it
from the rest of the annotation display.
parent 14ee63db
...@@ -152,6 +152,7 @@ module.exports = angular.module('h', [ ...@@ -152,6 +152,7 @@ module.exports = angular.module('h', [
.directive('sortDropdown', require('./directive/sort-dropdown')) .directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('spinner', require('./directive/spinner')) .directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button')) .directive('statusButton', require('./directive/status-button'))
.directive('timestamp', require('./directive/timestamp'))
.directive('topBar', require('./directive/top-bar')) .directive('topBar', require('./directive/top-bar'))
.directive('windowScroll', require('./directive/window-scroll')) .directive('windowScroll', require('./directive/window-scroll'))
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
var angular = require('angular'); var angular = require('angular');
var annotationMetadata = require('../annotation-metadata'); var annotationMetadata = require('../annotation-metadata');
var dateUtil = require('../date-util');
var documentDomain = require('../filter/document-domain'); var documentDomain = require('../filter/document-domain');
var documentTitle = require('../filter/document-title'); var documentTitle = require('../filter/document-title');
var events = require('../events'); var events = require('../events');
...@@ -111,7 +110,7 @@ function updateDomainModel(domainModel, vm, permissions) { ...@@ -111,7 +110,7 @@ function updateDomainModel(domainModel, vm, permissions) {
} }
/** Update the view model from the domain model changes. */ /** Update the view model from the domain model changes. */
function updateViewModel($scope, time, domainModel, function updateViewModel($scope, domainModel,
vm, permissions) { vm, permissions) {
vm.form = { vm.form = {
...@@ -132,22 +131,6 @@ function updateViewModel($scope, time, domainModel, ...@@ -132,22 +131,6 @@ function updateViewModel($scope, time, domainModel,
vm.isPrivate = permissions.isPrivate( vm.isPrivate = permissions.isPrivate(
domainModel.permissions, domainModel.user); domainModel.permissions, domainModel.user);
function updateTimestamp() {
vm.relativeTimestamp = time.toFuzzyString(domainModel.updated);
vm.absoluteTimestamp = dateUtil.format(new Date(domainModel.updated));
}
if (domainModel.updated) {
if (vm.cancelTimestampRefresh) {
vm.cancelTimestampRefresh();
}
vm.cancelTimestampRefresh =
time.decayingInterval(domainModel.updated, function () {
$scope.$apply(updateTimestamp);
});
updateTimestamp();
}
var documentMetadata = extractDocumentMetadata(domainModel); var documentMetadata = extractDocumentMetadata(domainModel);
vm.documentTitle = documentTitle(documentMetadata); vm.documentTitle = documentTitle(documentMetadata);
vm.documentDomain = documentDomain(documentMetadata); vm.documentDomain = documentDomain(documentMetadata);
...@@ -175,7 +158,7 @@ function viewModelTagsFromDomainModelTags(domainModelTags) { ...@@ -175,7 +158,7 @@ function viewModelTagsFromDomainModelTags(domainModelTags) {
function AnnotationController( function AnnotationController(
$document, $q, $rootScope, $scope, $timeout, $window, annotationUI, $document, $q, $rootScope, $scope, $timeout, $window, annotationUI,
annotationMapper, drafts, flash, features, groups, permissions, session, annotationMapper, drafts, flash, features, groups, permissions, session,
settings, tags, time) { settings, tags) {
var vm = this; var vm = this;
var domainModel; var domainModel;
...@@ -213,21 +196,6 @@ function AnnotationController( ...@@ -213,21 +196,6 @@ function AnnotationController(
/** Whether or not this annotation is private. */ /** Whether or not this annotation is private. */
vm.isPrivate = false; vm.isPrivate = false;
/** A fuzzy, relative (eg. '6 days ago') format of the annotation's
* last update timestamp
*/
vm.relativeTimestamp = null;
/** A formatted version of the annotation's last update timestamp
* (eg. 'Tue 22nd Dec 2015, 16:00')
*/
vm.absoluteTimestamp = '';
/** A callback for resetting the automatic refresh of
* vm.relativeTimestamp and vm.absoluteTimestamp
*/
vm.cancelTimestampRefresh = undefined;
/** Determines whether controls to expand/collapse the annotation body /** Determines whether controls to expand/collapse the annotation body
* are displayed adjacent to the tags field. * are displayed adjacent to the tags field.
*/ */
...@@ -304,7 +272,7 @@ function AnnotationController( ...@@ -304,7 +272,7 @@ function AnnotationController(
} }
function updateView(domainModel) { function updateView(domainModel) {
updateViewModel($scope, time, domainModel, vm, permissions); updateViewModel($scope, domainModel, vm, permissions);
} }
function onAnnotationUpdated(event, updatedDomainModel) { function onAnnotationUpdated(event, updatedDomainModel) {
...@@ -330,10 +298,6 @@ function AnnotationController( ...@@ -330,10 +298,6 @@ function AnnotationController(
if (vm.editing()) { if (vm.editing()) {
saveToDrafts(drafts, domainModel, vm); saveToDrafts(drafts, domainModel, vm);
} }
if (vm.cancelTimestampRefresh) {
vm.cancelTimestampRefresh();
}
} }
function onGroupFocused() { function onGroupFocused() {
......
...@@ -144,7 +144,6 @@ describe('annotation', function() { ...@@ -144,7 +144,6 @@ describe('annotation', function() {
var fakeGroups; var fakeGroups;
var fakePermissions; var fakePermissions;
var fakeSession; var fakeSession;
var fakeTime;
var sandbox; var sandbox;
function createDirective(annotation) { function createDirective(annotation) {
...@@ -236,11 +235,6 @@ describe('annotation', function() { ...@@ -236,11 +235,6 @@ describe('annotation', function() {
store: sandbox.stub() store: sandbox.stub()
}; };
fakeTime = {
toFuzzyString: sandbox.stub().returns('a while ago'),
decayingInterval: function () {},
};
fakeGroups = { fakeGroups = {
focused: function() { focused: function() {
return {}; return {};
...@@ -257,7 +251,6 @@ describe('annotation', function() { ...@@ -257,7 +251,6 @@ describe('annotation', function() {
$provide.value('session', fakeSession); $provide.value('session', fakeSession);
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
$provide.value('tags', fakeTags); $provide.value('tags', fakeTags);
$provide.value('time', fakeTime);
$provide.value('groups', fakeGroups); $provide.value('groups', fakeGroups);
})); }));
...@@ -742,115 +735,6 @@ describe('annotation', function() { ...@@ -742,115 +735,6 @@ describe('annotation', function() {
}); });
}); });
describe('relativeTimestamp', function() {
var annotation;
var clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
annotation = fixtures.defaultAnnotation();
annotation.created = (new Date()).toString();
annotation.updated = (new Date()).toString();
});
afterEach(function() {
clock.restore();
});
it('is not updated for unsaved annotations', function() {
annotation.updated = null;
var controller = createDirective(annotation).controller;
// Unsaved annotations don't have an updated time yet so a timestamp
// string can't be computed for them.
$scope.$digest();
assert.equal(controller.relativeTimestamp, null);
});
it('is updated when a new annotation is saved', function () {
fakeTime.decayingInterval = function (date, callback) {
callback();
};
// fake clocks are not required for this test
clock.restore();
annotation.updated = null;
annotation.$create = function () {
annotation.updated = (new Date()).toString();
return Promise.resolve(annotation);
};
var controller = createDirective(annotation).controller;
controller.action = 'create';
controller.form.text = 'test';
return controller.save().then(function () {
assert.equal(controller.relativeTimestamp, 'a while ago');
});
});
it('is updated when a change to an existing annotation is saved',
function () {
fakeTime.toFuzzyString = function(date) {
var ONE_MINUTE = 60 * 1000;
if (Date.now() - new Date(date) < ONE_MINUTE) {
return 'just now';
} else {
return 'ages ago';
}
};
clock.tick(10 * 60 * 1000);
annotation.$update = function () {
this.updated = (new Date()).toString();
return Promise.resolve(this);
};
var controller = createDirective(annotation).controller;
assert.equal(controller.relativeTimestamp, 'ages ago');
controller.edit();
controller.form.text = 'test';
clock.restore();
return controller.save().then(function () {
assert.equal(controller.relativeTimestamp, 'just now');
});
});
it('is updated on first digest', function() {
var controller = createDirective(annotation).controller;
$scope.$digest();
assert.equal(controller.relativeTimestamp, 'a while ago');
});
it('is updated after a timeout', function() {
fakeTime.decayingInterval = function (date, callback) {
setTimeout(callback, 10);
};
var controller = createDirective(annotation).controller;
fakeTime.toFuzzyString.returns('ages ago');
$scope.$digest();
clock.tick(11000);
assert.equal(controller.relativeTimestamp, 'ages ago');
});
it('is no longer updated after the scope is destroyed', function() {
createDirective(annotation);
$scope.$digest();
$scope.$destroy();
$timeout.verifyNoPendingTasks();
});
});
describe('absoluteTimestamp', function () {
it('returns the current time', function () {
var annotation = fixtures.defaultAnnotation();
var controller = createDirective(annotation).controller;
var expectedDate = new Date(annotation.updated);
// the exact format of the result will depend on the current locale,
// but check that at least the current year and time are present
assert.match(controller.absoluteTimestamp, new RegExp('.*2015.*' +
expectedDate.toLocaleTimeString()));
});
});
describe('deleteAnnotation() method', function() { describe('deleteAnnotation() method', function() {
beforeEach(function() { beforeEach(function() {
fakeAnnotationMapper.deleteAnnotation = sandbox.stub(); fakeAnnotationMapper.deleteAnnotation = sandbox.stub();
......
'use strict';
var angular = require('angular');
var util = require('./util');
describe('timestamp', function () {
var clock;
var fakeTime;
before(function () {
angular.module('app',[])
.directive('timestamp', require('../timestamp'));
});
beforeEach(function () {
clock = sinon.useFakeTimers();
fakeTime = {
toFuzzyString: sinon.stub().returns('a while ago'),
decayingInterval: function () {},
};
angular.mock.module('app', {
time: fakeTime,
});
});
afterEach(function() {
clock.restore();
});
describe('#relativeTimestamp', function() {
it('displays a relative time string', function() {
var element = util.createDirective(document, 'timestamp', {
timestamp: '2016-06-10T10:04:04.939Z',
});
assert.equal(element.ctrl.relativeTimestamp, 'a while ago');
});
it('is updated when the timestamp changes', function () {
var element = util.createDirective(document, 'timestamp', {
timestamp: '1776-07-04T10:04:04.939Z',
});
element.scope.timestamp = '1863-11-19T12:00:00.939Z';
fakeTime.toFuzzyString.returns('four score and seven years ago');
element.scope.$digest();
assert.equal(element.ctrl.relativeTimestamp, 'four score and seven years ago');
});
it('is updated after time passes', function() {
fakeTime.decayingInterval = function (date, callback) {
setTimeout(callback, 10);
};
var element = util.createDirective(document, 'timestamp', {
timestamp: '2016-06-10T10:04:04.939Z',
});
fakeTime.toFuzzyString.returns('60 jiffies');
element.scope.$digest();
clock.tick(1000);
assert.equal(element.ctrl.relativeTimestamp, '60 jiffies');
});
it('is no longer updated after the component is destroyed', function() {
var cancelRefresh = sinon.stub();
fakeTime.decayingInterval = function () {
return cancelRefresh;
};
var element = util.createDirective(document, 'timestamp', {
timestamp: '2016-06-10T10:04:04.939Z',
});
element.ctrl.$onDestroy();
assert.called(cancelRefresh);
});
});
describe('#absoluteTimestamp', function () {
it('displays the current time', function () {
var expectedDate = new Date('2016-06-10T10:04:04.939Z');
var element = util.createDirective(document, 'timestamp', {
timestamp: expectedDate.toISOString(),
});
// The exact format of the result will depend on the current locale,
// but check that at least the current year and time are present
assert.match(element.ctrl.absoluteTimestamp, new RegExp('.*2016.*' +
expectedDate.toLocaleTimeString()));
});
});
});
...@@ -151,6 +151,12 @@ function createDirective(document, name, attrs, initialScope, initialHtml, opts) ...@@ -151,6 +151,12 @@ function createDirective(document, name, attrs, initialScope, initialHtml, opts)
element.scope = childScope; element.scope = childScope;
childScope.$digest(); childScope.$digest();
element.ctrl = element.controller(name); element.ctrl = element.controller(name);
if (!element.ctrl) {
throw new Error('Failed to create "' + name + '" directive in test.' +
'Did you forget to register it with angular.module(...).directive() ?');
}
return element; return element;
}; };
......
'use strict';
var dateUtil = require('../date-util');
// @ngInject
function TimestampController($scope, time) {
var vm = this;
// A fuzzy, relative (eg. '6 days ago') format of the timestamp.
vm.relativeTimestamp = null;
// A formatted version of the timestamp (eg. 'Tue 22nd Dec 2015, 16:00')
vm.absoluteTimestamp = '';
var cancelTimestampRefresh;
function updateTimestamp() {
vm.relativeTimestamp = time.toFuzzyString(vm.timestamp);
vm.absoluteTimestamp = dateUtil.format(new Date(vm.timestamp));
if (vm.timestamp) {
if (cancelTimestampRefresh) {
cancelTimestampRefresh();
}
cancelTimestampRefresh = time.decayingInterval(vm.timestamp, function () {
updateTimestamp();
$scope.$digest();
});
}
}
this.$onChanges = function (changes) {
if (changes.timestamp) {
updateTimestamp();
}
};
this.$onDestroy = function () {
if (cancelTimestampRefresh) {
cancelTimestampRefresh();
}
};
}
module.exports = function () {
return {
bindToController: true,
controller: TimestampController,
controllerAs: 'vm',
restrict: 'E',
scope: {
className: '<',
href: '<',
timestamp: '<',
},
template: ['<a class="{{vm.className}}" target="_blank" ng-title="vm.absoluteTimestamp"',
' href="{{vm.href}}"',
'>{{vm.relativeTimestamp}}</a>'].join(''),
};
};
...@@ -40,13 +40,11 @@ ...@@ -40,13 +40,11 @@
<span class="u-flex-spacer"></span> <span class="u-flex-spacer"></span>
<!-- Timestamp --> <timestamp
<a class="annotation-header__timestamp" class-name="'annotation-header__timestamp'"
target="_blank" timestamp="vm.updated()"
title="{{vm.absoluteTimestamp}}" href="vm.linkHTML"
ng-if="!vm.editing() && vm.updated()" ng-if="!vm.editing() && vm.updated()"></timestamp>
ng-href="{{vm.linkHTML}}"
>{{vm.relativeTimestamp}}</a>
</header> </header>
<!-- Excerpts --> <!-- Excerpts -->
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment