Commit 76706c5e authored by Nick Stenning's avatar Nick Stenning Committed by GitHub

Merge pull request #3444 from hypothesis/timestamp-component

Extract timestamp out into its own component
parents 14ee63db 0052c0b4
...@@ -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() {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// @ngInject // @ngInject
module.exports = function () { module.exports = function () {
return { return {
controller: function () {},
restrict: 'E', restrict: 'E',
scope: { scope: {
filterActive: '<', filterActive: '<',
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
module.exports = function () { module.exports = function () {
return { return {
controller: function () {},
restrict: 'E', restrict: 'E',
scope: { scope: {
/** The name of the currently selected sort key. */ /** The name of the currently selected sort key. */
...@@ -12,5 +13,5 @@ module.exports = function () { ...@@ -12,5 +13,5 @@ module.exports = function () {
onChangeSortKey: '&', onChangeSortKey: '&',
}, },
template: require('../../../templates/client/sort_dropdown.html'), template: require('../../../templates/client/sort_dropdown.html'),
} };
}; };
...@@ -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();
......
...@@ -6,6 +6,7 @@ var util = require('./util'); ...@@ -6,6 +6,7 @@ var util = require('./util');
function testComponent() { function testComponent() {
return { return {
controller: function () {},
restrict: 'E', restrict: 'E',
template: '<div aria-label="Share" h-tooltip>Label</div>', template: '<div aria-label="Share" h-tooltip>Label</div>',
}; };
......
'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