Commit 41904b1b authored by Sean Hammond's avatar Sean Hammond Committed by GitHub

Merge pull request #8 from hypothesis/simplify-annot-cmp

Simplify annotation component by removing duplicated state
parents 9d6f56bc 8db47b83
/* jshint node: true */ /* jshint node: true */
'use strict'; 'use strict';
var angular = require('angular');
var annotationMetadata = require('../annotation-metadata'); var annotationMetadata = require('../annotation-metadata');
var events = require('../events'); var events = require('../events');
var memoize = require('../util/memoize');
var persona = require('../filter/persona'); var persona = require('../filter/persona');
var isNew = annotationMetadata.isNew; var isNew = annotationMetadata.isNew;
...@@ -31,95 +30,22 @@ function errorMessage(reason) { ...@@ -31,95 +30,22 @@ function errorMessage(reason) {
return message; return message;
} }
/** Restore unsaved changes to this annotation from the drafts service.
*
* If there are no draft changes to this annotation, does nothing.
*
*/
function restoreFromDrafts(drafts, domainModel, vm) {
var draft = drafts.get(domainModel);
if (draft) {
vm.isPrivate = draft.isPrivate;
vm.form.tags = draft.tags;
vm.form.text = draft.text;
}
}
/** /**
* Save the given annotation to the drafts service. * Return a copy of `annotation` with changes made in the editor applied.
*
* Any existing drafts for this annotation will be overwritten.
*
* @param {object} drafts - The drafts service
* @param {object} domainModel - The full domainModel object of the
* annotation to be saved. This full domainModel model is not retrieved
* again from drafts, it's only used to identify the annotation's draft in
* order to retrieve the fields below.
* @param {object} vm - The view model object containing the user's unsaved
* changes to the annotation.
*
*/ */
function saveToDrafts(drafts, domainModel, vm) { function updateModel(annotation, changes, permissions) {
drafts.update( return Object.assign({}, annotation, {
domainModel, // Explicitly copy across the non-enumerable local tag for the annotation
{ $$tag: annotation.$$tag,
isPrivate: vm.isPrivate,
tags: vm.form.tags, // Apply changes from the draft
text: vm.form.text, tags: changes.tags,
text: changes.text,
permissions: changes.isPrivate ?
permissions.private() : permissions.shared(annotation.group),
}); });
} }
/** Update domainModel from vm.
*
* Copy any properties from vm that might have been modified by the user into
* domainModel, overwriting any existing properties in domainModel.
*
* @param {object} domainModel The object to copy properties to
* @param {object} vm The object to copy properties from
*
*/
function updateDomainModel(domainModel, vm, permissions) {
domainModel.text = vm.form.text;
domainModel.tags = vm.form.tags;
if (vm.isPrivate) {
domainModel.permissions = permissions.private();
} else {
domainModel.permissions = permissions.shared(domainModel.group);
}
}
/** Update the view model from the domain model changes. */
function updateViewModel($scope, domainModel,
vm, permissions) {
vm.form = {
text: domainModel.text,
tags: domainModel.tags,
};
if (domainModel.links) {
vm.linkInContext = domainModel.links.incontext ||
domainModel.links.html ||
'';
vm.linkHTML = domainModel.links.html || '';
} else {
vm.linkInContext = '';
vm.linkHTML = '';
}
vm.isPrivate = permissions.isPrivate(
domainModel.permissions, domainModel.user);
vm.documentMeta = annotationMetadata.domainAndTitle(domainModel);
}
/**
* @ngdoc type
* @name annotation.AnnotationController
*
*/
// @ngInject // @ngInject
function AnnotationController( function AnnotationController(
$document, $q, $rootScope, $scope, $timeout, $window, annotationUI, $document, $q, $rootScope, $scope, $timeout, $window, annotationUI,
...@@ -127,7 +53,6 @@ function AnnotationController( ...@@ -127,7 +53,6 @@ function AnnotationController(
settings, store) { settings, store) {
var vm = this; var vm = this;
var domainModel;
var newlyCreatedByHighlightButton; var newlyCreatedByHighlightButton;
/** Save an annotation to the server. */ /** Save an annotation to the server. */
...@@ -164,13 +89,6 @@ function AnnotationController( ...@@ -164,13 +89,6 @@ function AnnotationController(
* can call the methods. * can call the methods.
*/ */
function init() { function init() {
/** The currently active action - 'view', 'create' or 'edit'. */
vm.action = 'view';
/** vm.form is the read-write part of vm for the templates: it contains
* the variables that the templates will write changes to via ng-model. */
vm.form = {};
// The remaining properties on vm are read-only properties for the // The remaining properties on vm are read-only properties for the
// templates. // templates.
...@@ -180,9 +98,6 @@ function AnnotationController( ...@@ -180,9 +98,6 @@ function AnnotationController(
/** Give the template access to the feature flags. */ /** Give the template access to the feature flags. */
vm.feature = features.flagEnabled; vm.feature = features.flagEnabled;
/** Whether or not this annotation is private. */
vm.isPrivate = false;
/** 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.
*/ */
...@@ -197,13 +112,6 @@ function AnnotationController( ...@@ -197,13 +112,6 @@ function AnnotationController(
/** True if the 'Share' dialog for this annotation is currently open. */ /** True if the 'Share' dialog for this annotation is currently open. */
vm.showShareDialog = false; vm.showShareDialog = false;
/** The domain model, contains the currently saved version of the
* annotation from the server (or in the case of new annotations that
* haven't been saved yet - the data that will be saved to the server when
* they are saved).
*/
domainModel = vm.annotation;
/** /**
* `true` if this AnnotationController instance was created as a result of * `true` if this AnnotationController instance was created as a result of
* the highlight button being clicked. * the highlight button being clicked.
...@@ -212,35 +120,34 @@ function AnnotationController( ...@@ -212,35 +120,34 @@ function AnnotationController(
* or annotation that was fetched from the server (as opposed to created * or annotation that was fetched from the server (as opposed to created
* new client-side). * new client-side).
*/ */
newlyCreatedByHighlightButton = domainModel.$highlight || false; newlyCreatedByHighlightButton = vm.annotation.$highlight || false;
// Call `onAnnotationUpdated()` whenever the "annotationUpdated" event is
// emitted. This event is emitted after changes to the annotation are
// successfully saved to the server, and also when changes to the
// annotation made by another client are received by this client from the
// server.
$rootScope.$on(events.ANNOTATION_UPDATED, onAnnotationUpdated);
// When a new annotation is created, remove any existing annotations that // When a new annotation is created, remove any existing annotations that
// are empty // are empty.
//
// This event is currently emitted with $emit rather than $broadcast so
// we have to listen for it on the $rootScope and manually de-register
// on destruction.
var removeNewAnnotListener =
$rootScope.$on(events.BEFORE_ANNOTATION_CREATED, deleteIfNewAndEmpty); $rootScope.$on(events.BEFORE_ANNOTATION_CREATED, deleteIfNewAndEmpty);
// Call `onDestroy()` when the component is destroyed. vm.$onDestroy = function () {
$scope.$on('$destroy', onDestroy); removeNewAnnotListener();
};
// Call `onGroupFocused()` whenever the currently-focused group changes. // Call `onGroupFocused()` whenever the currently-focused group changes.
$scope.$on(events.GROUP_FOCUSED, onGroupFocused); $scope.$on(events.GROUP_FOCUSED, onGroupFocused);
// New annotations (just created locally by the client, rather then // New annotations (just created locally by the client, rather then
// received from the server) have some fields missing. Add them. // received from the server) have some fields missing. Add them.
domainModel.user = domainModel.user || session.state.userid; vm.annotation.user = vm.annotation.user || session.state.userid;
domainModel.group = domainModel.group || groups.focused().id; vm.annotation.group = vm.annotation.group || groups.focused().id;
if (!domainModel.permissions) { if (!vm.annotation.permissions) {
domainModel.permissions = permissions['default'](domainModel.group); vm.annotation.permissions = permissions['default'](vm.annotation.group);
} }
domainModel.text = domainModel.text || ''; vm.annotation.text = vm.annotation.text || '';
if (!Array.isArray(domainModel.tags)) { if (!Array.isArray(vm.annotation.tags)) {
domainModel.tags = []; vm.annotation.tags = [];
} }
// Automatically save new highlights to the server when they're created. // Automatically save new highlights to the server when they're created.
...@@ -250,51 +157,26 @@ function AnnotationController( ...@@ -250,51 +157,26 @@ function AnnotationController(
// log in. // log in.
saveNewHighlight(); saveNewHighlight();
updateView(domainModel);
// If this annotation is not a highlight and if it's new (has just been // If this annotation is not a highlight and if it's new (has just been
// created by the annotate button) or it has edits not yet saved to the // created by the annotate button) or it has edits not yet saved to the
// server - then open the editor on AnnotationController instantiation. // server - then open the editor on AnnotationController instantiation.
if (!newlyCreatedByHighlightButton) { if (!newlyCreatedByHighlightButton) {
if (isNew(domainModel) || drafts.get(domainModel)) { if (isNew(vm.annotation) || drafts.get(vm.annotation)) {
vm.edit(); vm.edit();
} }
} }
} }
function updateView(domainModel) {
updateViewModel($scope, domainModel, vm, permissions);
}
function onAnnotationUpdated(event, updatedDomainModel) {
if (updatedDomainModel.id === domainModel.id) {
domainModel = updatedDomainModel;
updateView(updatedDomainModel);
}
}
function deleteIfNewAndEmpty() { function deleteIfNewAndEmpty() {
if (isNew(domainModel) && !vm.form.text && vm.form.tags.length === 0) { if (isNew(vm.annotation) && !vm.state().text && vm.state().tags.length === 0) {
vm.revert(); vm.revert();
} }
} }
function onDestroy() {
// If the annotation component is destroyed whilst the annotation is being
// edited, persist temporary state so that we can restore it if the
// annotation editor is later recreated.
//
// The annotation component may be destroyed when switching accounts,
// when switching groups or when the component is scrolled off-screen.
if (vm.editing()) {
saveToDrafts(drafts, domainModel, vm);
}
}
function onGroupFocused() { function onGroupFocused() {
// New annotations move to the new group, when a new group is focused. // New annotations move to the new group, when a new group is focused.
if (isNew(domainModel)) { if (isNew(vm.annotation)) {
domainModel.group = groups.focused().id; vm.annotation.group = groups.focused().id;
} }
} }
...@@ -308,7 +190,7 @@ function AnnotationController( ...@@ -308,7 +190,7 @@ function AnnotationController(
* *
*/ */
function saveNewHighlight() { function saveNewHighlight() {
if (!isNew(domainModel)) { if (!isNew(vm.annotation)) {
// Already saved. // Already saved.
return; return;
} }
...@@ -318,27 +200,18 @@ function AnnotationController( ...@@ -318,27 +200,18 @@ function AnnotationController(
return; return;
} }
if (domainModel.user) { if (vm.annotation.user) {
// User is logged in, save to server. // User is logged in, save to server.
// Highlights are always private. // Highlights are always private.
domainModel.permissions = permissions.private(); vm.annotation.permissions = permissions.private();
save(domainModel).then(function(model) { save(vm.annotation).then(function(model) {
domainModel = model; model.$$tag = vm.annotation.$$tag;
$rootScope.$emit(events.ANNOTATION_CREATED, model); $rootScope.$emit(events.ANNOTATION_CREATED, model);
updateView(domainModel);
}); });
} else { } else {
// User isn't logged in, save to drafts. // User isn't logged in, save to drafts.
saveToDrafts(drafts, domainModel, vm); drafts.update(vm.annotation, vm.state());
}
} }
/** Switches the view to a viewer, closing the editor controls if they're
* open.
* @name annotation.AnnotationController#view
*/
function view() {
vm.action = 'view';
} }
/** /**
...@@ -355,7 +228,7 @@ function AnnotationController( ...@@ -355,7 +228,7 @@ function AnnotationController(
// performance bottleneck and we would need to get the id token into the // performance bottleneck and we would need to get the id token into the
// session, which we should probably do anyway (and move to opaque bearer // session, which we should probably do anyway (and move to opaque bearer
// tokens for the access token). // tokens for the access token).
return permissions.permits(action, domainModel, session.state.userid); return permissions.permits(action, vm.annotation, session.state.userid);
}; };
/** /**
...@@ -372,7 +245,7 @@ function AnnotationController( ...@@ -372,7 +245,7 @@ function AnnotationController(
errorMessage(reason), 'Deleting annotation failed'); errorMessage(reason), 'Deleting annotation failed');
}; };
$scope.$apply(function() { $scope.$apply(function() {
annotationMapper.deleteAnnotation(domainModel).then( annotationMapper.deleteAnnotation(vm.annotation).then(
null, onRejected); null, onRejected);
}); });
} }
...@@ -385,8 +258,9 @@ function AnnotationController( ...@@ -385,8 +258,9 @@ function AnnotationController(
* @description Switches the view to an editor. * @description Switches the view to an editor.
*/ */
vm.edit = function() { vm.edit = function() {
restoreFromDrafts(drafts, domainModel, vm); if (!drafts.get(vm.annotation)) {
vm.action = isNew(domainModel) ? 'create' : 'edit'; drafts.update(vm.annotation, vm.state());
}
}; };
/** /**
...@@ -396,11 +270,7 @@ function AnnotationController( ...@@ -396,11 +270,7 @@ function AnnotationController(
* (i.e. the annotation editor form should be open), `false` otherwise. * (i.e. the annotation editor form should be open), `false` otherwise.
*/ */
vm.editing = function() { vm.editing = function() {
if (vm.action === 'create' || vm.action === 'edit') { return drafts.get(vm.annotation) && !vm.isSaving;
return true;
} else {
return false;
}
}; };
/** /**
...@@ -409,7 +279,7 @@ function AnnotationController( ...@@ -409,7 +279,7 @@ function AnnotationController(
* @returns {Object} The full group object associated with the annotation. * @returns {Object} The full group object associated with the annotation.
*/ */
vm.group = function() { vm.group = function() {
return groups.get(domainModel.group); return groups.get(vm.annotation.group);
}; };
/** /**
...@@ -419,14 +289,14 @@ function AnnotationController( ...@@ -419,14 +289,14 @@ function AnnotationController(
* otherwise. * otherwise.
*/ */
vm.hasContent = function() { vm.hasContent = function() {
return vm.form.text.length > 0 || vm.form.tags.length > 0; return vm.state().text.length > 0 || vm.state().tags.length > 0;
}; };
/** /**
* @returns {boolean} True if this annotation has quotes * @returns {boolean} True if this annotation has quotes
*/ */
vm.hasQuotes = function() { vm.hasQuotes = function() {
return domainModel.target.some(function(target) { return vm.annotation.target.some(function(target) {
return target.selector && target.selector.some(function(selector) { return target.selector && target.selector.some(function(selector) {
return selector.type === 'TextQuoteSelector'; return selector.type === 'TextQuoteSelector';
}); });
...@@ -434,7 +304,7 @@ function AnnotationController( ...@@ -434,7 +304,7 @@ function AnnotationController(
}; };
vm.id = function() { vm.id = function() {
return domainModel.id; return vm.annotation.id;
}; };
/** /**
...@@ -445,16 +315,16 @@ function AnnotationController( ...@@ -445,16 +315,16 @@ function AnnotationController(
vm.isHighlight = function() { vm.isHighlight = function() {
if (newlyCreatedByHighlightButton) { if (newlyCreatedByHighlightButton) {
return true; return true;
} else if (isNew(domainModel)) { } else if (isNew(vm.annotation)) {
return false; return false;
} else { } else {
// Once an annotation has been saved to the server there's no longer a // Once an annotation has been saved to the server there's no longer a
// simple property that says whether it's a highlight or not. For // simple property that says whether it's a highlight or not. For
// example there's no domainModel.highlight: true. Instead a highlight is // example there's no vm.annotation.highlight: true. Instead a highlight is
// defined as an annotation that isn't a page note or a reply and that // defined as an annotation that isn't a page note or a reply and that
// has no text or tags. // has no text or tags.
var isPageNote = (domainModel.target || []).length === 0; var isPageNote = (vm.annotation.target || []).length === 0;
return (!isPageNote && !isReply(domainModel) && !vm.hasContent()); return (!isPageNote && !isReply(vm.annotation) && !vm.hasContent());
} }
}; };
...@@ -465,7 +335,7 @@ function AnnotationController( ...@@ -465,7 +335,7 @@ function AnnotationController(
* current group or with everyone). * current group or with everyone).
*/ */
vm.isShared = function() { vm.isShared = function() {
return !vm.isPrivate; return !vm.state().isPrivate;
}; };
// Save on Meta + Enter or Ctrl + Enter. // Save on Meta + Enter or Ctrl + Enter.
...@@ -488,15 +358,15 @@ function AnnotationController( ...@@ -488,15 +358,15 @@ function AnnotationController(
* Creates a new message in reply to this annotation. * Creates a new message in reply to this annotation.
*/ */
vm.reply = function() { vm.reply = function() {
var references = (domainModel.references || []).concat(domainModel.id); var references = (vm.annotation.references || []).concat(vm.annotation.id);
var reply = annotationMapper.createAnnotation({ var reply = annotationMapper.createAnnotation({
references: references, references: references,
uri: domainModel.uri uri: vm.annotation.uri
}); });
reply.group = domainModel.group; reply.group = vm.annotation.group;
if (session.state.userid) { if (session.state.userid) {
if (vm.isPrivate) { if (vm.state().isPrivate) {
reply.permissions = permissions.private(); reply.permissions = permissions.private();
} else { } else {
reply.permissions = permissions.shared(reply.group); reply.permissions = permissions.shared(reply.group);
...@@ -510,56 +380,38 @@ function AnnotationController( ...@@ -510,56 +380,38 @@ function AnnotationController(
* @description Reverts an edit in progress and returns to the viewer. * @description Reverts an edit in progress and returns to the viewer.
*/ */
vm.revert = function() { vm.revert = function() {
drafts.remove(domainModel); drafts.remove(vm.annotation);
if (vm.action === 'create') { if (isNew(vm.annotation)) {
$rootScope.$emit(events.ANNOTATION_DELETED, domainModel); $rootScope.$emit(events.ANNOTATION_DELETED, vm.annotation);
} else {
updateView(domainModel);
view();
} }
}; };
/**
* @ngdoc method
* @name annotation.AnnotationController#save
* @description Saves any edits and returns to the viewer.
*/
vm.save = function() { vm.save = function() {
if (!domainModel.user) { if (!vm.annotation.user) {
flash.info('Please sign in to save your annotations.'); flash.info('Please sign in to save your annotations.');
return Promise.resolve(); return Promise.resolve();
} }
if ((vm.action === 'create' || vm.action === 'edit') && if (!vm.hasContent() && vm.isShared()) {
!vm.hasContent() && vm.isShared()) {
flash.info('Please add text or a tag before publishing.'); flash.info('Please add text or a tag before publishing.');
return Promise.resolve(); return Promise.resolve();
} }
var updatedModel = angular.copy(domainModel); var updatedModel = updateModel(vm.annotation, vm.state(), permissions);
// Copy across the non-enumerable local tag for the annotation // Optimistically switch back to view mode and display the saving
updatedModel.$$tag = domainModel.$$tag;
updateDomainModel(updatedModel, vm, permissions);
var saved = save(updatedModel).then(function (model) {
var isNew = !domainModel.id;
drafts.remove(domainModel);
domainModel = model;
if (isNew) {
$rootScope.$emit(events.ANNOTATION_CREATED, domainModel);
} else {
$rootScope.$emit(events.ANNOTATION_UPDATED, domainModel);
}
updateView(domainModel);
});
// optimistically switch back to view mode and display the saving
// indicator // indicator
vm.isSaving = true; vm.isSaving = true;
view();
return saved.then(function () { return save(updatedModel).then(function (model) {
Object.assign(updatedModel, model);
vm.isSaving = false; vm.isSaving = false;
var event = isNew(vm.annotation) ?
events.ANNOTATION_CREATED : events.ANNOTATION_UPDATED;
drafts.remove(vm.annotation);
$rootScope.$emit(event, updatedModel);
}).catch(function (reason) { }).catch(function (reason) {
vm.isSaving = false; vm.isSaving = false;
vm.edit(); vm.edit();
...@@ -584,10 +436,14 @@ function AnnotationController( ...@@ -584,10 +436,14 @@ function AnnotationController(
// creating or editing, we cache that and use the same privacy level the // creating or editing, we cache that and use the same privacy level the
// next time they create an annotation. // next time they create an annotation.
// But _don't_ cache it when they change the privacy level of a reply. // But _don't_ cache it when they change the privacy level of a reply.
if (!isReply(domainModel)) { if (!isReply(vm.annotation)) {
permissions.setDefault(privacy); permissions.setDefault(privacy);
} }
vm.isPrivate = (privacy === 'private'); drafts.update(vm.annotation, {
tags: vm.state().tags,
text: vm.state().text,
isPrivate: privacy === 'private'
});
}; };
vm.tagStreamURL = function(tag) { vm.tagStreamURL = function(tag) {
...@@ -595,23 +451,34 @@ function AnnotationController( ...@@ -595,23 +451,34 @@ function AnnotationController(
}; };
vm.target = function() { vm.target = function() {
return domainModel.target; return vm.annotation.target;
}; };
vm.updated = function() { vm.updated = function() {
return domainModel.updated; return vm.annotation.updated;
}; };
vm.user = function() { vm.user = function() {
return domainModel.user; return vm.annotation.user;
}; };
vm.username = function() { vm.username = function() {
return persona.username(domainModel.user); return persona.username(vm.annotation.user);
}; };
vm.isReply = function () { vm.isReply = function () {
return isReply(domainModel); return isReply(vm.annotation);
};
vm.links = function () {
if (vm.annotation.links) {
return {incontext: vm.annotation.links.incontext ||
vm.annotation.links.html ||
'',
html: vm.annotation.links.html};
} else {
return {incontext: '', html: ''};
}
}; };
/** /**
...@@ -630,11 +497,37 @@ function AnnotationController( ...@@ -630,11 +497,37 @@ function AnnotationController(
}; };
vm.setText = function (text) { vm.setText = function (text) {
vm.form.text = text; drafts.update(vm.annotation, {
isPrivate: vm.state().isPrivate,
tags: vm.state().tags,
text: text,
});
}; };
vm.setTags = function (tags) { vm.setTags = function (tags) {
vm.form.tags = tags; drafts.update(vm.annotation, {
isPrivate: vm.state().isPrivate,
tags: tags,
text: vm.state().text,
});
};
vm.state = function () {
var draft = drafts.get(vm.annotation);
if (draft) {
return draft;
}
return {
tags: vm.annotation.tags,
text: vm.annotation.text,
isPrivate: permissions.isPrivate(vm.annotation.permissions,
vm.annotation.user),
};
};
var documentMeta = memoize(annotationMetadata.domainAndTitle);
vm.documentMeta = function () {
return documentMeta(vm.annotation);
}; };
init(); init();
...@@ -664,7 +557,7 @@ module.exports = { ...@@ -664,7 +557,7 @@ module.exports = {
// to be unit tested. // to be unit tested.
// FIXME: The code should be refactored to enable unit testing without having // FIXME: The code should be refactored to enable unit testing without having
// to do this. // to do this.
updateDomainModel: updateDomainModel, updateModel: updateModel,
// These are meant to be the public API of this module. // These are meant to be the public API of this module.
directive: annotation, directive: annotation,
......
...@@ -11,6 +11,12 @@ var util = require('./util'); ...@@ -11,6 +11,12 @@ var util = require('./util');
var inject = angular.mock.inject; var inject = angular.mock.inject;
var fakeDocumentMeta = {
domain: 'docs.io',
titleLink: 'http://docs.io/doc.html',
titleText: 'Dummy title',
};
/** /**
* Returns the annotation directive with helpers stubbed out. * Returns the annotation directive with helpers stubbed out.
*/ */
...@@ -19,10 +25,13 @@ function annotationDirective() { ...@@ -19,10 +25,13 @@ function annotationDirective() {
var annotation = proxyquire('../annotation', { var annotation = proxyquire('../annotation', {
angular: testUtil.noCallThru(angular), angular: testUtil.noCallThru(angular),
'../filter/document-domain': noop,
'../filter/document-title': noop,
'../filter/persona': { '../filter/persona': {
username: noop, username: noop,
},
'../annotation-metadata': {
domainAndTitle: function (annot) {
return fakeDocumentMeta;
},
} }
}); });
...@@ -30,8 +39,8 @@ function annotationDirective() { ...@@ -30,8 +39,8 @@ function annotationDirective() {
} }
describe('annotation', function() { describe('annotation', function() {
describe('updateDomainModel()', function() { describe('updateModel()', function() {
var updateDomainModel = require('../annotation').updateDomainModel; var updateModel = require('../annotation').updateModel;
function fakePermissions() { function fakePermissions() {
return { return {
...@@ -40,89 +49,30 @@ describe('annotation', function() { ...@@ -40,89 +49,30 @@ describe('annotation', function() {
}; };
} }
function fakeGroups() { it('copies tags and text into the new model', function() {
return { var changes = {text: 'bar', tags: ['foo', 'bar']};
focused: function() {return {};}, var newModel = updateModel(fixtures.defaultAnnotation(), changes,
}; fakePermissions());
} assert.deepEqual(newModel.tags, changes.tags);
assert.equal(newModel.text, changes.text);
it('copies text from viewModel into domainModel', function() {
var domainModel = {};
var viewModel = {form: {text: 'bar', tags: []}};
updateDomainModel(domainModel, viewModel, fakePermissions(),
fakeGroups());
assert.equal(domainModel.text, viewModel.form.text);
});
it('overwrites text in domainModel', function() {
var domainModel = {text: 'foo'};
var viewModel = {form: {text: 'bar', tags: []}};
updateDomainModel(domainModel, viewModel, fakePermissions(),
fakeGroups());
assert.equal(domainModel.text, viewModel.form.text);
}); });
it('doesn\'t touch other properties in domainModel', function() { it('sets permissions to private if the draft is private', function() {
var domainModel = {foo: 'foo', bar: 'bar'}; var changes = {isPrivate: true, text: 'bar', tags: ['foo', 'bar']};
var viewModel = {form: {foo: 'FOO', tags: []}}; var annot = fixtures.defaultAnnotation();
updateDomainModel(domainModel, viewModel, fakePermissions(),
fakeGroups());
assert.equal(
domainModel.bar, 'bar',
'updateDomainModel() should not touch properties of domainModel' +
'that don\'t exist in viewModel');
});
it('copies tag texts from viewModel into domainModel', function() {
var domainModel = {};
var viewModel = {
form: {
tags: ['foo', 'bar'],
}
};
updateDomainModel(domainModel, viewModel, fakePermissions(),
fakeGroups());
assert.deepEqual(domainModel.tags, ['foo', 'bar']);
});
it('sets domainModel.permissions to private if vm.isPrivate', function() {
var domainModel = {};
var viewModel = {
isPrivate: true,
form: {
text: 'foo',
},
};
var permissions = fakePermissions(); var permissions = fakePermissions();
permissions.private = sinon.stub().returns('private permissions'); permissions.private = sinon.stub().returns('private permissions');
var newModel = updateModel(annot, changes, permissions);
updateDomainModel(domainModel, viewModel, permissions, fakeGroups()); assert.equal(newModel.permissions, 'private permissions');
assert.equal(domainModel.permissions, 'private permissions');
}); });
it('sets domainModel.permissions to shared if !vm.isPrivate', function() { it('sets permissions to shared if the draft is shared', function() {
var domainModel = {}; var changes = {isPrivate: false, text: 'bar', tags: ['foo', 'bar']};
var viewModel = { var annot = fixtures.defaultAnnotation();
isPrivate: false,
form: {
text: 'foo',
},
};
var permissions = fakePermissions(); var permissions = fakePermissions();
permissions.shared = sinon.stub().returns('shared permissions'); permissions.shared = sinon.stub().returns('shared permissions');
var newModel = updateModel(annot, changes, permissions);
updateDomainModel(domainModel, viewModel, permissions, fakeGroups()); assert.equal(newModel.permissions, 'shared permissions');
assert.equal(domainModel.permissions, 'shared permissions');
}); });
}); });
...@@ -160,8 +110,6 @@ describe('annotation', function() { ...@@ -160,8 +110,6 @@ describe('annotation', function() {
}; };
} }
before(function() { before(function() {
angular.module('h', []) angular.module('h', [])
.directive('annotation', annotationDirective()); .directive('annotation', annotationDirective());
...@@ -248,11 +196,11 @@ describe('annotation', function() { ...@@ -248,11 +196,11 @@ describe('annotation', function() {
$provide.value('drafts', fakeDrafts); $provide.value('drafts', fakeDrafts);
$provide.value('features', fakeFeatures); $provide.value('features', fakeFeatures);
$provide.value('flash', fakeFlash); $provide.value('flash', fakeFlash);
$provide.value('groups', fakeGroups);
$provide.value('permissions', fakePermissions); $provide.value('permissions', fakePermissions);
$provide.value('session', fakeSession); $provide.value('session', fakeSession);
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
$provide.value('store', fakeStore); $provide.value('store', fakeStore);
$provide.value('groups', fakeGroups);
})); }));
beforeEach( beforeEach(
...@@ -380,12 +328,22 @@ describe('annotation', function() { ...@@ -380,12 +328,22 @@ describe('annotation', function() {
assert.notCalled(fakeStore.annotation.create); assert.notCalled(fakeStore.annotation.create);
}); });
it('edits new annotations on initialization', function() { it('creates drafts for new annotations on initialization', function() {
var annotation = fixtures.newAnnotation(); var annotation = fixtures.newAnnotation();
createDirective(annotation);
assert.calledWith(fakeDrafts.update, annotation, {
isPrivate: false,
tags: annotation.tags,
text: annotation.text,
});
});
it('does not create drafts for new highlights on initialization', function() {
var annotation = fixtures.newHighlight();
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
assert.isTrue(controller.editing()); assert.notOk(controller.editing());
assert.notCalled(fakeDrafts.update);
}); });
it('edits annotations with drafts on initialization', function() { it('edits annotations with drafts on initialization', function() {
...@@ -397,51 +355,29 @@ describe('annotation', function() { ...@@ -397,51 +355,29 @@ describe('annotation', function() {
assert.isTrue(controller.editing()); assert.isTrue(controller.editing());
}); });
it('does not edit new highlights on initialization', function() {
var annotation = fixtures.newHighlight();
var controller = createDirective(annotation).controller;
assert.isFalse(controller.editing());
});
it('edits highlights with drafts on initialization', function() {
var annotation = fixtures.oldHighlight();
// You can edit a highlight, enter some text or tags, and save it (the
// highlight then becomes an annotation). You can also edit a highlight
// and then change focus to another group and back without saving the
// highlight, in which case the highlight will have draft edits.
// This highlight has draft edits.
fakeDrafts.get.returns({text: '', tags: []});
var controller = createDirective(annotation).controller;
assert.isTrue(controller.editing());
});
}); });
describe('.editing()', function() { describe('#editing()', function() {
it('returns true if action is "create"', function() { it('returns false if the annotation does not have a draft', function () {
var controller = createDirective().controller; var controller = createDirective().controller;
controller.action = 'create'; assert.notOk(controller.editing());
assert.equal(controller.editing(), true);
}); });
it('returns true if action is "edit"', function() { it('returns true if the annotation has a draft', function () {
var controller = createDirective().controller; var controller = createDirective().controller;
controller.action = 'edit'; fakeDrafts.get.returns({tags: [], text: '', isPrivate: false});
assert.equal(controller.editing(), true); assert.isTrue(controller.editing());
}); });
it('returns false if action is "view"', function() { it('returns false if the annotation has a draft but is being saved', function () {
var controller = createDirective().controller; var controller = createDirective().controller;
controller.action = 'view'; fakeDrafts.get.returns({tags: [], text: '', isPrivate: false});
assert.equal(controller.editing(), false); controller.isSaving = true;
assert.isFalse(controller.editing());
}); });
}); });
describe('.isHighlight()', function() { describe('#isHighlight()', function() {
it('returns true for new highlights', function() { it('returns true for new highlights', function() {
var annotation = fixtures.newHighlight(); var annotation = fixtures.newHighlight();
...@@ -582,7 +518,7 @@ describe('annotation', function() { ...@@ -582,7 +518,7 @@ describe('annotation', function() {
'does not add the world readable principal if the parent is private', 'does not add the world readable principal if the parent is private',
function() { function() {
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
controller.isPrivate = true; fakePermissions.isPrivate.returns(true);
var reply = {}; var reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply); fakeAnnotationMapper.createAnnotation.returns(reply);
controller.reply(); controller.reply();
...@@ -606,78 +542,49 @@ describe('annotation', function() { ...@@ -606,78 +542,49 @@ describe('annotation', function() {
describe('#setPrivacy', function() { describe('#setPrivacy', function() {
it('makes the annotation private when level is "private"', function() { it('makes the annotation private when level is "private"', function() {
var parts = createDirective(); var parts = createDirective();
// Make this annotation shared.
parts.controller.isPrivate = false;
fakePermissions.isPrivate.returns(false);
// Edit the annotation and make it private.
parts.controller.edit();
parts.controller.setPrivacy('private'); parts.controller.setPrivacy('private');
fakePermissions.isPrivate.returns(true); assert.calledWith(fakeDrafts.update, parts.controller.annotation, sinon.match({
isPrivate: true,
return parts.controller.save().then(function() { }));
// Verify that the permissions are updated once the annotation
// is saved.
assert.equal(parts.controller.isPrivate, true);
});
}); });
it('makes the annotation shared when level is "shared"', function() { it('makes the annotation shared when level is "shared"', function() {
var parts = createDirective(); var parts = createDirective();
parts.controller.isPrivate = true;
parts.controller.edit();
parts.controller.form.text = 'test';
parts.controller.setPrivacy('shared'); parts.controller.setPrivacy('shared');
return parts.controller.save().then(function() { assert.calledWith(fakeDrafts.update, parts.controller.annotation, sinon.match({
assert.equal(parts.controller.isPrivate, false); isPrivate: false,
}); }));
}); });
it('saves the "shared" visibility level to localStorage', function() { it('sets the default visibility level if "shared"', function() {
var parts = createDirective(); var parts = createDirective();
parts.controller.edit(); parts.controller.edit();
parts.controller.setPrivacy('shared'); parts.controller.setPrivacy('shared');
parts.controller.form.text = 'test';
return parts.controller.save().then(function() {
assert.calledWith(fakePermissions.setDefault, 'shared'); assert.calledWith(fakePermissions.setDefault, 'shared');
}); });
});
it('saves the "private" visibility level to localStorage', function() { it('sets the default visibility if "private"', function() {
var parts = createDirective(); var parts = createDirective();
parts.controller.edit(); parts.controller.edit();
parts.controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return parts.controller.save().then(function() {
assert.calledWith(fakePermissions.setDefault, 'private'); assert.calledWith(fakePermissions.setDefault, 'private');
}); });
});
it('doesn\'t save the visibility if the annotation is a reply', function() { it('doesn\'t save the visibility if the annotation is a reply', function() {
var parts = createDirective(); var parts = createDirective(fixtures.oldReply());
parts.annotation.references = ['parent id'];
parts.controller.edit();
parts.controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return parts.controller.save().then(function() {
assert.notCalled(fakePermissions.setDefault); assert.notCalled(fakePermissions.setDefault);
}); });
}); });
});
describe('#hasContent', function() { describe('#hasContent', function() {
it('returns false if the annotation has no tags or text', function() { it('returns false if the annotation has no tags or text', function() {
var controller = createDirective().controller; var controller = createDirective(fixtures.oldHighlight()).controller;
controller.form.text = '';
controller.form.tags = [];
assert.ok(!controller.hasContent()); assert.ok(!controller.hasContent());
}); });
it('returns true if the annotation has tags or text', function() { it('returns true if the annotation has tags or text', function() {
var controller = createDirective().controller; var controller = createDirective(fixtures.oldAnnotation()).controller;
controller.form.text = 'bar';
assert.ok(controller.hasContent());
controller.form.text = '';
controller.form.tags = ['foo'];
assert.ok(controller.hasContent()); assert.ok(controller.hasContent());
}); });
}); });
...@@ -786,6 +693,14 @@ describe('annotation', function() { ...@@ -786,6 +693,14 @@ describe('annotation', function() {
}); });
}); });
describe('#documentMeta()', function () {
it('returns the domain, title link and text for the annotation', function () {
var annot = fixtures.defaultAnnotation();
var controller = createDirective(annot).controller;
assert.deepEqual(controller.documentMeta(), fakeDocumentMeta);
});
});
describe('saving a new annotation', function() { describe('saving a new annotation', function() {
var annotation; var annotation;
...@@ -793,26 +708,27 @@ describe('annotation', function() { ...@@ -793,26 +708,27 @@ describe('annotation', function() {
annotation = fixtures.newAnnotation(); annotation = fixtures.newAnnotation();
}); });
function controllerWithActionCreate() { function createController() {
var controller = createDirective(annotation).controller; return createDirective(annotation).controller;
controller.action = 'create';
controller.form.text = 'new annotation';
return controller;
} }
it( it('removes the draft when saving an annotation succeeds', function () {
'emits annotationCreated when saving an annotation succeeds', var controller = createController();
function() { return controller.save().then(function () {
var controller = controllerWithActionCreate(); assert.calledWith(fakeDrafts.remove, annotation);
});
});
it('emits annotationCreated when saving an annotation succeeds', function () {
var controller = createController();
sandbox.spy($rootScope, '$emit'); sandbox.spy($rootScope, '$emit');
return controller.save().then(function() { return controller.save().then(function() {
assert.calledWith($rootScope.$emit, events.ANNOTATION_CREATED); assert.calledWith($rootScope.$emit, events.ANNOTATION_CREATED);
}); });
} });
);
it('flashes a generic error if the server can\'t be reached', function() { it('flashes a generic error if the server can\'t be reached', function() {
var controller = controllerWithActionCreate(); var controller = createController();
fakeStore.annotation.create = sinon.stub().returns(Promise.reject({ fakeStore.annotation.create = sinon.stub().returns(Promise.reject({
status: 0 status: 0
})); }));
...@@ -823,7 +739,7 @@ describe('annotation', function() { ...@@ -823,7 +739,7 @@ describe('annotation', function() {
}); });
it('flashes an error if saving the annotation fails on the server', function() { it('flashes an error if saving the annotation fails on the server', function() {
var controller = controllerWithActionCreate(); var controller = createController();
fakeStore.annotation.create = sinon.stub().returns(Promise.reject({ fakeStore.annotation.create = sinon.stub().returns(Promise.reject({
status: 500, status: 500,
statusText: 'Server Error', statusText: 'Server Error',
...@@ -836,59 +752,41 @@ describe('annotation', function() { ...@@ -836,59 +752,41 @@ describe('annotation', function() {
}); });
it('doesn\'t flash an error when saving an annotation succeeds', function() { it('doesn\'t flash an error when saving an annotation succeeds', function() {
var controller = controllerWithActionCreate(); var controller = createController();
controller.save(); return controller.save().then(function () {
assert.notCalled(fakeFlash.error); assert.notCalled(fakeFlash.error);
}); });
});
it('shows a saving indicator when saving an annotation', function() { it('shows a saving indicator when saving an annotation', function() {
var controller = controllerWithActionCreate(); var controller = createController();
var create; var create;
fakeStore.annotation.create = sinon.stub().returns(new Promise(function (resolve) { fakeStore.annotation.create = sinon.stub().returns(new Promise(function (resolve) {
create = resolve; create = resolve;
})); }));
var saved = controller.save(); var saved = controller.save();
assert.equal(controller.isSaving, true); assert.equal(controller.isSaving, true);
assert.equal(controller.action, 'view'); create(Object.assign({}, controller.annotation, {id: 'new-id'}));
create();
return saved.then(function () { return saved.then(function () {
assert.equal(controller.isSaving, false); assert.equal(controller.isSaving, false);
}); });
}); });
it('reverts to edit mode if saving fails', function () { it('does not remove the draft if saving fails', function () {
var controller = controllerWithActionCreate(); var controller = createController();
var failCreation; fakeStore.annotation.create = sinon.stub().returns(Promise.reject({status: -1}));
fakeStore.annotation.create = sinon.stub().returns(new Promise(function (resolve, reject) { return controller.save().then(function () {
failCreation = reject; assert.notCalled(fakeDrafts.remove);
}));
var saved = controller.save();
assert.equal(controller.isSaving, true);
failCreation({status: -1});
return saved.then(function () {
assert.equal(controller.isSaving, false);
assert.ok(controller.editing());
}); });
}); });
it( it('sets the annotation\'s group to the focused group', function() {
'Passes group:<id> to the server when saving a new annotation',
function() {
fakeGroups.focused = function () { fakeGroups.focused = function () {
return { id: 'test-id' }; return { id: 'test-id' };
}; };
var annotation = { var controller = createDirective(fixtures.newAnnotation()).controller;
user: 'acct:fred@hypothes.is', assert.equal(controller.annotation.group, 'test-id');
text: 'foo',
};
var controller = createDirective(annotation).controller;
controller.action = 'create';
return controller.save().then(function() {
assert.calledWith(fakeStore.annotation.create, sinon.match({}),
sinon.match({group: 'test-id'}));
}); });
}
);
}); });
describe('saving an edited an annotation', function() { describe('saving an edited an annotation', function() {
...@@ -896,19 +794,15 @@ describe('annotation', function() { ...@@ -896,19 +794,15 @@ describe('annotation', function() {
beforeEach(function() { beforeEach(function() {
annotation = fixtures.defaultAnnotation(); annotation = fixtures.defaultAnnotation();
fakeDrafts.get.returns({text: 'unsaved change'});
}); });
function controllerWithActionEdit() { function createController() {
var controller = createDirective(annotation).controller; return createDirective(annotation).controller;
controller.action = 'edit';
controller.form.text = 'updated text';
return controller;
} }
it( it('flashes a generic error if the server cannot be reached', function () {
'flashes a generic error if the server cannot be reached', var controller = createController();
function() {
var controller = controllerWithActionEdit();
fakeStore.annotation.update = sinon.stub().returns(Promise.reject({ fakeStore.annotation.update = sinon.stub().returns(Promise.reject({
status: -1 status: -1
})); }));
...@@ -916,13 +810,10 @@ describe('annotation', function() { ...@@ -916,13 +810,10 @@ describe('annotation', function() {
assert.calledWith(fakeFlash.error, assert.calledWith(fakeFlash.error,
'Service unreachable.', 'Saving annotation failed'); 'Service unreachable.', 'Saving annotation failed');
}); });
} });
);
it( it('flashes an error if saving the annotation fails on the server', function () {
'flashes an error if saving the annotation fails on the server', var controller = createController();
function() {
var controller = controllerWithActionEdit();
fakeStore.annotation.update = sinon.stub().returns(Promise.reject({ fakeStore.annotation.update = sinon.stub().returns(Promise.reject({
status: 500, status: 500,
statusText: 'Server Error', statusText: 'Server Error',
...@@ -932,18 +823,14 @@ describe('annotation', function() { ...@@ -932,18 +823,14 @@ describe('annotation', function() {
assert.calledWith(fakeFlash.error, assert.calledWith(fakeFlash.error,
'500 Server Error', 'Saving annotation failed'); '500 Server Error', 'Saving annotation failed');
}); });
} });
);
it( it('doesn\'t flash an error if saving the annotation succeeds', function () {
'doesn\'t flash an error if saving the annotation succeeds', var controller = createController();
function() { return controller.save().then(function () {
var controller = controllerWithActionEdit();
controller.form.text = 'updated text';
controller.save();
assert.notCalled(fakeFlash.error); assert.notCalled(fakeFlash.error);
} });
); });
}); });
describe('drafts', function() { describe('drafts', function() {
...@@ -962,8 +849,8 @@ describe('annotation', function() { ...@@ -962,8 +849,8 @@ describe('annotation', function() {
text: 'unsaved-text' text: 'unsaved-text'
}); });
var controller = createDirective().controller; var controller = createDirective().controller;
assert.deepEqual(controller.form.tags, ['unsaved-tag']); assert.deepEqual(controller.state().tags, ['unsaved-tag']);
assert.equal(controller.form.text, 'unsaved-text'); assert.equal(controller.state().text, 'unsaved-text');
}); });
it('removes the draft when changes are discarded', function() { it('removes the draft when changes are discarded', function() {
...@@ -976,41 +863,13 @@ describe('annotation', function() { ...@@ -976,41 +863,13 @@ describe('annotation', function() {
it('removes the draft when changes are saved', function() { it('removes the draft when changes are saved', function() {
var annotation = fixtures.defaultAnnotation(); var annotation = fixtures.defaultAnnotation();
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
controller.edit(); fakeDrafts.get.returns({text: 'unsaved changes'});
controller.form.text = 'test annotation';
return controller.save().then(function() { return controller.save().then(function() {
assert.calledWith(fakeDrafts.remove, annotation); assert.calledWith(fakeDrafts.remove, annotation);
}); });
}); });
}); });
describe('onAnnotationUpdated()', function() {
it('updates vm.form.text', function() {
var parts = createDirective();
var updatedModel = {
id: parts.annotation.id,
text: 'new text',
};
$rootScope.$emit(events.ANNOTATION_UPDATED, updatedModel);
assert.equal(parts.controller.form.text, 'new text');
});
it('doesn\'t update if a different annotation was updated', function() {
var parts = createDirective();
parts.controller.form.text = 'original text';
var updatedModel = {
id: 'different annotation id',
text: 'new text',
};
$rootScope.$emit(events.ANNOTATION_UPDATED, updatedModel);
assert.equal(parts.controller.form.text, 'original text');
});
});
describe('when another new annotation is created', function () { describe('when another new annotation is created', function () {
it('removes the current annotation if empty', function () { it('removes the current annotation if empty', function () {
var annotation = fixtures.newEmptyAnnotation(); var annotation = fixtures.newEmptyAnnotation();
...@@ -1021,65 +880,43 @@ describe('annotation', function() { ...@@ -1021,65 +880,43 @@ describe('annotation', function() {
}); });
it('does not remove the current annotation if is is not new', function () { it('does not remove the current annotation if is is not new', function () {
var parts = createDirective(fixtures.defaultAnnotation()); createDirective(fixtures.defaultAnnotation());
parts.controller.form.text = ''; fakeDrafts.get.returns({text: '', tags: []});
parts.controller.form.tags = [];
$rootScope.$emit(events.BEFORE_ANNOTATION_CREATED, $rootScope.$emit(events.BEFORE_ANNOTATION_CREATED,
fixtures.newAnnotation()); fixtures.newAnnotation());
assert.notCalled(fakeDrafts.remove); assert.notCalled(fakeDrafts.remove);
}); });
it('does not remove the current annotation if it has text', function () { it('does not remove the current annotation if the scope was destroyed', function () {
var annotation = fixtures.newAnnotation(); var annotation = fixtures.newEmptyAnnotation();
var parts = createDirective(annotation); var parts = createDirective(annotation);
parts.controller.form.text = 'An incomplete thought'; parts.scope.$destroy();
$rootScope.$emit(events.BEFORE_ANNOTATION_CREATED, $rootScope.$emit(events.BEFORE_ANNOTATION_CREATED,
fixtures.newAnnotation()); fixtures.newAnnotation());
assert.notCalled(fakeDrafts.remove); assert.notCalled(fakeDrafts.remove);
}); });
it('does not remove the current annotation if it has tags', function () { it('does not remove the current annotation if it has text', function () {
var annotation = fixtures.newAnnotation(); var annotation = fixtures.newAnnotation();
var parts = createDirective(annotation); createDirective(annotation);
parts.controller.form.tags = ['a-tag']; fakeDrafts.get.returns({text: 'An incomplete thought'});
$rootScope.$emit(events.BEFORE_ANNOTATION_CREATED, $rootScope.$emit(events.BEFORE_ANNOTATION_CREATED,
fixtures.newAnnotation()); fixtures.newAnnotation());
assert.notCalled(fakeDrafts.remove); assert.notCalled(fakeDrafts.remove);
}); });
});
describe('when component is destroyed', function () {
it('if the annotation is being edited it updates drafts', function() {
var parts = createDirective();
parts.controller.isPrivate = true;
parts.controller.edit();
parts.controller.form.text = 'unsaved-text';
parts.controller.form.tags = [];
fakeDrafts.get = sinon.stub().returns({
text: 'old-draft'
});
fakeDrafts.update = sinon.stub();
parts.scope.$broadcast('$destroy'); it('does not remove the current annotation if it has tags', function () {
var annotation = fixtures.newAnnotation();
assert.calledWith( createDirective(annotation);
fakeDrafts.update, fakeDrafts.get.returns({tags: ['a-tag']});
parts.annotation, {isPrivate:true, tags:[], text:'unsaved-text'}); $rootScope.$emit(events.BEFORE_ANNOTATION_CREATED,
}); fixtures.newAnnotation());
assert.notCalled(fakeDrafts.remove);
it('if the annotation isn\'t being edited it doesn\'t update drafts', function() {
var parts = createDirective();
parts.controller.isPrivate = true;
fakeDrafts.update = sinon.stub();
parts.scope.$broadcast('$destroy');
assert.notCalled(fakeDrafts.update);
}); });
}); });
describe('onGroupFocused()', function() { describe('when the focused group changes', function() {
it('updates domainModel.group if the annotation is new', function () { it('moves new annotations to the focused group', function () {
var annotation = fixtures.newAnnotation(); var annotation = fixtures.newAnnotation();
annotation.group = 'old-group-id'; annotation.group = 'old-group-id';
createDirective(annotation); createDirective(annotation);
...@@ -1090,7 +927,7 @@ describe('annotation', function() { ...@@ -1090,7 +927,7 @@ describe('annotation', function() {
assert.equal(annotation.group, 'new-group-id'); assert.equal(annotation.group, 'new-group-id');
}); });
it('does not update domainModel.group if the annotation is not new', it('does not modify the group of saved annotations',
function () { function () {
var annotation = fixtures.oldAnnotation(); var annotation = fixtures.oldAnnotation();
annotation.group = 'old-group-id'; annotation.group = 'old-group-id';
...@@ -1104,80 +941,24 @@ describe('annotation', function() { ...@@ -1104,80 +941,24 @@ describe('annotation', function() {
); );
}); });
describe('reverting edits', function () { describe('reverting edits', function () {
// Simulate what happens when the user edits an annotation, it('removes the current draft', function() {
// clicks Save, gets an error because the server fails to save the var controller = createDirective(fixtures.defaultAnnotation()).controller;
// annotation, then clicks Cancel - in the frontend the annotation should
// be restored to its original value, the edits lost.
it('restores the original text', function() {
var controller = createDirective({
id: 'test-annotation-id',
user: 'acct:bill@localhost',
text: 'Initial annotation body text',
}).controller;
fakeStore.annotation.update = function () {
return Promise.reject({
status: 500,
statusText: 'Server Error',
data: {}
});
};
var originalText = controller.form.text;
// Simulate the user clicking the Edit button on the annotation.
controller.edit();
// Simulate the user typing some text into the annotation editor textarea.
controller.form.text = 'changed by test code';
// Simulate the user hitting the Save button and wait for the
// (unsuccessful) response from the server.
controller.save();
// At this point the annotation editor controls are still open, and the
// annotation's text is still the modified (unsaved) text.
assert.equal(controller.form.text, 'changed by test code');
// Simulate the user clicking the Cancel button.
controller.revert();
assert.equal(controller.form.text, originalText);
});
// Test that editing reverting changes to an annotation with
// no text resets the text to be empty.
it('clears the text if the text was originally empty', function() {
var controller = createDirective({
id: 'test-annotation-id',
user: 'acct:bill@localhost',
}).controller;
controller.edit(); controller.edit();
assert.equal(controller.action, 'edit');
controller.form.text = 'this should be reverted';
controller.revert(); controller.revert();
assert.equal(controller.form.text, ''); assert.calledWith(fakeDrafts.remove, controller.annotation);
}); });
it('reverts to the most recently saved version', function () { it('deletes the annotation if it was new', function () {
fakeStore.annotation.update = function (params, ann) { var controller = createDirective(fixtures.newAnnotation()).controller;
return Promise.resolve(Object.assign({}, ann)); sandbox.spy($rootScope, '$emit');
};
var controller = createDirective({
id: 'new-annot',
user: 'acct:bill@localhost',
}).controller;
controller.edit();
controller.form.text = 'New annotation text';
return controller.save().then(function () {
controller.edit();
controller.form.text = 'Updated annotation text';
return controller.save();
}).then(function () {
controller.edit();
controller.revert(); controller.revert();
assert.equal(controller.form.text, 'Updated annotation text'); assert.calledWith($rootScope.$emit, events.ANNOTATION_DELETED);
});
}); });
}); });
describe('tag display', function () { describe('tag display', function () {
it('displays annotation tags', function () { it('displays links to tags on the stream', function () {
var directive = createDirective({ var directive = createDirective({
id: '1234', id: '1234',
tags: ['atag'] tags: ['atag']
...@@ -1193,7 +974,7 @@ describe('annotation', function() { ...@@ -1193,7 +974,7 @@ describe('annotation', function() {
}); });
describe('annotation links', function () { describe('annotation links', function () {
it('linkInContext uses the in-context links when available', function () { it('uses the in-context links when available', function () {
var annotation = Object.assign({}, fixtures.defaultAnnotation(), { var annotation = Object.assign({}, fixtures.defaultAnnotation(), {
links: { links: {
html: 'https://test.hypothes.is/a/deadbeef', html: 'https://test.hypothes.is/a/deadbeef',
...@@ -1201,22 +982,20 @@ describe('annotation', function() { ...@@ -1201,22 +982,20 @@ describe('annotation', function() {
}, },
}); });
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
assert.equal(controller.links().incontext, annotation.links.incontext);
assert.equal(controller.linkInContext, annotation.links.incontext);
}); });
it('linkInContext falls back to the HTML link when in-context links are missing', function () { it('falls back to the HTML link when in-context links are missing', function () {
var annotation = Object.assign({}, fixtures.defaultAnnotation(), { var annotation = Object.assign({}, fixtures.defaultAnnotation(), {
links: { links: {
html: 'https://test.hypothes.is/a/deadbeef', html: 'https://test.hypothes.is/a/deadbeef',
}, },
}); });
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
assert.equal(controller.links().html, annotation.links.html);
assert.equal(controller.linkInContext, annotation.links.html);
}); });
it('linkHTML uses the HTML link when available', function () { it('uses the HTML link when available', function () {
var annotation = Object.assign({}, fixtures.defaultAnnotation(), { var annotation = Object.assign({}, fixtures.defaultAnnotation(), {
links: { links: {
html: 'https://test.hypothes.is/a/deadbeef', html: 'https://test.hypothes.is/a/deadbeef',
...@@ -1224,22 +1003,19 @@ describe('annotation', function() { ...@@ -1224,22 +1003,19 @@ describe('annotation', function() {
}, },
}); });
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
assert.equal(controller.links().html, annotation.links.html);
assert.equal(controller.linkHTML, annotation.links.html);
}); });
it('linkInContext is blank when unknown', function () { it('in-context link is blank when unknown', function () {
var annotation = fixtures.defaultAnnotation(); var annotation = fixtures.defaultAnnotation();
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
assert.equal(controller.links().incontext, '');
assert.equal(controller.linkInContext, '');
}); });
it('linkHTML is blank when unknown', function () { it('HTML is blank when unknown', function () {
var annotation = fixtures.defaultAnnotation(); var annotation = fixtures.defaultAnnotation();
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
assert.equal(controller.links().html, '');
assert.equal(controller.linkHTML, '');
}); });
}); });
}); });
......
...@@ -22,20 +22,20 @@ ...@@ -22,20 +22,20 @@
target="_blank" ng-if="vm.group() && vm.group().url" href="{{vm.group().url}}"> target="_blank" ng-if="vm.group() && vm.group().url" href="{{vm.group().url}}">
<i class="h-icon-group"></i><span class="annotation-header__group-name">{{vm.group().name}}</span> <i class="h-icon-group"></i><span class="annotation-header__group-name">{{vm.group().name}}</span>
</a> </a>
<span ng-show="vm.isPrivate" <span ng-show="vm.state().isPrivate"
title="This annotation is visible only to you."> title="This annotation is visible only to you.">
<i class="h-icon-lock"></i><span class="annotation-header__group-name" ng-show="!vm.group().url">Only me</span> <i class="h-icon-lock"></i><span class="annotation-header__group-name" ng-show="!vm.group().url">Only me</span>
</span> </span>
<i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing()" title="This is a highlight. Click 'edit' to add a note or tag."></i> <i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing()" title="This is a highlight. Click 'edit' to add a note or tag."></i>
<span ng-if="::vm.showDocumentInfo"> <span ng-if="::vm.showDocumentInfo">
<span class="annotation-citation" ng-if="vm.documentMeta.titleLink"> <span class="annotation-citation" ng-if="vm.documentMeta().titleLink">
on "<a ng-href="{{vm.documentMeta.titleLink}}">{{vm.documentMeta.titleText}}</a>" on "<a ng-href="{{vm.documentMeta().titleLink}}">{{vm.documentMeta().titleText}}</a>"
</span> </span>
<span class="annotation-citation" ng-if="!vm.documentMeta.titleLink"> <span class="annotation-citation" ng-if="!vm.documentMeta().titleLink">
on "{{vm.documentMeta.titleText}}" on "{{vm.documentMeta().titleText}}"
</span> </span>
<span class="annotation-citation-domain" <span class="annotation-citation-domain"
ng-if="vm.documentMeta.domain">({{vm.documentMeta.domain}})</span> ng-if="vm.documentMeta().domain">({{vm.documentMeta().domain}})</span>
</span> </span>
</span> </span>
</span> </span>
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<timestamp <timestamp
class-name="'annotation-header__timestamp'" class-name="'annotation-header__timestamp'"
timestamp="vm.updated()" timestamp="vm.updated()"
href="vm.linkHTML" href="vm.links().html"
ng-if="!vm.editing() && vm.updated()"></timestamp> ng-if="!vm.editing() && vm.updated()"></timestamp>
</header> </header>
...@@ -75,8 +75,8 @@ ...@@ -75,8 +75,8 @@
collapse="vm.collapseBody" collapse="vm.collapseBody"
collapsed-height="400" collapsed-height="400"
overflow-hysteresis="20" overflow-hysteresis="20"
content-data="vm.form.text"> content-data="vm.state().text">
<markdown text="vm.form.text" <markdown text="vm.state().text"
on-edit-text="vm.setText(text)" on-edit-text="vm.setText(text)"
read-only="!vm.editing()"> read-only="!vm.editing()">
</markdown> </markdown>
...@@ -86,14 +86,14 @@ ...@@ -86,14 +86,14 @@
<!-- Tags --> <!-- Tags -->
<div class="annotation-body form-field" ng-if="vm.editing()"> <div class="annotation-body form-field" ng-if="vm.editing()">
<tag-editor tags="vm.form.tags" <tag-editor tags="vm.state().tags"
on-edit-tags="vm.setTags(tags)"></tag-editor> on-edit-tags="vm.setTags(tags)"></tag-editor>
</div> </div>
<div class="annotation-body u-layout-row tags tags-read-only" <div class="annotation-body u-layout-row tags tags-read-only"
ng-if="(vm.canCollapseBody || vm.form.tags.length) && !vm.editing()"> ng-if="(vm.canCollapseBody || vm.state().tags.length) && !vm.editing()">
<ul class="tag-list"> <ul class="tag-list">
<li class="tag-item" ng-repeat="tag in vm.form.tags"> <li class="tag-item" ng-repeat="tag in vm.state().tags">
<a ng-href="{{vm.tagStreamURL(tag)}}" target="_blank">{{tag}}</a> <a ng-href="{{vm.tagStreamURL(tag)}}" target="_blank">{{tag}}</a>
</li> </li>
</ul> </ul>
...@@ -170,8 +170,8 @@ ...@@ -170,8 +170,8 @@
</button> </button>
<annotation-share-dialog <annotation-share-dialog
group="vm.group()" group="vm.group()"
uri="vm.linkInContext" uri="vm.links().incontext"
is-private="vm.isPrivate" is-private="vm.state().isPrivate"
is-open="vm.showShareDialog" is-open="vm.showShareDialog"
on-close="vm.showShareDialog = false"> on-close="vm.showShareDialog = false">
</annotation-share-dialog> </annotation-share-dialog>
......
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