Commit 796ecdfe authored by Robert Knight's avatar Robert Knight

Improve behavior when selecting pre-created annotations

Selecting a pre-created annotation in the page resulted in a
'You do not have permission to see this annotation' being displayed
because the selection mechanism only supports annotations which have a
server-assigned ID.

This commit improves that behavior by ignoring annotations without IDs
when selecting annotations.

It also fixes an issue in AnnotationUISync that prevents Annotation
objects being replaced in the UI state after they are updated on the
server. Previously AnnotationUISync would receive the tags of
annotations that had been selected in the page and would then look up
annotations in a cache maintained by AnnotationSync in order to get
Annotation objects that could be passed to `annotationUI` functions
which needed objects with an ID.

Fixes #3547
parent 9f00dfa3
...@@ -53,11 +53,6 @@ module.exports = class AnnotationSync ...@@ -53,11 +53,6 @@ module.exports = class AnnotationSync
this._syncCache(channel) this._syncCache(channel)
@bridge.onConnect(onConnect) @bridge.onConnect(onConnect)
# Provide a public interface to the annotation cache so that other
# sync services can lookup annotations by tag.
getAnnotationForTag: (tag) ->
@cache[tag] or null
sync: (annotations) -> sync: (annotations) ->
annotations = (this._format a for a in annotations) annotations = (this._format a for a in annotations)
@bridge.call 'sync', annotations, (err, annotations = []) => @bridge.call 'sync', annotations, (err, annotations = []) =>
......
...@@ -21,7 +21,7 @@ function AnnotationUIController($rootScope, $scope, annotationUI) { ...@@ -21,7 +21,7 @@ function AnnotationUIController($rootScope, $scope, annotationUI) {
}); });
$rootScope.$on(events.ANNOTATION_DELETED, function (event, annotation) { $rootScope.$on(events.ANNOTATION_DELETED, function (event, annotation) {
annotationUI.removeSelectedAnnotation(annotation); annotationUI.removeSelectedAnnotation(annotation.id);
}); });
} }
......
...@@ -9,7 +9,6 @@ var uiConstants = require('./ui-constants'); ...@@ -9,7 +9,6 @@ var uiConstants = require('./ui-constants');
* @name AnnotationUISync * @name AnnotationUISync
* @param {$window} $window An Angular window service. * @param {$window} $window An Angular window service.
* @param {Bridge} bridge * @param {Bridge} bridge
* @param {AnnotationSync} annotationSync
* @param {AnnotationUI} annotationUI An instance of the AnnotatonUI service * @param {AnnotationUI} annotationUI An instance of the AnnotatonUI service
* @description * @description
* Listens for incoming events over the bridge concerning the annotation * Listens for incoming events over the bridge concerning the annotation
...@@ -17,29 +16,36 @@ var uiConstants = require('./ui-constants'); ...@@ -17,29 +16,36 @@ var uiConstants = require('./ui-constants');
* that the messages are broadcast out to other frames. * that the messages are broadcast out to other frames.
*/ */
// @ngInject // @ngInject
function AnnotationUISync($rootScope, $window, bridge, annotationSync, function AnnotationUISync($rootScope, $window, annotationUI, bridge) {
annotationUI) { function lookupByTag(tag) {
// Retrieves annotations from the annotationSync cache. return annotationUI.getState().annotations.find(function (annot) {
var getAnnotationsByTags = function (tags) { return annot.$$tag === tag;
return tags.map(annotationSync.getAnnotationForTag, annotationSync); });
}; }
function findIDsForTags(tags) {
var ids = [];
tags.forEach(function (tag) {
var annot = lookupByTag(tag);
if (annot && annot.id) {
ids.push(annot.id);
}
});
return ids;
}
var channelListeners = { var channelListeners = {
showAnnotations: function (tags) { showAnnotations: function (tags) {
tags = tags || []; var ids = findIDsForTags(tags || []);
var annotations = getAnnotationsByTags(tags); annotationUI.selectAnnotations(ids);
annotationUI.selectAnnotations(annotations);
annotationUI.selectTab(uiConstants.TAB_ANNOTATIONS); annotationUI.selectTab(uiConstants.TAB_ANNOTATIONS);
}, },
focusAnnotations: function (tags) { focusAnnotations: function (tags) {
tags = tags || []; annotationUI.focusAnnotations(tags || []);
var annotations = getAnnotationsByTags(tags);
annotationUI.focusAnnotations(annotations);
}, },
toggleAnnotationSelection: function (tags) { toggleAnnotationSelection: function (tags) {
tags = tags || []; var ids = findIDsForTags(tags || []);
var annotations = getAnnotationsByTags(tags); annotationUI.toggleSelectedAnnotations(ids);
annotationUI.toggleSelectedAnnotations(annotations);
}, },
setVisibleHighlights: function (state) { setVisibleHighlights: function (state) {
if (typeof state !== 'boolean') { if (typeof state !== 'boolean') {
......
...@@ -20,6 +20,13 @@ function freeze(selection) { ...@@ -20,6 +20,13 @@ function freeze(selection) {
} }
} }
function toSet(list) {
return list.reduce(function (set, key) {
set[key] = true;
return set;
}, {});
}
function initialSelection(settings) { function initialSelection(settings) {
var selection = {}; var selection = {};
if (settings.annotations) { if (settings.annotations) {
...@@ -197,17 +204,12 @@ module.exports = function (settings) { ...@@ -197,17 +204,12 @@ module.exports = function (settings) {
/** /**
* Sets which annotations are currently focused. * Sets which annotations are currently focused.
* *
* @param {Array<Annotation>} annotations * @param {Array<string>} Tags of annotations to focus
*/ */
focusAnnotations: function (annotations) { focusAnnotations: function (tags) {
var selection = {};
for (var i = 0, annotation; i < annotations.length; i++) {
annotation = annotations[i];
selection[annotation.$$tag] = true;
}
store.dispatch({ store.dispatch({
type: types.FOCUS_ANNOTATIONS, type: types.FOCUS_ANNOTATIONS,
focused: freeze(selection), focused: freeze(toSet(tags)),
}); });
}, },
...@@ -258,28 +260,16 @@ module.exports = function (settings) { ...@@ -258,28 +260,16 @@ module.exports = function (settings) {
/** /**
* Set the currently selected annotation IDs. * Set the currently selected annotation IDs.
*
* @param {Array<string|{id:string}>} annotations - Annotations or IDs
* of annotations to select.
*/ */
selectAnnotations: function (annotations) { selectAnnotations: function (ids) {
var selection = {}; select(toSet(ids));
for (var i = 0; i < annotations.length; i++) {
if (typeof annotations[i] === 'string') {
selection[annotations[i]] = true;
} else {
selection[annotations[i].id] = true;
}
}
select(selection);
}, },
/** Toggle whether annotations are selected or not. */ /** Toggle whether annotations are selected or not. */
toggleSelectedAnnotations: function (annotations) { toggleSelectedAnnotations: function (ids) {
var selection = Object.assign({}, store.getState().selectedAnnotationMap); var selection = Object.assign({}, store.getState().selectedAnnotationMap);
for (var i = 0, annotation; i < annotations.length; i++) { for (var i = 0; i < ids.length; i++) {
annotation = annotations[i]; var id = ids[i];
var id = annotation.id;
if (selection[id]) { if (selection[id]) {
delete selection[id]; delete selection[id];
} else { } else {
...@@ -290,12 +280,13 @@ module.exports = function (settings) { ...@@ -290,12 +280,13 @@ module.exports = function (settings) {
}, },
/** De-select an annotation. */ /** De-select an annotation. */
removeSelectedAnnotation: function (annotation) { removeSelectedAnnotation: function (id) {
var selection = Object.assign({}, store.getState().selectedAnnotationMap); var selection = Object.assign({}, store.getState().selectedAnnotationMap);
if (selection) { if (!selection || !id) {
delete selection[annotation.id]; return;
select(selection);
} }
delete selection[id];
select(selection);
}, },
/** De-select all annotations. */ /** De-select all annotations. */
......
...@@ -39,7 +39,7 @@ module.exports = class CrossFrame ...@@ -39,7 +39,7 @@ module.exports = class CrossFrame
new AnnotationSync(bridge, options) new AnnotationSync(bridge, options)
createAnnotationUISync = (annotationSync) -> createAnnotationUISync = (annotationSync) ->
new AnnotationUISync($rootScope, $window, bridge, annotationSync, annotationUI) new AnnotationUISync($rootScope, $window, annotationUI, bridge)
addFrame = (channel) => addFrame = (channel) =>
channel.call 'getDocumentInfo', (err, info) => channel.call 'getDocumentInfo', (err, info) =>
......
...@@ -58,20 +58,6 @@ describe 'AnnotationSync', -> ...@@ -58,20 +58,6 @@ describe 'AnnotationSync', ->
assert.notCalled(channel.call) assert.notCalled(channel.call)
describe '.getAnnotationForTag', ->
it 'returns the annotation if present in the cache', ->
ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync()
annSync.cache['tag1'] = ann
cached = annSync.getAnnotationForTag('tag1')
assert.equal(cached, ann)
it 'returns null if not present in the cache', ->
annSync = createAnnotationSync()
cached = annSync.getAnnotationForTag('tag1')
assert.isNull(cached)
describe 'channel event handlers', -> describe 'channel event handlers', ->
assertBroadcast = (channelEvent, publishEvent) -> assertBroadcast = (channelEvent, publishEvent) ->
it 'broadcasts the "' + publishEvent + '" event over the local event bus', -> it 'broadcasts the "' + publishEvent + '" event over the local event bus', ->
......
...@@ -53,10 +53,7 @@ describe('AnnotationUIController', function () { ...@@ -53,10 +53,7 @@ describe('AnnotationUIController', function () {
}); });
it('updates the focused annotations when the focus map changes', function () { it('updates the focused annotations when the focus map changes', function () {
annotationUI.focusAnnotations([ annotationUI.focusAnnotations(['1', '2']);
{$$tag: '1'},
{$$tag: '2'},
]);
assert.deepEqual($scope.focusedAnnotations, { 1: true, 2: true }); assert.deepEqual($scope.focusedAnnotations, { 1: true, 2: true });
}); });
......
...@@ -10,7 +10,6 @@ describe('AnnotationUISync', function () { ...@@ -10,7 +10,6 @@ describe('AnnotationUISync', function () {
var publish; var publish;
var fakeBridge; var fakeBridge;
var annotationUI; var annotationUI;
var fakeAnnotationSync;
var createAnnotationUISync; var createAnnotationUISync;
var createChannel = function () { var createChannel = function () {
return { call: sandbox.stub() }; return { call: sandbox.stub() };
...@@ -43,20 +42,15 @@ describe('AnnotationUISync', function () { ...@@ -43,20 +42,15 @@ describe('AnnotationUISync', function () {
] ]
}; };
fakeAnnotationSync = {
getAnnotationForTag: function (tag) {
return {
id: Number(tag.replace('tag', '')),
$$tag: tag,
};
}
};
annotationUI = annotationUIFactory({}); annotationUI = annotationUIFactory({});
annotationUI.addAnnotations([
{id: 'id1', $$tag: 'tag1'},
{id: 'id2', $$tag: 'tag2'},
{id: 'id3', $$tag: 'tag3'},
]);
createAnnotationUISync = function () { createAnnotationUISync = function () {
new AnnotationUISync( new AnnotationUISync(
$rootScope, fakeWindow, fakeBridge, fakeAnnotationSync, $rootScope, fakeWindow, annotationUI, fakeBridge
annotationUI
); );
}; };
})); }));
...@@ -89,14 +83,18 @@ describe('AnnotationUISync', function () { ...@@ -89,14 +83,18 @@ describe('AnnotationUISync', function () {
}); });
describe('on "showAnnotations" event', function () { describe('on "showAnnotations" event', function () {
it('updates the annotationUI to include the shown annotations', function () { it('selects annotations which have an ID', function () {
createAnnotationUISync(); createAnnotationUISync();
annotationUI.selectAnnotations = sinon.stub();
publish('showAnnotations', ['tag1', 'tag2', 'tag3']); publish('showAnnotations', ['tag1', 'tag2', 'tag3']);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { assert.calledWith(annotationUI.selectAnnotations, ['id1', 'id2', 'id3']);
1: true, });
2: true,
3: true, it('does not select annotations which do not have an ID', function () {
}); createAnnotationUISync();
annotationUI.selectAnnotations = sinon.stub();
publish('showAnnotations', ['tag1', 'tag-for-a-new-annotation']);
assert.calledWith(annotationUI.selectAnnotations, ['id1']);
}); });
it('triggers a digest', function () { it('triggers a digest', function () {
...@@ -107,7 +105,7 @@ describe('AnnotationUISync', function () { ...@@ -107,7 +105,7 @@ describe('AnnotationUISync', function () {
}); });
describe('on "focusAnnotations" event', function () { describe('on "focusAnnotations" event', function () {
it('updates the annotationUI to show the provided annotations', function () { it('focuses the annotations', function () {
createAnnotationUISync(); createAnnotationUISync();
publish('focusAnnotations', ['tag1', 'tag2', 'tag3']); publish('focusAnnotations', ['tag1', 'tag2', 'tag3']);
assert.deepEqual(annotationUI.getState().focusedAnnotationMap, { assert.deepEqual(annotationUI.getState().focusedAnnotationMap, {
...@@ -125,14 +123,11 @@ describe('AnnotationUISync', function () { ...@@ -125,14 +123,11 @@ describe('AnnotationUISync', function () {
}); });
describe('on "toggleAnnotationSelection" event', function () { describe('on "toggleAnnotationSelection" event', function () {
it('updates the annotationUI to show the provided annotations', function () { it('toggles the selected state of the annotations', function () {
createAnnotationUISync(); createAnnotationUISync();
annotationUI.toggleSelectedAnnotations = sinon.stub();
publish('toggleAnnotationSelection', ['tag1', 'tag2', 'tag3']); publish('toggleAnnotationSelection', ['tag1', 'tag2', 'tag3']);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { assert.calledWith(annotationUI.toggleSelectedAnnotations, ['id1', 'id2', 'id3']);
1: true,
2: true,
3: true,
});
}); });
it('triggers a digest', function () { it('triggers a digest', function () {
......
...@@ -116,22 +116,22 @@ describe('annotationUI', function () { ...@@ -116,22 +116,22 @@ describe('annotationUI', function () {
describe('#focusAnnotations()', function () { describe('#focusAnnotations()', function () {
it('adds the passed annotations to the focusedAnnotationMap', function () { it('adds the passed annotations to the focusedAnnotationMap', function () {
annotationUI.focusAnnotations([{ $$tag: 1 }, { $$tag: 2 }, { $$tag: 3 }]); annotationUI.focusAnnotations([1, 2, 3]);
assert.deepEqual(annotationUI.getState().focusedAnnotationMap, { assert.deepEqual(annotationUI.getState().focusedAnnotationMap, {
1: true, 2: true, 3: true 1: true, 2: true, 3: true
}); });
}); });
it('replaces any annotations originally in the map', function () { it('replaces any annotations originally in the map', function () {
annotationUI.focusAnnotations([{ $$tag: 1 }]); annotationUI.focusAnnotations([1]);
annotationUI.focusAnnotations([{ $$tag: 2 }, { $$tag: 3 }]); annotationUI.focusAnnotations([2, 3]);
assert.deepEqual(annotationUI.getState().focusedAnnotationMap, { assert.deepEqual(annotationUI.getState().focusedAnnotationMap, {
2: true, 3: true 2: true, 3: true
}); });
}); });
it('nulls the map if no annotations are focused', function () { it('nulls the map if no annotations are focused', function () {
annotationUI.focusAnnotations([{$$tag: 1}]); annotationUI.focusAnnotations([1]);
annotationUI.focusAnnotations([]); annotationUI.focusAnnotations([]);
assert.isNull(annotationUI.getState().focusedAnnotationMap); assert.isNull(annotationUI.getState().focusedAnnotationMap);
}); });
...@@ -139,7 +139,7 @@ describe('annotationUI', function () { ...@@ -139,7 +139,7 @@ describe('annotationUI', function () {
describe('#hasSelectedAnnotations', function () { describe('#hasSelectedAnnotations', function () {
it('returns true if there are any selected annotations', function () { it('returns true if there are any selected annotations', function () {
annotationUI.selectAnnotations([{id: 1}]); annotationUI.selectAnnotations([1]);
assert.isTrue(annotationUI.hasSelectedAnnotations()); assert.isTrue(annotationUI.hasSelectedAnnotations());
}); });
...@@ -150,12 +150,12 @@ describe('annotationUI', function () { ...@@ -150,12 +150,12 @@ describe('annotationUI', function () {
describe('#isAnnotationSelected', function () { describe('#isAnnotationSelected', function () {
it('returns true if the id provided is selected', function () { it('returns true if the id provided is selected', function () {
annotationUI.selectAnnotations([{id: 1}]); annotationUI.selectAnnotations([1]);
assert.isTrue(annotationUI.isAnnotationSelected(1)); assert.isTrue(annotationUI.isAnnotationSelected(1));
}); });
it('returns false if the id provided is not selected', function () { it('returns false if the id provided is not selected', function () {
annotationUI.selectAnnotations([{id: 1}]); annotationUI.selectAnnotations([1]);
assert.isFalse(annotationUI.isAnnotationSelected(2)); assert.isFalse(annotationUI.isAnnotationSelected(2));
}); });
...@@ -166,22 +166,22 @@ describe('annotationUI', function () { ...@@ -166,22 +166,22 @@ describe('annotationUI', function () {
describe('#selectAnnotations()', function () { describe('#selectAnnotations()', function () {
it('adds the passed annotations to the selectedAnnotationMap', function () { it('adds the passed annotations to the selectedAnnotationMap', function () {
annotationUI.selectAnnotations([{ id: 1 }, { id: 2 }, { id: 3 }]); annotationUI.selectAnnotations([1, 2, 3]);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
1: true, 2: true, 3: true 1: true, 2: true, 3: true
}); });
}); });
it('replaces any annotations originally in the map', function () { it('replaces any annotations originally in the map', function () {
annotationUI.selectAnnotations([{ id:1 }]); annotationUI.selectAnnotations([1]);
annotationUI.selectAnnotations([{ id: 2 }, { id: 3 }]); annotationUI.selectAnnotations([2, 3]);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
2: true, 3: true 2: true, 3: true
}); });
}); });
it('nulls the map if no annotations are selected', function () { it('nulls the map if no annotations are selected', function () {
annotationUI.selectAnnotations([{id:1}]); annotationUI.selectAnnotations([1]);
annotationUI.selectAnnotations([]); annotationUI.selectAnnotations([]);
assert.isNull(annotationUI.getState().selectedAnnotationMap); assert.isNull(annotationUI.getState().selectedAnnotationMap);
}); });
...@@ -189,45 +189,45 @@ describe('annotationUI', function () { ...@@ -189,45 +189,45 @@ describe('annotationUI', function () {
describe('#toggleSelectedAnnotations()', function () { describe('#toggleSelectedAnnotations()', function () {
it('adds annotations missing from the selectedAnnotationMap', function () { it('adds annotations missing from the selectedAnnotationMap', function () {
annotationUI.selectAnnotations([{ id: 1 }, { id: 2}]); annotationUI.selectAnnotations([1, 2]);
annotationUI.toggleSelectedAnnotations([{ id: 3 }, { id: 4 }]); annotationUI.toggleSelectedAnnotations([3, 4]);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
1: true, 2: true, 3: true, 4: true 1: true, 2: true, 3: true, 4: true
}); });
}); });
it('removes annotations already in the selectedAnnotationMap', function () { it('removes annotations already in the selectedAnnotationMap', function () {
annotationUI.selectAnnotations([{id: 1}, {id: 3}]); annotationUI.selectAnnotations([1, 3]);
annotationUI.toggleSelectedAnnotations([{ id: 1 }, { id: 2 }]); annotationUI.toggleSelectedAnnotations([1, 2]);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { 2: true, 3: true }); assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { 2: true, 3: true });
}); });
it('nulls the map if no annotations are selected', function () { it('nulls the map if no annotations are selected', function () {
annotationUI.selectAnnotations([{id: 1}]); annotationUI.selectAnnotations([1]);
annotationUI.toggleSelectedAnnotations([{ id: 1 }]); annotationUI.toggleSelectedAnnotations([1]);
assert.isNull(annotationUI.getState().selectedAnnotationMap); assert.isNull(annotationUI.getState().selectedAnnotationMap);
}); });
}); });
describe('#removeSelectedAnnotation()', function () { describe('#removeSelectedAnnotation()', function () {
it('removes an annotation from the selectedAnnotationMap', function () { it('removes an annotation from the selectedAnnotationMap', function () {
annotationUI.selectAnnotations([{id: 1}, {id: 2}, {id: 3}]); annotationUI.selectAnnotations([1, 2, 3]);
annotationUI.removeSelectedAnnotation({ id: 2 }); annotationUI.removeSelectedAnnotation(2);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
1: true, 3: true 1: true, 3: true
}); });
}); });
it('nulls the map if no annotations are selected', function () { it('nulls the map if no annotations are selected', function () {
annotationUI.selectAnnotations([{id: 1}]); annotationUI.selectAnnotations([1]);
annotationUI.removeSelectedAnnotation({ id: 1 }); annotationUI.removeSelectedAnnotation(1);
assert.isNull(annotationUI.getState().selectedAnnotationMap); assert.isNull(annotationUI.getState().selectedAnnotationMap);
}); });
}); });
describe('#clearSelectedAnnotations()', function () { describe('#clearSelectedAnnotations()', function () {
it('removes all annotations from the selection', function () { it('removes all annotations from the selection', function () {
annotationUI.selectAnnotations([{id: 1}]); annotationUI.selectAnnotations([1]);
annotationUI.clearSelectedAnnotations(); annotationUI.clearSelectedAnnotations();
assert.isNull(annotationUI.getState().selectedAnnotationMap); assert.isNull(annotationUI.getState().selectedAnnotationMap);
}); });
......
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