Commit f9ee271c authored by Robert Knight's avatar Robert Knight

Consolidate logic for annotation <-> tab assignment

Previously the logic for determining which tab an annotation should appear
in occurred both in `root-thread.js` where it was used to filter
annotations based on the selected tab, and in `widget-controller.js` where
it was used to switch to the tab containing a particular annotation.

Consolidate all the logic for deciding which tab an annotation should
appear in into a single module. This makes it easier to test that tab
assignment is correct with different combinations of feature flags and
annotation states and to add additional logic in future.
parent 6460fbc1
...@@ -49,6 +49,17 @@ function updateSession(session) { ...@@ -49,6 +49,17 @@ function updateSession(session) {
}; };
} }
/**
* Return true if a given feature flag is enabled.
*
* @param {object} state - The application state
* @param {string} feature - The name of the feature flag. This matches the
* name of the feature flag as declared in the Hypothesis service.
*/
function isFeatureEnabled(state, feature) {
return !!state.session.features[feature];
}
module.exports = { module.exports = {
init: init, init: init,
update: update, update: update,
...@@ -56,4 +67,7 @@ module.exports = { ...@@ -56,4 +67,7 @@ module.exports = {
actions: { actions: {
updateSession: updateSession, updateSession: updateSession,
}, },
// Selectors
isFeatureEnabled: isFeatureEnabled,
}; };
...@@ -4,6 +4,7 @@ var buildThread = require('./build-thread'); ...@@ -4,6 +4,7 @@ var buildThread = require('./build-thread');
var events = require('./events'); var events = require('./events');
var memoize = require('./util/memoize'); var memoize = require('./util/memoize');
var metadata = require('./annotation-metadata'); var metadata = require('./annotation-metadata');
var tabs = require('./tabs');
var uiConstants = require('./ui-constants'); var uiConstants = require('./ui-constants');
function truthyKeys(map) { function truthyKeys(map) {
...@@ -39,7 +40,7 @@ var sortFns = { ...@@ -39,7 +40,7 @@ var sortFns = {
* The root thread is then displayed by viewer.html * The root thread is then displayed by viewer.html
*/ */
// @ngInject // @ngInject
function RootThread($rootScope, annotationUI, drafts, features, searchFilter, viewFilter) { function RootThread($rootScope, annotationUI, drafts, searchFilter, viewFilter) {
/** /**
* Build the root conversation thread from the given UI state. * Build the root conversation thread from the given UI state.
...@@ -60,38 +61,16 @@ function RootThread($rootScope, annotationUI, drafts, features, searchFilter, vi ...@@ -60,38 +61,16 @@ function RootThread($rootScope, annotationUI, drafts, features, searchFilter, vi
var threadFilterFn; var threadFilterFn;
if (state.isSidebar && !state.filterQuery) { if (state.isSidebar && !state.filterQuery) {
if (!features.flagEnabled('orphans_tab')) {
threadFilterFn = function (thread) { threadFilterFn = function (thread) {
if (!thread.annotation) { if (!thread.annotation) {
return false; return false;
} }
if (state.selectedTab === uiConstants.TAB_NOTES) {
return metadata.isPageNote(thread.annotation); var separateOrphans = tabs.shouldSeparateOrphans(state);
} else { return tabs.shouldShowInTab(thread.annotation, state.selectedTab,
return metadata.isAnnotation(thread.annotation) || metadata.isOrphan(thread.annotation); separateOrphans);
}
};
} else {
threadFilterFn = function (thread) {
if (!thread.annotation) {
return false;
}
if (metadata.isWaitingToAnchor(thread.annotation)) {
return false;
}
switch (state.selectedTab) {
case uiConstants.TAB_ANNOTATIONS:
return metadata.isAnnotation(thread.annotation);
case uiConstants.TAB_ORPHANS:
return metadata.isOrphan(thread.annotation);
case uiConstants.TAB_NOTES:
return metadata.isPageNote(thread.annotation);
default:
throw new Error('Invalid selected tab');
}
}; };
} }
}
// Get the currently loaded annotations and the set of inputs which // Get the currently loaded annotations and the set of inputs which
// determines what is visible and build the visible thread structure // determines what is visible and build the visible thread structure
......
'use strict';
var metadata = require('./annotation-metadata');
var countIf = require('./util/array-util').countIf;
/**
* Return a count of the number of Annotations, Page Notes, Orphans and
* annotations still being anchored in a set of `annotations`
*/
function tabCounts(annotations, opts) {
opts = opts || {separateOrphans: false};
var counts = {
notes: countIf(annotations, metadata.isPageNote),
annotations: countIf(annotations, metadata.isAnnotation),
orphans: countIf(annotations, metadata.isOrphan),
anchoring: countIf(annotations, metadata.isWaitingToAnchor),
};
if (opts.separateOrphans) {
return counts;
} else {
return Object.assign({}, counts, {
annotations: counts.annotations + counts.orphans,
orphans: 0,
});
}
}
module.exports = tabCounts;
'use strict';
// Selectors that calculate the annotation counts displayed in tab headings
// and determine which tab an annotation should be displayed in.
var countIf = require('./util/array-util').countIf;
var metadata = require('./annotation-metadata');
var session = require('./reducers/session');
var uiConstants = require('./ui-constants');
/**
* Return true if Annotations and Orphans should be displayed in separate tabs.
*
* @param {object} state - The current application state.
*/
function shouldSeparateOrphans(state) {
return session.isFeatureEnabled(state, 'orphans_tab');
}
/**
* Return the tab in which an annotation should be displayed.
*
* @param {Annotation} ann
* @param {boolean} separateOrphans - True if orphans should be displayed in a
* separate tab.
*/
function tabForAnnotation(ann, separateOrphans) {
if (metadata.isOrphan(ann) && separateOrphans) {
return uiConstants.TAB_ORPHANS;
} else if (metadata.isPageNote(ann)) {
return uiConstants.TAB_NOTES;
} else {
return uiConstants.TAB_ANNOTATIONS;
}
}
/**
* Return true if an annotation should be displayed in a given tab.
*
* @param {Annotation} ann
* @param {number} tab - The TAB_* value indicating the tab
* @param {boolean} separateOrphans - True if orphans should be separated into
* their own tab.
*/
function shouldShowInTab(ann, tab, separateOrphans) {
if (metadata.isWaitingToAnchor(ann) && separateOrphans) {
// Until this annotation anchors or fails to anchor, we do not know which
// tab it should be displayed in.
return false;
}
return tabForAnnotation(ann, separateOrphans) === tab;
}
/**
* Return the counts for the headings of different tabs.
*
* @param {Annotation[]} annotations - List of annotations to display
* @param {boolean} separateOrphans - True if orphans should be displayed in a
* separate tab.
*/
function counts(annotations, separateOrphans) {
var counts = {
notes: countIf(annotations, metadata.isPageNote),
annotations: countIf(annotations, metadata.isAnnotation),
orphans: countIf(annotations, metadata.isOrphan),
anchoring: countIf(annotations, metadata.isWaitingToAnchor),
};
if (separateOrphans) {
return counts;
} else {
return Object.assign({}, counts, {
annotations: counts.annotations + counts.orphans,
orphans: 0,
});
}
}
module.exports = {
counts: counts,
shouldSeparateOrphans: shouldSeparateOrphans,
shouldShowInTab: shouldShowInTab,
tabForAnnotation: tabForAnnotation,
};
...@@ -27,7 +27,6 @@ describe('rootThread', function () { ...@@ -27,7 +27,6 @@ describe('rootThread', function () {
var fakeAnnotationUI; var fakeAnnotationUI;
var fakeBuildThread; var fakeBuildThread;
var fakeDrafts; var fakeDrafts;
var fakeFeatures;
var fakeSearchFilter; var fakeSearchFilter;
var fakeViewFilter; var fakeViewFilter;
...@@ -46,6 +45,9 @@ describe('rootThread', function () { ...@@ -46,6 +45,9 @@ describe('rootThread', function () {
highlighted: [], highlighted: [],
isSidebar: true, isSidebar: true,
selectedAnnotationMap: null, selectedAnnotationMap: null,
session: {
features: {'orphans_tab': true},
},
sortKey: 'Location', sortKey: 'Location',
sortKeysAvailable: ['Location'], sortKeysAvailable: ['Location'],
visibleHighlights: false, visibleHighlights: false,
...@@ -68,10 +70,6 @@ describe('rootThread', function () { ...@@ -68,10 +70,6 @@ describe('rootThread', function () {
remove: sinon.stub(), remove: sinon.stub(),
}; };
fakeFeatures = {
flagEnabled: sinon.stub().returns(true),
};
fakeSearchFilter = { fakeSearchFilter = {
generateFacetedFilter: sinon.stub(), generateFacetedFilter: sinon.stub(),
}; };
...@@ -83,7 +81,6 @@ describe('rootThread', function () { ...@@ -83,7 +81,6 @@ describe('rootThread', function () {
angular.module('app', []) angular.module('app', [])
.value('annotationUI', fakeAnnotationUI) .value('annotationUI', fakeAnnotationUI)
.value('drafts', fakeDrafts) .value('drafts', fakeDrafts)
.value('features', fakeFeatures)
.value('searchFilter', fakeSearchFilter) .value('searchFilter', fakeSearchFilter)
.value('viewFilter', fakeViewFilter) .value('viewFilter', fakeViewFilter)
.service('rootThread', proxyquire('../root-thread', { .service('rootThread', proxyquire('../root-thread', {
...@@ -276,22 +273,6 @@ describe('rootThread', function () { ...@@ -276,22 +273,6 @@ describe('rootThread', function () {
// pages, since we show all types of annotations here // pages, since we show all types of annotations here
assert.notOk(threadFilterFn); assert.notOk(threadFilterFn);
}); });
it('filter does not match annotation when it is still waiting to anchor', function () {
fakeBuildThread.reset();
fakeAnnotationUI.state = Object.assign({}, fakeAnnotationUI.state,
{selectedTab: uiConstants.TAB_ANNOTATIONS});
rootThread.thread(fakeAnnotationUI.state);
var threadFilterFn = fakeBuildThread.args[0][1].threadFilterFn;
var annotation = {
$orphan: undefined,
target: [{ selector: {} }],
};
assert.isFalse(threadFilterFn({annotation: annotation}));
});
}); });
describe('when the filter query changes', function () { describe('when the filter query changes', function () {
......
'use strict';
var fixtures = require('./annotation-fixtures');
var tabCounts = require('../tab-counts');
describe('tabCounts', function () {
var annotation = Object.assign(fixtures.defaultAnnotation(), {$orphan:false});
var orphan = Object.assign(fixtures.defaultAnnotation(), {$orphan:true});
it('counts Annotations and Orphans together when the Orphans tab is not enabled', function () {
assert.deepEqual(tabCounts([annotation, orphan]), {
anchoring: 0,
annotations: 2,
notes: 0,
orphans: 0,
});
});
it('counts Annotations and Orphans separately when the Orphans tab is enabled', function () {
assert.deepEqual(tabCounts([annotation, orphan], {separateOrphans: true}), {
anchoring: 0,
annotations: 1,
notes: 0,
orphans: 1,
});
});
});
'use strict';
var fixtures = require('./annotation-fixtures');
var uiConstants = require('../ui-constants');
var tabs = require('../tabs');
var unroll = require('./util').unroll;
describe('tabs', function () {
describe('shouldSeparateOrphans', function () {
it('returns true if the "orphans_tab" flag is enabled', function () {
var state = {
session: {
features: {'orphans_tab': true},
},
};
assert.isTrue(tabs.shouldSeparateOrphans(state));
});
it('returns false if the "orphans_tab" flag is not enabled', function () {
var state = {
session: {
features: {'orphans_tab': false},
},
};
assert.isFalse(tabs.shouldSeparateOrphans(state));
});
});
describe('tabForAnnotation', function () {
unroll('shows annotation in correct tab', function (testCase) {
var ann = testCase.ann;
var expectedTab = testCase.expectedTab;
assert.equal(tabs.tabForAnnotation(ann, true /* separateOrphans */), expectedTab);
}, [{
ann: fixtures.defaultAnnotation(),
expectedTab: uiConstants.TAB_ANNOTATIONS,
},{
ann: fixtures.oldPageNote(),
expectedTab: uiConstants.TAB_NOTES,
},{
ann: Object.assign(fixtures.defaultAnnotation(), {$orphan: true}),
expectedTab: uiConstants.TAB_ORPHANS,
}]);
});
describe('shouldShowInTab', function () {
unroll('returns true if the annotation should be shown', function (testCase) {
var ann = fixtures.defaultAnnotation();
ann.$orphan = testCase.orphan;
assert.equal(tabs.shouldShowInTab(ann, uiConstants.TAB_ANNOTATIONS,
testCase.separateOrphans), testCase.expectedTab === uiConstants.TAB_ANNOTATIONS);
assert.equal(tabs.shouldShowInTab(ann, uiConstants.TAB_ORPHANS,
testCase.separateOrphans), testCase.expectedTab === uiConstants.TAB_ORPHANS);
}, [{
orphan: undefined,
separateOrphans: false,
expectedTab: uiConstants.TAB_ANNOTATIONS,
},{
orphan: undefined,
separateOrphans: true,
expectedTab: null,
},{
orphan: false,
separateOrphans: true,
expectedTab: uiConstants.TAB_ANNOTATIONS,
},{
orphan: true,
separateOrphans: true,
expectedTab: uiConstants.TAB_ORPHANS,
}]);
});
describe('counts', function () {
var annotation = Object.assign(fixtures.defaultAnnotation(), {$orphan:false});
var orphan = Object.assign(fixtures.defaultAnnotation(), {$orphan:true});
it('counts Annotations and Orphans together when the Orphans tab is not enabled', function () {
assert.deepEqual(tabs.counts([annotation, orphan], false), {
anchoring: 0,
annotations: 2,
notes: 0,
orphans: 0,
});
});
it('counts Annotations and Orphans separately when the Orphans tab is enabled', function () {
assert.deepEqual(tabs.counts([annotation, orphan], true), {
anchoring: 0,
annotations: 1,
notes: 0,
orphans: 1,
});
});
});
});
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
var SearchClient = require('./search-client'); var SearchClient = require('./search-client');
var events = require('./events'); var events = require('./events');
var memoize = require('./util/memoize'); var memoize = require('./util/memoize');
var metadata = require('./annotation-metadata'); var tabs = require('./tabs');
var tabCounts = require('./tab-counts');
var uiConstants = require('./ui-constants');
function firstKey(object) { function firstKey(object) {
for (var k in object) { for (var k in object) {
...@@ -47,9 +45,8 @@ module.exports = function WidgetController( ...@@ -47,9 +45,8 @@ module.exports = function WidgetController(
$scope.rootThread = thread(); $scope.rootThread = thread();
$scope.selectedTab = state.selectedTab; $scope.selectedTab = state.selectedTab;
var counts = tabCounts(state.annotations, { var separateOrphans = tabs.shouldSeparateOrphans(state);
separateOrphans: features.flagEnabled('orphans_tab'), var counts = tabs.counts(state.annotations, separateOrphans);
});
Object.assign($scope, { Object.assign($scope, {
totalNotes: counts.notes, totalNotes: counts.notes,
...@@ -76,23 +73,6 @@ module.exports = function WidgetController( ...@@ -76,23 +73,6 @@ module.exports = function WidgetController(
frameSync.scrollToAnnotation(annotation.$tag); frameSync.scrollToAnnotation(annotation.$tag);
} }
/** Returns the annotation type - note or annotation of the first annotation
* in `results` whose ID is a key in `selectedAnnotationMap`.
*/
function tabContainingAnnotation(annot) {
if (metadata.isOrphan(annot)) {
if (features.flagEnabled('orphans_tab')) {
return uiConstants.TAB_ORPHANS;
} else {
return uiConstants.TAB_ANNOTATIONS;
}
} else if (metadata.isPageNote(annot)) {
return uiConstants.TAB_NOTES;
} else {
return uiConstants.TAB_ANNOTATIONS;
}
}
/** /**
* Returns the Annotation object for the first annotation in the * Returns the Annotation object for the first annotation in the
* selected annotation set. Note that 'first' refers to the order * selected annotation set. Note that 'first' refers to the order
...@@ -249,8 +229,8 @@ module.exports = function WidgetController( ...@@ -249,8 +229,8 @@ module.exports = function WidgetController(
focusAnnotation(selectedAnnot); focusAnnotation(selectedAnnot);
scrollToAnnotation(selectedAnnot); scrollToAnnotation(selectedAnnot);
var targetTab = tabContainingAnnotation(selectedAnnot); var separateOrphans = tabs.shouldSeparateOrphans(annotationUI.getState());
annotationUI.selectTab(targetTab); annotationUI.selectTab(tabs.tabForAnnotation(selectedAnnot, separateOrphans));
}); });
$scope.$on(events.GROUP_FOCUSED, function () { $scope.$on(events.GROUP_FOCUSED, function () {
......
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