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', [
.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() {
......
......@@ -3,6 +3,7 @@
// @ngInject
module.exports = function () {
return {
controller: function () {},
restrict: 'E',
scope: {
filterActive: '<',
......
......@@ -2,6 +2,7 @@
module.exports = function () {
return {
controller: function () {},
restrict: 'E',
scope: {
/** The name of the currently selected sort key. */
......@@ -12,5 +13,5 @@ module.exports = function () {
onChangeSortKey: '&',
},
template: require('../../../templates/client/sort_dropdown.html'),
}
};
};
......@@ -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();
......
......@@ -6,6 +6,7 @@ var util = require('./util');
function testComponent() {
return {
controller: function () {},
restrict: 'E',
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)
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