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', [
.directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button'))
.directive('timestamp', require('./directive/timestamp'))
.directive('topBar', require('./directive/top-bar'))
.directive('windowScroll', require('./directive/window-scroll'))
......
......@@ -4,7 +4,6 @@
var angular = require('angular');
var annotationMetadata = require('../annotation-metadata');
var dateUtil = require('../date-util');
var documentDomain = require('../filter/document-domain');
var documentTitle = require('../filter/document-title');
var events = require('../events');
......@@ -111,7 +110,7 @@ function updateDomainModel(domainModel, vm, permissions) {
}
/** Update the view model from the domain model changes. */
function updateViewModel($scope, time, domainModel,
function updateViewModel($scope, domainModel,
vm, permissions) {
vm.form = {
......@@ -132,22 +131,6 @@ function updateViewModel($scope, time, domainModel,
vm.isPrivate = permissions.isPrivate(
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);
vm.documentTitle = documentTitle(documentMetadata);
vm.documentDomain = documentDomain(documentMetadata);
......@@ -175,7 +158,7 @@ function viewModelTagsFromDomainModelTags(domainModelTags) {
function AnnotationController(
$document, $q, $rootScope, $scope, $timeout, $window, annotationUI,
annotationMapper, drafts, flash, features, groups, permissions, session,
settings, tags, time) {
settings, tags) {
var vm = this;
var domainModel;
......@@ -213,21 +196,6 @@ function AnnotationController(
/** Whether or not this annotation is private. */
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
* are displayed adjacent to the tags field.
*/
......@@ -304,7 +272,7 @@ function AnnotationController(
}
function updateView(domainModel) {
updateViewModel($scope, time, domainModel, vm, permissions);
updateViewModel($scope, domainModel, vm, permissions);
}
function onAnnotationUpdated(event, updatedDomainModel) {
......@@ -330,10 +298,6 @@ function AnnotationController(
if (vm.editing()) {
saveToDrafts(drafts, domainModel, vm);
}
if (vm.cancelTimestampRefresh) {
vm.cancelTimestampRefresh();
}
}
function onGroupFocused() {
......
......@@ -144,7 +144,6 @@ describe('annotation', function() {
var fakeGroups;
var fakePermissions;
var fakeSession;
var fakeTime;
var sandbox;
function createDirective(annotation) {
......@@ -236,11 +235,6 @@ describe('annotation', function() {
store: sandbox.stub()
};
fakeTime = {
toFuzzyString: sandbox.stub().returns('a while ago'),
decayingInterval: function () {},
};
fakeGroups = {
focused: function() {
return {};
......@@ -257,7 +251,6 @@ describe('annotation', function() {
$provide.value('session', fakeSession);
$provide.value('settings', fakeSettings);
$provide.value('tags', fakeTags);
$provide.value('time', fakeTime);
$provide.value('groups', fakeGroups);
}));
......@@ -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() {
beforeEach(function() {
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)
element.scope = childScope;
childScope.$digest();
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;
};
......
'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 @@
<span class="u-flex-spacer"></span>
<!-- Timestamp -->
<a class="annotation-header__timestamp"
target="_blank"
title="{{vm.absoluteTimestamp}}"
ng-if="!vm.editing() && vm.updated()"
ng-href="{{vm.linkHTML}}"
>{{vm.relativeTimestamp}}</a>
<timestamp
class-name="'annotation-header__timestamp'"
timestamp="vm.updated()"
href="vm.linkHTML"
ng-if="!vm.editing() && vm.updated()"></timestamp>
</header>
<!-- 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