Commit c6423754 authored by Robert Knight's avatar Robert Knight

Begin to centralize UI state in annotationUI

This adds the initial infrastructure using Redux for centralizing the UI
state as an immutable object accessible via `annotationUI.getState()`
which is updated as a result of actions from the UI, network etc.

For background on why we want to do this, see the design overview at
https://github.com/hypothesis/h/pull/3176

Additionally this commit removes a couple of tests that checked for
non-mutation of the selected/focused annotation maps and uses
seamless-immutable instead which provides a better guarantee of this,
but only in debug builds.
parent 25d7a493
......@@ -5,25 +5,19 @@ var events = require('./events');
/** Watch the UI state and update scope properties. */
// @ngInject
function AnnotationUIController($rootScope, $scope, annotationUI) {
$rootScope.$watch(function () {
return annotationUI.selectedAnnotationMap;
}, function (map) {
map = map || {};
var count = Object.keys(map).length;
$scope.selectedAnnotationsCount = count;
annotationUI.subscribe(function () {
var state = annotationUI.getState();
if (count) {
$scope.selectedAnnotations = map;
$scope.selectedAnnotations = state.selectedAnnotationMap;
if (state.selectedAnnotationMap) {
$scope.selectedAnnotationsCount =
Object.keys(state.selectedAnnotationMap).length;
} else {
$scope.selectedAnnotations = null;
$scope.selectedAnnotationsCount = 0;
}
});
$rootScope.$watch(function () {
return annotationUI.focusedAnnotationMap;
}, function (map) {
map = map || {};
$scope.focusedAnnotations = map;
$scope.focusedAnnotations = state.focusedAnnotationMap;
});
$rootScope.$on(events.ANNOTATION_DELETED, function (event, annotation) {
......
......@@ -42,8 +42,8 @@ function AnnotationUISync($rootScope, $window, bridge, annotationSync,
if (typeof state !== 'boolean') {
state = true;
}
if (annotationUI.visibleHighlights !== state) {
annotationUI.visibleHighlights = state;
if (annotationUI.getState().visibleHighlights !== state) {
annotationUI.setShowHighlights(state);
bridge.call('setVisibleHighlights', state);
}
}
......@@ -72,7 +72,7 @@ function AnnotationUISync($rootScope, $window, bridge, annotationSync,
return;
} else {
// Synchronize the state of guests
channel.call('setVisibleHighlights', annotationUI.visibleHighlights);
channel.call('setVisibleHighlights', annotationUI.getState().visibleHighlights);
}
};
......
'use strict';
var immutable = require('seamless-immutable');
var redux = require('redux');
function value(selection) {
if (Object.keys(selection).length) {
return Object.freeze(selection);
return immutable(selection);
} else {
return null;
}
......@@ -16,33 +19,81 @@ function initialSelection(settings) {
return value(selection);
}
function initialState(settings) {
return Object.freeze({
visibleHighlights: false,
// Contains a map of annotation tag:true pairs.
focusedAnnotationMap: null,
// Contains a map of annotation id:true pairs.
selectedAnnotationMap: initialSelection(settings),
});
}
var types = {
SELECT_ANNOTATIONS: 'SELECT_ANNOTATIONS',
FOCUS_ANNOTATIONS: 'FOCUS_ANNOTATIONS',
SET_HIGHLIGHTS_VISIBLE: 'SET_HIGHLIGHTS_VISIBLE',
};
function reducer(state, action) {
switch (action.type) {
case types.SELECT_ANNOTATIONS:
return Object.assign({}, state, {selectedAnnotationMap: action.selection});
case types.FOCUS_ANNOTATIONS:
return Object.assign({}, state, {focusedAnnotationMap: action.focused});
case types.SET_HIGHLIGHTS_VISIBLE:
return Object.assign({}, state, {visibleHighlights: action.visible});
default:
return state;
}
}
/**
* Stores the UI state of the annotator in connected clients.
*
* This includes:
* - The set of annotations that are currently selected
* - The annotation(s) that are currently hovered/focused
* - The IDs of annotations that are currently selected or focused
* - The state of the bucket bar
*
*/
// @ngInject
module.exports = function (settings) {
var store = redux.createStore(reducer, initialState(settings));
function select(annotations) {
store.dispatch({
type: types.SELECT_ANNOTATIONS,
selection: value(annotations),
});
}
return {
visibleHighlights: false,
/**
* Return the current UI state of the sidebar. This should not be modified
* directly but only though the helper methods below.
*/
getState: store.getState,
// Contains a map of annotation tag:true pairs.
focusedAnnotationMap: null,
/** Listen for changes to the UI state of the sidebar. */
subscribe: store.subscribe,
// Contains a map of annotation id:true pairs.
selectedAnnotationMap: initialSelection(settings),
/**
* Sets whether annotation highlights in connected documents are shown
* or not.
*/
setShowHighlights: function (show) {
store.dispatch({
type: types.SET_HIGHLIGHTS_VISIBLE,
visible: show,
});
},
/**
* @ngdoc method
* @name annotationUI.focusedAnnotations
* @returns nothing
* @description Takes an array of annotations and uses them to set
* the focusedAnnotationMap.
* Sets which annotations are currently focused.
*
* @param {Array<Annotation>} annotations
*/
focusAnnotations: function (annotations) {
var selection = {};
......@@ -50,25 +101,24 @@ module.exports = function (settings) {
annotation = annotations[i];
selection[annotation.$$tag] = true;
}
this.focusedAnnotationMap = value(selection);
store.dispatch({
type: types.FOCUS_ANNOTATIONS,
focused: value(selection),
});
},
/**
* @ngdoc method
* @name annotationUI.hasSelectedAnnotations
* @returns true if there are any selected annotations.
* Return true if any annotations are currently selected.
*/
hasSelectedAnnotations: function () {
return !!this.selectedAnnotationMap;
return !!store.getState().selectedAnnotationMap;
},
/**
* @ngdoc method
* @name annotationUI.isAnnotationSelected
* @returns true if the provided annotation is selected.
* Returns true if the annotation with the given `id` is selected.
*/
isAnnotationSelected: function (id) {
return (this.selectedAnnotationMap || {}).hasOwnProperty(id);
return (store.getState().selectedAnnotationMap || {}).hasOwnProperty(id);
},
/**
......@@ -86,18 +136,12 @@ module.exports = function (settings) {
selection[annotations[i].id] = true;
}
}
this.selectedAnnotationMap = value(selection);
select(selection);
},
/**
* @ngdoc method
* @name annotationUI.xorSelectedAnnotations()
* @returns nothing
* @description takes an array of annotations and adds them to the
* selectedAnnotationMap if not present otherwise removes them.
*/
/** Toggle whether annotations are selected or not. */
xorSelectedAnnotations: function (annotations) {
var selection = Object.assign({}, this.selectedAnnotationMap);
var selection = Object.assign({}, store.getState().selectedAnnotationMap);
for (var i = 0, annotation; i < annotations.length; i++) {
annotation = annotations[i];
var id = annotation.id;
......@@ -107,31 +151,21 @@ module.exports = function (settings) {
selection[id] = true;
}
}
this.selectedAnnotationMap = value(selection);
select(selection);
},
/**
* @ngdoc method
* @name annotationUI.removeSelectedAnnotation()
* @returns nothing
* @description removes an annotation from the current selection.
*/
/** De-select an annotation. */
removeSelectedAnnotation: function (annotation) {
var selection = Object.assign({}, this.selectedAnnotationMap);
var selection = Object.assign({}, store.getState().selectedAnnotationMap);
if (selection) {
delete selection[annotation.id];
this.selectedAnnotationMap = value(selection);
select(selection);
}
},
/**
* @ngdoc method
* @name annotationUI.clearSelectedAnnotations()
* @returns nothing
* @description removes all annotations from the current selection.
*/
/** De-select all annotations. */
clearSelectedAnnotations: function () {
this.selectedAnnotationMap = null;
}
select({});
},
};
};
......@@ -2,6 +2,8 @@
var angular = require('angular');
var createFakeStore = require('./create-fake-store');
describe('AnnotationUIController', function () {
var $scope;
var $rootScope;
......@@ -22,11 +24,15 @@ describe('AnnotationUIController', function () {
$scope = $rootScope.$new();
$scope.search = {};
annotationUI = {
tool: 'comment',
var store = createFakeStore({
selectedAnnotationMap: null,
focusedAnnotationsMap: null,
removeSelectedAnnotation: sandbox.stub()
});
annotationUI = {
removeSelectedAnnotation: sandbox.stub(),
setState: store.setState,
getState: store.getState,
subscribe: store.subscribe,
};
$controller('AnnotationUIController', {
......@@ -40,27 +46,23 @@ describe('AnnotationUIController', function () {
});
it('updates the view when the selection changes', function () {
annotationUI.selectedAnnotationMap = { 1: true, 2: true };
$rootScope.$digest();
annotationUI.setState({selectedAnnotationMap: { 1: true, 2: true }});
assert.deepEqual($scope.selectedAnnotations, { 1: true, 2: true });
});
it('updates the selection counter when the selection changes', function () {
annotationUI.selectedAnnotationMap = { 1: true, 2: true };
$rootScope.$digest();
annotationUI.setState({selectedAnnotationMap: { 1: true, 2: true }});
assert.deepEqual($scope.selectedAnnotationsCount, 2);
});
it('clears the selection when no annotations are selected', function () {
annotationUI.selectedAnnotationMap = {};
$rootScope.$digest();
annotationUI.setState({selectedAnnotationMap: null});
assert.deepEqual($scope.selectedAnnotations, null);
assert.deepEqual($scope.selectedAnnotationsCount, 0);
});
it('updates the focused annotations when the focus map changes', function () {
annotationUI.focusedAnnotationMap = { 1: true, 2: true };
$rootScope.$digest();
annotationUI.setState({focusedAnnotationMap: { 1: true, 2: true }});
assert.deepEqual($scope.focusedAnnotations, { 1: true, 2: true });
});
......
......@@ -2,6 +2,8 @@
var angular = require('angular');
var createFakeStore = require('./create-fake-store');
describe('AnnotationUISync', function () {
var sandbox = sinon.sandbox.create();
var $digest;
......@@ -47,11 +49,13 @@ describe('AnnotationUISync', function () {
}
};
var store = createFakeStore({visibleHighlights: false});
fakeAnnotationUI = {
focusAnnotations: sandbox.stub(),
selectAnnotations: sandbox.stub(),
xorSelectedAnnotations: sandbox.stub(),
visibleHighlights: false,
setShowHighlights: sandbox.stub(),
getState: store.getState,
};
createAnnotationUISync = function () {
......@@ -141,10 +145,10 @@ describe('AnnotationUISync', function () {
});
describe('on "setVisibleHighlights" event', function () {
it('updates the annotationUI with the new value', function () {
it('updates the annotationUI state', function () {
createAnnotationUISync();
publish('setVisibleHighlights', true);
assert.equal(fakeAnnotationUI.visibleHighlights, true);
assert.calledWith(fakeAnnotationUI.setShowHighlights, true);
});
it('notifies the other frames of the change', function () {
......
......@@ -2,6 +2,8 @@
var annotationUIFactory = require('../annotation-ui');
var unroll = require('./util').unroll;
describe('annotationUI', function () {
var annotationUI;
......@@ -16,154 +18,147 @@ describe('annotationUI', function () {
it('sets the selection when settings.annotations is set', function () {
annotationUI = annotationUIFactory({annotations: 'testid'});
assert.deepEqual(annotationUI.selectedAnnotationMap, {
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
testid: true,
});
});
});
describe('.focusAnnotations()', function () {
describe('#setShowHighlights()', function () {
unroll('sets the visibleHighlights state flag to #state', function (testCase) {
annotationUI.setShowHighlights(testCase.state);
assert.equal(annotationUI.getState().visibleHighlights, testCase.state);
}, [
{state: true},
{state: false},
]);
});
describe('#subscribe()', function () {
it('notifies subscribers when the UI state changes', function () {
var listener = sinon.stub();
annotationUI.subscribe(listener);
annotationUI.focusAnnotations([{ $$tag: 1}]);
assert.called(listener);
});
});
describe('#focusAnnotations()', function () {
it('adds the passed annotations to the focusedAnnotationMap', function () {
annotationUI.focusAnnotations([{ $$tag: 1 }, { $$tag: 2 }, { $$tag: 3 }]);
assert.deepEqual(annotationUI.focusedAnnotationMap, {
assert.deepEqual(annotationUI.getState().focusedAnnotationMap, {
1: true, 2: true, 3: true
});
});
it('replaces any annotations originally in the map', function () {
annotationUI.focusedAnnotationMap = { 1: true };
annotationUI.focusAnnotations([{ $$tag: 1 }]);
annotationUI.focusAnnotations([{ $$tag: 2 }, { $$tag: 3 }]);
assert.deepEqual(annotationUI.focusedAnnotationMap, {
assert.deepEqual(annotationUI.getState().focusedAnnotationMap, {
2: true, 3: true
});
});
it('does not modify the original map object', function () {
var orig = annotationUI.focusedAnnotationMap = { 1: true };
annotationUI.focusAnnotations([{ $$tag: 2 }, { $$tag: 3 }]);
assert.notEqual(annotationUI.focusedAnnotationMap, orig);
});
it('nulls the map if no annotations are focused', function () {
annotationUI.focusedAnnotationMap = { $$tag: true };
annotationUI.focusAnnotations([{$$tag: 1}]);
annotationUI.focusAnnotations([]);
assert.isNull(annotationUI.focusedAnnotationMap);
assert.isNull(annotationUI.getState().focusedAnnotationMap);
});
});
describe('.hasSelectedAnnotations', function () {
describe('#hasSelectedAnnotations', function () {
it('returns true if there are any selected annotations', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{id: 1}]);
assert.isTrue(annotationUI.hasSelectedAnnotations());
});
it('returns false if there are no selected annotations', function () {
annotationUI.selectedAnnotationMap = null;
assert.isFalse(annotationUI.hasSelectedAnnotations());
});
});
describe('.isAnnotationSelected', function () {
describe('#isAnnotationSelected', function () {
it('returns true if the id provided is selected', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{id: 1}]);
assert.isTrue(annotationUI.isAnnotationSelected(1));
});
it('returns false if the id provided is not selected', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{id: 1}]);
assert.isFalse(annotationUI.isAnnotationSelected(2));
});
it('returns false if there are no selected annotations', function () {
annotationUI.selectedAnnotationMap = null;
assert.isFalse(annotationUI.isAnnotationSelected(1));
});
});
describe('.selectAnnotations()', function () {
describe('#selectAnnotations()', function () {
it('adds the passed annotations to the selectedAnnotationMap', function () {
annotationUI.selectAnnotations([{ id: 1 }, { id: 2 }, { id: 3 }]);
assert.deepEqual(annotationUI.selectedAnnotationMap, {
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
1: true, 2: true, 3: true
});
});
it('replaces any annotations originally in the map', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{ id:1 }]);
annotationUI.selectAnnotations([{ id: 2 }, { id: 3 }]);
assert.deepEqual(annotationUI.selectedAnnotationMap, {
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
2: true, 3: true
});
});
it('does not modify the original map object', function () {
var orig = annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{ id: 2 }, { id: 3 }]);
assert.notEqual(annotationUI.selectedAnnotationMap, orig);
});
it('nulls the map if no annotations are selected', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{id:1}]);
annotationUI.selectAnnotations([]);
assert.isNull(annotationUI.selectedAnnotationMap);
assert.isNull(annotationUI.getState().selectedAnnotationMap);
});
});
describe('.xorSelectedAnnotations()', function () {
describe('#xorSelectedAnnotations()', function () {
it('adds annotations missing from the selectedAnnotationMap', function () {
annotationUI.selectedAnnotationMap = { 1: true, 2: true };
annotationUI.selectAnnotations([{ id: 1 }, { id: 2}]);
annotationUI.xorSelectedAnnotations([{ id: 3 }, { id: 4 }]);
assert.deepEqual(annotationUI.selectedAnnotationMap, {
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
1: true, 2: true, 3: true, 4: true
});
});
it('removes annotations already in the selectedAnnotationMap', function () {
annotationUI.selectedAnnotationMap = { 1: true, 3: true };
annotationUI.selectAnnotations([{id: 1}, {id: 3}]);
annotationUI.xorSelectedAnnotations([{ id: 1 }, { id: 2 }]);
assert.deepEqual(annotationUI.selectedAnnotationMap, { 2: true, 3: true });
});
it('does not modify the original map object', function () {
var orig = annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.xorSelectedAnnotations([{ id: 2 }, { id: 3 }]);
assert.notEqual(annotationUI.selectedAnnotationMap, orig);
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, { 2: true, 3: true });
});
it('nulls the map if no annotations are selected', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{id: 1}]);
annotationUI.xorSelectedAnnotations([{ id: 1 }]);
assert.isNull(annotationUI.selectedAnnotationMap);
assert.isNull(annotationUI.getState().selectedAnnotationMap);
});
});
describe('.removeSelectedAnnotation', function () {
describe('#removeSelectedAnnotation()', function () {
it('removes an annotation from the selectedAnnotationMap', function () {
annotationUI.selectedAnnotationMap = { 1: true, 2: true, 3: true };
annotationUI.selectAnnotations([{id: 1}, {id: 2}, {id: 3}]);
annotationUI.removeSelectedAnnotation({ id: 2 });
assert.deepEqual(annotationUI.selectedAnnotationMap, {
assert.deepEqual(annotationUI.getState().selectedAnnotationMap, {
1: true, 3: true
});
});
it('does not modify the original map object', function () {
var orig = annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.removeSelectedAnnotation({ id: 1 });
assert.notEqual(annotationUI.selectedAnnotationMap, orig);
});
it('nulls the map if no annotations are selected', function () {
annotationUI.selectedAnnotationMap = { 1: true };
annotationUI.selectAnnotations([{id: 1}]);
annotationUI.removeSelectedAnnotation({ id: 1 });
assert.isNull(annotationUI.selectedAnnotationMap);
assert.isNull(annotationUI.getState().selectedAnnotationMap);
});
});
describe('.clearSelectedAnnotations', function () {
describe('#clearSelectedAnnotations()', function () {
it('removes all annotations from the selection', function () {
annotationUI.selectedAnnotationMap = { 1: true, 2: true, 3: true };
annotationUI.selectAnnotations([{id: 1}]);
annotationUI.clearSelectedAnnotations();
assert.isNull(annotationUI.selectedAnnotationMap);
assert.isNull(annotationUI.getState().selectedAnnotationMap);
});
});
});
'use strict';
var redux = require('redux');
function reducer(state, action) {
if (action.type === 'STATE_UPDATE') {
return Object.assign({}, state, action.update);
} else {
return state;
}
}
/**
* Creates a fake Redux store for use in tests.
*
* Unlike a normal Redux store where the user provides a function that
* transforms the state in response to actions and calls dispatch() to update
* the state when actions occur, this store has a setState() method for
* replacing state fields directly.
*/
function createFakeStore(initialState) {
var store = redux.createStore(reducer, initialState);
store.setState = function (update) {
store.dispatch({
type: 'STATE_UPDATE',
update: update,
});
};
return store;
}
module.exports = createFakeStore;
......@@ -5,6 +5,7 @@ var inherits = require('inherits');
var proxyquire = require('proxyquire');
var EventEmitter = require('tiny-emitter');
var createFakeStore = require('./create-fake-store');
var events = require('../events');
var noCallThru = require('./util').noCallThru;
......@@ -62,12 +63,26 @@ describe('WidgetController', function () {
unloadAnnotations: sandbox.spy()
};
var store = createFakeStore({selectedAnnotationMap: {}});
fakeAnnotationUI = {
clearSelectedAnnotations: sandbox.spy(),
selectedAnnotationMap: {},
hasSelectedAnnotations: function () {
return !!this.selectedAnnotationMap &&
Object.keys(this.selectedAnnotationMap).length > 0;
return !!this.getState().selectedAnnotationMap &&
Object.keys(this.getState().selectedAnnotationMap).length > 0;
},
getState: store.getState,
subscribe: store.subscribe,
_select: function (ids) {
if (!ids.length) {
store.setState({selectedAnnotationMap: null});
return;
}
store.setState({
selectedAnnotationMap: ids.reduce(function (map, id) {
map[id] = true;
return map;
}, {}),
});
},
};
fakeCrossFrame = {
......@@ -172,7 +187,7 @@ describe('WidgetController', function () {
beforeEach(function () {
fakeCrossFrame.frames = [{uri: uri}];
fakeAnnotationUI.selectedAnnotationMap[id] = true;
fakeAnnotationUI._select([id]);
$scope.$digest();
});
......@@ -217,7 +232,7 @@ describe('WidgetController', function () {
beforeEach(function () {
fakeCrossFrame.frames = [{uri: uri}];
fakeAnnotationUI.selectedAnnotationMap[id] = true;
fakeAnnotationUI._select([id]);
fakeGroups.focused = function () { return { id: 'private-group' }; };
$scope.$digest();
});
......@@ -233,7 +248,7 @@ describe('WidgetController', function () {
describe('when an annotation is anchored', function () {
it('focuses and scrolls to the annotation if already selected', function () {
var uri = 'http://example.com';
fakeAnnotationUI.selectedAnnotationMap = {'123': true};
fakeAnnotationUI._select(['123']);
fakeCrossFrame.frames.push({uri: uri});
var annot = {
$$tag: 'atag',
......@@ -296,21 +311,21 @@ describe('WidgetController', function () {
describe('direct linking messages', function () {
it('displays a message if the selection is unavailable', function () {
fakeAnnotationUI.selectedAnnotationMap = {'missing': true};
fakeAnnotationUI._select(['missing']);
fakeThreading.idTable = {'123': {}};
$scope.$digest();
assert.isTrue($scope.selectedAnnotationUnavailable());
});
it('does not show a message if the selection is available', function () {
fakeAnnotationUI.selectedAnnotationMap = {'123': true};
fakeAnnotationUI._select(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable());
});
it('does not a show a message if there is no selection', function () {
fakeAnnotationUI.selectedAnnotationMap = null;
fakeAnnotationUI._select([]);
$scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable());
});
......@@ -319,7 +334,7 @@ describe('WidgetController', function () {
$scope.auth = {
status: 'signed-out'
};
fakeAnnotationUI.selectedAnnotationMap = {'123': true};
fakeAnnotationUI._select(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest();
assert.isTrue($scope.shouldShowLoggedOutMessage());
......@@ -329,7 +344,7 @@ describe('WidgetController', function () {
$scope.auth = {
status: 'signed-out'
};
fakeAnnotationUI.selectedAnnotationMap = {'missing': true};
fakeAnnotationUI._select(['missing']);
fakeThreading.idTable = {'123': {}};
$scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage());
......@@ -339,7 +354,7 @@ describe('WidgetController', function () {
$scope.auth = {
status: 'signed-out'
};
fakeAnnotationUI.selectedAnnotationMap = null;
fakeAnnotationUI._select([]);
$scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage());
});
......@@ -348,7 +363,7 @@ describe('WidgetController', function () {
$scope.auth = {
status: 'signed-in'
};
fakeAnnotationUI.selectedAnnotationMap = {'123': true};
fakeAnnotationUI._select(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage());
......@@ -359,7 +374,7 @@ describe('WidgetController', function () {
status: 'signed-out'
};
delete fakeSettings.annotations;
fakeAnnotationUI.selectedAnnotationMap = {'123': true};
fakeAnnotationUI._select(['123']);
fakeThreading.idTable = {'123': {}};
$scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage());
......
......@@ -58,8 +58,8 @@ module.exports = function WidgetController(
* not the order in which they appear in the document.
*/
function firstSelectedAnnotation() {
if (annotationUI.selectedAnnotationMap) {
var id = Object.keys(annotationUI.selectedAnnotationMap)[0];
if (annotationUI.getState().selectedAnnotationMap) {
var id = Object.keys(annotationUI.getState().selectedAnnotationMap)[0];
return threading.idTable[id] && threading.idTable[id].message;
} else {
return null;
......@@ -88,8 +88,8 @@ module.exports = function WidgetController(
if (annotationUI.hasSelectedAnnotations()) {
// Focus the group containing the selected annotation and filter
// annotations to those from this group
var groupID = groupIDFromSelection(annotationUI.selectedAnnotationMap,
results);
var groupID = groupIDFromSelection(
annotationUI.getState().selectedAnnotationMap, results);
if (!groupID) {
// If the selected annotation is not available, fall back to
// loading annotations for the currently focused group
......@@ -200,16 +200,20 @@ module.exports = function WidgetController(
$scope.scrollTo = scrollToAnnotation;
$scope.hasFocus = function (annotation) {
if (!annotation || !$scope.focusedAnnotations) {
if (!annotation || !annotationUI.getState().focusedAnnotationMap) {
return false;
}
return annotation.$$tag in $scope.focusedAnnotations;
return annotation.$$tag in annotationUI.getState().focusedAnnotationMap;
};
function selectedID() {
return firstKey(annotationUI.getState().selectedAnnotationMap);
}
$scope.selectedAnnotationUnavailable = function () {
return !isLoading() &&
annotationUI.hasSelectedAnnotations() &&
!threading.idTable[firstKey(annotationUI.selectedAnnotationMap)];
!!selectedID() &&
!threading.idTable[selectedID()];
};
$scope.shouldShowLoggedOutMessage = function () {
......@@ -228,8 +232,8 @@ module.exports = function WidgetController(
// annotation. If there is an annotation selection and that
// selection is available to the user, show the CTA.
return !isLoading() &&
annotationUI.hasSelectedAnnotations() &&
!!threading.idTable[firstKey(annotationUI.selectedAnnotationMap)];
!!selectedID() &&
!!threading.idTable[selectedID()];
};
$scope.isLoading = isLoading;
......
......@@ -55,8 +55,10 @@
"query-string": "^3.0.1",
"raf": "^3.1.0",
"raven-js": "^2.0.2",
"redux": "^3.5.2",
"retry": "^0.8.0",
"scroll-into-view": "^1.3.1",
"seamless-immutable": "^6.0.1",
"showdown": "^1.2.1",
"stringify": "^5.1.0",
"through2": "^2.0.1",
......
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