Commit 40ab48f7 authored by Sheetal Umesh Kumar's avatar Sheetal Umesh Kumar Committed by Nick Stenning

Separate all top level annotations into 'notes' and 'annotations' tabs. (#3504)

Currently annotations, page notes, and orphans are all jumbled up in one view.
We want to separate out these three objects from each other so that the user
can better understand what each is, as well as their relationship to the document.

Hide tabs for direct links and when search results are displayed.

https://trello.com/c/OLdLTlLT/342-separate-annotations-and-notes

Hide tabs behind feature flag.
parent 39b4ea8e
......@@ -46,30 +46,42 @@ function isNew(annotation) {
return !annotation.id;
}
/** Return `true` if the given annotation is a page note, `false` otherwise. */
function isPageNote(annotation) {
return !isAnnotation(annotation) && !isReply(annotation)
}
/** Return `true` if the given annotation is a top level annotation, `false` otherwise. */
function isAnnotation(annotation) {
return (annotation.target && annotation.target.length > 0 && annotation.target[0].selector);
}
/** Return a numeric key that can be used to sort annotations by location.
*
* @return {number} - A key representing the location of the annotation in
* the document, where lower numbers mean closer to the
* start.
*/
function location(annotation) {
if (annotation) {
var targets = annotation.target || [];
for (var i=0; i < targets.length; i++) {
var selectors = targets[i].selector || [];
for (var k=0; k < selectors.length; k++) {
if (selectors[k].type === 'TextPositionSelector') {
return selectors[k].start;
}
}
}
}
return Number.POSITIVE_INFINITY;
}
function location(annotation) {
if (annotation) {
var targets = annotation.target || [];
for (var i = 0; i < targets.length; i++) {
var selectors = targets[i].selector || [];
for (var k = 0; k < selectors.length; k++) {
if (selectors[k].type === 'TextPositionSelector') {
return selectors[k].start;
}
}
}
}
return Number.POSITIVE_INFINITY;
}
module.exports = {
extractDocumentMetadata: extractDocumentMetadata,
isReply: isReply,
isAnnotation: isAnnotation,
isNew: isNew,
isPageNote: isPageNote,
isReply: isReply,
location: location,
};
'use strict';
var uiConstants = require('./ui-constants');
/**
* Uses a channel between the sidebar and the attached frames to ensure
* the interface remains in sync.
......@@ -27,6 +29,7 @@ function AnnotationUISync($rootScope, $window, bridge, annotationSync,
tags = tags || [];
var annotations = getAnnotationsByTags(tags);
annotationUI.selectAnnotations(annotations);
annotationUI.selectTab(uiConstants.TAB_ANNOTATIONS);
},
focusAnnotations: function (tags) {
tags = tags || [];
......
......@@ -10,6 +10,7 @@
var immutable = require('seamless-immutable');
var redux = require('redux');
var uiConstants = require('./ui-constants');
function freeze(selection) {
if (Object.keys(selection).length) {
......@@ -55,6 +56,8 @@ function initialState(settings) {
filterQuery: null,
selectedTab: uiConstants.TAB_ANNOTATIONS,
// Key by which annotations are currently sorted.
sortKey: 'Location',
// Keys by which annotations can be sorted.
......@@ -75,6 +78,7 @@ var types = {
CLEAR_ANNOTATIONS: 'CLEAR_ANNOTATIONS',
SET_FILTER_QUERY: 'SET_FILTER_QUERY',
SET_SORT_KEY: 'SET_SORT_KEY',
SELECT_TAB: 'SELECT_TAB',
};
function excludeAnnotations(current, annotations) {
......@@ -125,6 +129,8 @@ function reducer(state, action) {
return Object.assign({}, state, {expanded: action.expanded});
case types.HIGHLIGHT_ANNOTATIONS:
return Object.assign({}, state, {highlighted: action.highlighted});
case types.SELECT_TAB:
return Object.assign({}, state, {selectedTab: action.tab});
case types.SET_FILTER_QUERY:
return Object.assign({}, state, {
filterQuery: action.query,
......@@ -308,6 +314,14 @@ module.exports = function (settings) {
store.dispatch({type: types.CLEAR_ANNOTATIONS});
},
/** Set the type annotations to be displayed. */
selectTab: function (type) {
store.dispatch({
type: types.SELECT_TAB,
tab: type,
});
},
/** Set the query used to filter displayed annotations. */
setFilterQuery: function (query) {
store.dispatch({
......
......@@ -149,6 +149,7 @@ module.exports = angular.module('h', [
.directive('sidebarTutorial', require('./directive/sidebar-tutorial').directive)
.directive('signinControl', require('./directive/signin-control'))
.directive('searchInput', require('./directive/search-input'))
.directive('selectionTabs', require('./directive/selection-tabs'))
.directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('spinner', require('./directive/spinner'))
.directive('statusButton', require('./directive/status-button'))
......
......@@ -237,6 +237,11 @@ var defaultOpts = {
* displayed.
*/
filterFn: undefined,
/**
* A filter function which should return true if a given annotation and
* its replies should be displayed.
*/
threadFilterFn: undefined,
/**
* Mapping of annotation IDs to expansion states.
*/
......@@ -341,6 +346,12 @@ function buildThread(annotations, opts) {
return child.visible || hasVisibleChildren(child);
});
// Get annotations which are of type notes or annotations depending
// on the filter.
if (opts.threadFilterFn) {
thread.children = thread.children.filter(opts.threadFilterFn);
}
// Sort the root thread according to the current search criteria
thread = sortThread(thread, opts.sortCompareFn, opts.replySortCompareFn);
......
......@@ -10,8 +10,12 @@ module.exports = function () {
filterMatchCount: '<',
onClearSelection: '&',
searchQuery: '<',
selectedTab: '<',
selectionCount: '<',
totalCount: '<',
tabAnnotations: '<',
tabNotes: '<',
totalAnnotations: '<',
totalNotes: '<',
},
template: require('../../../templates/client/search_status_bar.html'),
};
......
'use strict';
module.exports = function () {
return {
bindToController: true,
controllerAs: 'vm',
//@ngInject
controller: function (annotationUI) {
this.selectTab = function (type) {
annotationUI.clearSelectedAnnotations();
annotationUI.selectTab(type);
};
},
restrict: 'E',
scope: {
selectedTab: '<',
totalAnnotations: '<',
totalNotes: '<',
tabAnnotations: '<',
tabNotes: '<',
},
template: require('../../../templates/client/selection_tabs.html'),
};
};
......@@ -3,7 +3,6 @@
var angular = require('angular');
var util = require('./util');
var unroll = require('../../test/util').unroll;
describe('searchStatusBar', function () {
before(function () {
......@@ -26,19 +25,34 @@ describe('searchStatusBar', function () {
});
context('when there is a selection', function () {
var FIXTURES = [
{count: 0, message: 'Show all annotations'},
{count: 1, message: 'Show all annotations'},
{count: 10, message: 'Show all 10 annotations'},
];
it('should display the "Show all annotations (2)" message when there are 2 annotations', function () {
var msg = 'Show all annotations';
var msgCount = '(2)'
var elem = util.createDirective(document, 'searchStatusBar', {
selectionCount: 1,
totalAnnotations: 2,
selectedTab: 'annotation',
tabAnnotations: 'annotation',
tabNotes: 'note',
});
var clearBtn = elem[0].querySelector('button');
assert.include(clearBtn.textContent, msg);
assert.include(clearBtn.textContent, msgCount);
});
unroll('should display the "Show all annotations" message when there are #count annotations', function (testCase) {
it('should display the "Show all notes (3)" message when there are 3 notes', function () {
var msg = 'Show all notes';
var msgCount = '(3)';
var elem = util.createDirective(document, 'searchStatusBar', {
selectionCount: 1,
totalCount: testCase.count
totalNotes: 3,
selectedTab: 'note',
tabAnnotations: 'annotation',
tabNotes: 'note',
});
var clearBtn = elem[0].querySelector('button');
assert.include(clearBtn.textContent, testCase.message);
}, FIXTURES);
assert.include(clearBtn.textContent, msg);
assert.include(clearBtn.textContent, msgCount);
});
});
});
'use strict';
var angular = require('angular');
var util = require('./util');
describe('selectionTabs', function () {
before(function () {
angular.module('app', [])
.directive('selectionTabs', require('../selection-tabs'));
});
beforeEach(function () {
var fakeAnnotationUI = {};
angular.mock.module('app', {
annotationUI: fakeAnnotationUI,
});
});
context('displays selection tabs, counts and a selection', function () {
it('should display the tabs and counts of annotations and notes', function () {
var elem = util.createDirective(document, 'selectionTabs', {
selectedTab: 'annotation',
totalAnnotations: '123',
totalNotes: '456',
tabAnnotations: 'annotation',
tabNotes: 'note',
});
var tabs = elem[0].querySelectorAll('li');
var sups = elem[0].querySelectorAll('sup');
assert.include(tabs[0].textContent, "Annotations");
assert.include(tabs[1].textContent, "Notes");
assert.include(sups[0].textContent, "123");
assert.include(sups[1].textContent, "456");
});
it('should display annotations tab as selected', function () {
var elem = util.createDirective(document, 'selectionTabs', {
selectedTab: 'annotation',
totalAnnotations: '123',
totalNotes: '456',
tabAnnotations: 'annotation',
tabNotes: 'note',
});
var tabs = elem[0].querySelectorAll('li');
assert.include(tabs[0].className, "selection-tabs--selected");
});
it('should display notes tab as selected', function () {
var elem = util.createDirective(document, 'selectionTabs', {
selectedTab: 'note',
totalAnnotations: '123',
totalNotes: '456',
tabAnnotations: 'annotation',
tabNotes: 'note',
});
var tabs = elem[0].querySelectorAll('li');
assert.include(tabs[1].className, "selection-tabs--selected");
});
});
});
......@@ -4,6 +4,7 @@ var buildThread = require('./build-thread');
var events = require('./events');
var memoize = require('./util/memoize');
var metadata = require('./annotation-metadata');
var uiConstants = require('./ui-constants');
function truthyKeys(map) {
return Object.keys(map).filter(function (k) {
......@@ -38,7 +39,7 @@ var sortFns = {
* The root thread is then displayed by viewer.html
*/
// @ngInject
function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
function RootThread($rootScope, annotationUI, features, searchFilter, viewFilter) {
/**
* Build the root conversation thread from the given UI state.
......@@ -57,6 +58,17 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
};
}
var threadFilterFn;
if (features.flagEnabled('selection_tabs') && !state.filterQuery) {
threadFilterFn = function (thread) {
if (state.selectedTab === uiConstants.TAB_ANNOTATIONS) {
return thread.annotation && metadata.isAnnotation(thread.annotation);
} else if (state.selectedTab === uiConstants.TAB_NOTES) {
return thread.annotation && metadata.isPageNote(thread.annotation);
}
};
}
// Get the currently loaded annotations and the set of inputs which
// determines what is visible and build the visible thread structure
return buildThread(state.annotations, {
......@@ -66,6 +78,7 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
selected: truthyKeys(state.selectedAnnotationMap || {}),
sortCompareFn: sortFn,
filterFn: filterFn,
threadFilterFn: threadFilterFn,
});
}
......@@ -89,6 +102,16 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
// Ensure that newly created annotations are always visible
if (event.name === events.BEFORE_ANNOTATION_CREATED) {
// If the annotation is of type note or annotation, make sure
// the appropriate tab is selected. If it is of type reply, user
// stays in the selected tab.
if (metadata.isPageNote(annotation)) {
annotationUI.selectTab(uiConstants.TAB_NOTES);
} else if (metadata.isAnnotation(annotation)) {
annotationUI.selectTab(uiConstants.TAB_ANNOTATIONS);
}
(annotation.references || []).forEach(function (parent) {
annotationUI.setCollapsed(parent, false);
});
......
......@@ -9,7 +9,7 @@ function defaultAnnotation() {
document: {
title: 'A special document'
},
target: [{}],
target: [{source: 'source', 'selector': []}],
uri: 'http://example.com',
user: 'acct:bill@localhost',
updated: '2015-05-10T20:18:56.613388+00:00',
......
......@@ -112,4 +112,39 @@ describe('annotation-metadata', function () {
}), Number.POSITIVE_INFINITY);
});
});
describe('.isPageNote', function () {
it ('returns true for an annotation with an empty target', function () {
assert.isTrue(annotationMetadata.isPageNote({
target: []
}));
});
it ('returns true for an annotation without selectors', function () {
assert.isTrue(annotationMetadata.isPageNote({
target: [{selector: undefined}]
}));
});
it ('returns true for an annotation without a target', function () {
assert.isTrue(annotationMetadata.isPageNote({
target: undefined
}));
});
it ('returns false for an annotation which is a reply', function () {
assert.isFalse(annotationMetadata.isPageNote({
target: [],
references: ['xyz']
}));
});
});
describe ('.isAnnotation', function () {
it ('returns true if an annotation is a top level annotation', function () {
assert(annotationMetadata.isAnnotation({
target: [{selector: []}]
}));
});
it ('returns false if an annotation has no target', function () {
assert.equal(annotationMetadata.isAnnotation({}), undefined);
});
});
});
......@@ -239,4 +239,12 @@ describe('annotationUI', function () {
assert.deepEqual(annotationUI.getState().highlighted, ['id1', 'id2']);
});
});
describe('#selectTab()', function () {
it('sets the selected tab', function () {
var annotationTab = 'annotation';
annotationUI.selectTab(annotationTab);
assert.equal(annotationUI.getState().selectedTab, annotationTab);
});
});
});
'use strict';
var buildThread = require('../build-thread');
var metadata = require('../annotation-metadata');
// Fixture with two top level annotations and one reply
// Fixture with two top level annotations, one note and one reply
var SIMPLE_FIXTURE = [{
id: '1',
text: 'first annotation',
......@@ -262,6 +263,31 @@ describe('build-thread', function () {
}]);
});
});
describe('thread filtering', function () {
var fixture = [{
id: '1',
text: 'annotation',
target: [{selector: {}}],
},{
id: '2',
text: 'note',
target: [{selector: undefined}],
}];
it('shows only annotations matching the thread filter', function () {
var thread = createThread(fixture, {
threadFilterFn: function (thread) {
return metadata.isPageNote(thread.annotation);
},
});
assert.deepEqual(thread, [{
annotation: fixture[1],
children: []
}]);
});
});
});
describe('sort order', function () {
......
......@@ -9,12 +9,14 @@ var fixtures = immutable({
annotations: [{
id: '1',
references: [],
target: [{selector: []}],
text: 'first annotation',
updated: 50,
},{
id: '2',
references: [],
text: 'second annotation',
target: [{selector: []}],
updated: 200,
},{
id: '3',
......@@ -34,11 +36,16 @@ describe('annotation threading', function () {
fold: function (s) { return s; },
};
var fakeFeatures = {
flagEnabled: sinon.stub().returns(true),
};
angular.module('app', [])
.service('annotationUI', require('../../annotation-ui'))
.service('rootThread', require('../../root-thread'))
.service('searchFilter', require('../../search-filter'))
.service('viewFilter', require('../../view-filter'))
.value('features', fakeFeatures)
.value('settings', {})
.value('unicode', fakeUnicode);
......
......@@ -20,6 +20,7 @@ var fixtures = immutable({
describe('rootThread', function () {
var fakeAnnotationUI;
var fakeBuildThread;
var fakeFeatures;
var fakeSearchFilter;
var fakeViewFilter;
......@@ -41,7 +42,6 @@ describe('rootThread', function () {
sortKeysAvailable: ['Location'],
visibleHighlights: false,
},
getState: function () {
return this.state;
},
......@@ -50,10 +50,15 @@ describe('rootThread', function () {
removeSelectedAnnotation: sinon.stub(),
addAnnotations: sinon.stub(),
setCollapsed: sinon.stub(),
selectTab: sinon.stub(),
};
fakeBuildThread = sinon.stub().returns(fixtures.emptyThread);
fakeFeatures = {
flagEnabled: sinon.stub().returns(true),
};
fakeSearchFilter = {
generateFacetedFilter: sinon.stub(),
};
......@@ -64,6 +69,7 @@ describe('rootThread', function () {
angular.module('app', [])
.value('annotationUI', fakeAnnotationUI)
.value('features', fakeFeatures)
.value('searchFilter', fakeSearchFilter)
.value('viewFilter', fakeViewFilter)
.service('rootThread', proxyquire('../root-thread', {
......@@ -179,6 +185,47 @@ describe('rootThread', function () {
]);
});
describe('when the thread filter query is set', function () {
it('generates a thread filter function to match annotations', function () {
fakeBuildThread.reset();
fakeAnnotationUI.state = Object.assign({}, fakeAnnotationUI.state,
{selectedTab: 'annotation'});
rootThread.thread(fakeAnnotationUI.state);
var threadFilterFn = fakeBuildThread.args[0][1].threadFilterFn;
var annotation = {target: [{ selector: {} }]};
assert(threadFilterFn({annotation: annotation}));
});
it('generates a thread filter function to match notes', function () {
fakeBuildThread.reset();
fakeBuildThread.annotation = {target: [{}]};
fakeAnnotationUI.state = Object.assign({}, fakeAnnotationUI.state,
{selectedTab: 'note'});
rootThread.thread(fakeAnnotationUI.state);
var threadFilterFn = fakeBuildThread.args[0][1].threadFilterFn;
assert.equal(threadFilterFn(fakeBuildThread), true);
});
it('generates a thread filter function for annotations, when all annotations are of type notes', function () {
fakeBuildThread.reset();
fakeBuildThread.annotation = {target: [{}]};
fakeAnnotationUI.state = Object.assign({}, fakeAnnotationUI.state,
{selectedTab: 'annotation'});
rootThread.thread(fakeAnnotationUI.state);
var threadFilterFn = fakeBuildThread.args[0][1].threadFilterFn;
assert.equal(threadFilterFn(fakeBuildThread), undefined);
});
});
describe('when the filter query changes', function () {
it('generates a thread filter function from the query', function () {
fakeBuildThread.reset();
......
......@@ -54,6 +54,7 @@ describe('WidgetController', function () {
var fakeAnnotationMapper;
var fakeCrossFrame;
var fakeDrafts;
var fakeFeatures;
var fakeGroups;
var fakeRootThread;
var fakeSettings;
......@@ -90,10 +91,15 @@ describe('WidgetController', function () {
call: sinon.stub(),
frames: [],
};
fakeDrafts = {
unsaved: sandbox.stub().returns([]),
};
fakeFeatures = {
flagEnabled: sandbox.stub().returns(true),
};
fakeStreamer = {
setConfig: sandbox.spy()
};
......@@ -124,6 +130,7 @@ describe('WidgetController', function () {
$provide.value('annotationUI', annotationUI);
$provide.value('crossframe', fakeCrossFrame);
$provide.value('drafts', fakeDrafts);
$provide.value('features', fakeFeatures);
$provide.value('rootThread', fakeRootThread);
$provide.value('store', fakeStore);
$provide.value('streamer', fakeStreamer);
......
'use strict';
/**
* uiConstants is a set of globally used constants across the application.
*/
module.exports = {
TAB_ANNOTATIONS: 'annotation',
TAB_NOTES: 'note',
};
......@@ -3,7 +3,9 @@
var SearchClient = require('./search-client');
var events = require('./events');
var memoize = require('./util/memoize');
var metadata = require('./annotation-metadata');
var scopeTimeout = require('./util/scope-timeout');
var uiConstants = require('./ui-constants');
function firstKey(object) {
for (var k in object) {
......@@ -33,10 +35,31 @@ function groupIDFromSelection(selection, results) {
// @ngInject
module.exports = function WidgetController(
$scope, $rootScope, annotationUI, crossframe, annotationMapper,
drafts, groups, rootThread, settings, streamer, streamFilter, store,
drafts, features, groups, rootThread, settings, streamer, streamFilter, store,
VirtualThreadList
) {
/**
* Returns the number of top level annotations which are of type annotations
* and not notes or replies.
*/
function countAnnotations(annotations) {
var total = annotations.reduce(function (count, annotation) {
return annotation && metadata.isAnnotation(annotation) ? count + 1 : count;
}, 0);
return total;
}
/**
* Returns the number of top level annotations which are of type notes.
*/
function countNotes(annotations) {
var total = annotations.reduce(function (count, annotation) {
return annotation && metadata.isPageNote(annotation) ? count + 1 : count;
}, 0);
return total;
}
/**
* Returns the height of the thread for an annotation if it exists in the view
* or undefined otherwise.
......@@ -72,6 +95,9 @@ module.exports = function WidgetController(
var visibleThreads = new VirtualThreadList($scope, window, thread());
annotationUI.subscribe(function () {
visibleThreads.setRootThread(thread());
$scope.totalAnnotations = countAnnotations(annotationUI.getState().annotations);
$scope.totalNotes = countNotes(annotationUI.getState().annotations);
$scope.selectedTab = annotationUI.getState().selectedTab;
});
visibleThreads.on('changed', function (state) {
......@@ -117,6 +143,25 @@ module.exports = function WidgetController(
crossframe.call('scrollToAnnotation', annotation.$$tag);
}
/** Returns the annotation type - note or annotation of the first annotation
* in `results` whose ID is a key in `selectedAnnotationMap`.
*/
function tabTypeFromSelection(selection, results) {
var id = firstKey(selection);
var annot = results.find(function (annot) {
return annot.id === id;
});
if (!annot) {
return null;
}
if (metadata.isAnnotation(annot)) {
return uiConstants.TAB_ANNOTATIONS;
}
if (metadata.isPageNote(annot)) {
return uiConstants.TAB_NOTES;
}
}
/**
* Returns the Annotation object for the first annotation in the
* selected annotation set. Note that 'first' refers to the order
......@@ -154,6 +199,10 @@ module.exports = function WidgetController(
searchClients.push(searchClient);
searchClient.on('results', function (results) {
if (annotationUI.hasSelectedAnnotations()) {
// Select appropriate tab - notes or annotations, for selection
annotationUI.selectTab(
tabTypeFromSelection(annotationUI.getState().selectedAnnotationMap, results));
// Focus the group containing the selected annotation and filter
// annotations to those from this group
var groupID = groupIDFromSelection(
......@@ -332,6 +381,9 @@ module.exports = function WidgetController(
};
$scope.isLoading = isLoading;
$scope.tabAnnotations = uiConstants.TAB_ANNOTATIONS;
$scope.tabNotes = uiConstants.TAB_NOTES;
$scope.selectionTabsFlagEnabled = features.flagEnabled('selection_tabs');
var visibleCount = memoize(function (thread) {
return thread.children.reduce(function (count, child) {
......
......@@ -21,6 +21,7 @@ $base-line-height: 20px;
@import './primary-action-btn';
@import './publish-annotation-btn';
@import './search-status-bar';
@import './selection-tabs';
@import './share-link';
@import './sidebar-tutorial';
@import './signin-control';
......
.selection-tabs {
display: flex;
flex-direction: row;
color: $grey-4;
@include font-normal;
font-weight: bold;
margin: 10px 0px;
}
.selection-tabs--selected {
color: $grey-6;
}
.selection-tabs__type {
margin-right: 20px;
cursor: pointer;
}
......@@ -14,13 +14,22 @@
<div class="search-status-bar" ng-if="!filterActive && selectionCount > 0">
<button class="primary-action-btn primary-action-btn--short"
ng-click="onClearSelection()"
title="Clear the selection and show all annotations"
>
<span ng-pluralize
count="totalCount"
when="{'0': 'Show all annotations',
'one': 'Show all annotations',
'other': 'Show all {{totalCount}} annotations'}"></span>
title="Clear the selection and show all annotations">
<span ng-if="!selectedTab">
Show all annotations and notes
</span>
<span ng-if="selectedTab === tabAnnotations">
Show all annotations
<span ng-if="totalAnnotations > 1">
({{totalAnnotations}})
</span>
</span>
<span ng-if="selectedTab === tabNotes">
Show all notes
<span ng-if="totalNotes > 1">
({{totalNotes}})
</span>
</span>
</button>
</div>
<!-- Tabbed display of annotations and notes. -->
<ul class="selection-tabs">
<li class="selection-tabs__type" ng-class="{'selection-tabs--selected': vm.selectedTab === vm.tabAnnotations}" ng-click="vm.selectTab('annotation')">
Annotations
<sup ng-if="vm.totalAnnotations > 0">{{ vm.totalAnnotations }}</sup>
</li>
<li class="selection-tabs__type" ng-class="{'selection-tabs--selected': vm.selectedTab === vm.tabNotes}" ng-click="vm.selectTab('note')">
Notes
<sup ng-if="vm.totalNotes > 0">{{ vm.totalNotes }}</sup>
</li>
</ul>
......@@ -2,6 +2,14 @@
(See gh2642 for rationale for 'ng-show="true"')
-->
<selection-tabs ng-if="selectionTabsFlagEnabled && !search.query() && selectedAnnotationCount() <= 0"
selected-tab="selectedTab"
total-annotations="totalAnnotations"
total-notes="totalNotes"
tab-annotations="tabAnnotations"
tab-notes="tabNotes">
</selection-tabs>
<ul class="thread-list ng-hide"
ng-show="true"
window-scroll="loadMore(20)">
......@@ -14,7 +22,11 @@
search-query="search ? search.query : ''"
selection-count="selectedAnnotationCount()"
total-count="topLevelThreadCount()"
>
selected-tab="selectedTab"
tab-annotations="tabAnnotations"
tab-notes="tabNotes"
total-annotations="totalAnnotations"
total-notes="totalNotes">
</search-status-bar>
<li class="annotation-unavailable-message"
ng-if="selectedAnnotationUnavailable()">
......
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