Commit a5923eb2 authored by Robert Knight's avatar Robert Knight

Convert sidebar content to a component

Convert 'WidgetController' into a `<sidebar-content>` component which
uses `sidebar_content.html` as a template and convert the route for the
sidebar into a trivial template which just renders this component.

 * Move `widget-controller` to src/sidebar/components/sidebar-content.js

 * Move various helpers for the sidebar content template from the scope
   to the controller, in line with other components.

 * Pass the "auth" and "search" scope properties from the AppController
   instance explicitly to `<sidebar-content>`, since components do not
   inherit scope properties automatically.
parent 6c8eaf12
...@@ -75,8 +75,7 @@ function configureRoutes($routeProvider) { ...@@ -75,8 +75,7 @@ function configureRoutes($routeProvider) {
resolve: resolve, resolve: resolve,
}); });
$routeProvider.otherwise({ $routeProvider.otherwise({
controller: 'WidgetController', template: '<sidebar-content search="search" auth="auth"></sidebar-content>',
template: require('./templates/sidebar_content.html'),
reloadOnSearch: false, reloadOnSearch: false,
resolve: resolve, resolve: resolve,
}); });
...@@ -128,7 +127,6 @@ module.exports = angular.module('h', [ ...@@ -128,7 +127,6 @@ module.exports = angular.module('h', [
.controller('AnnotationViewerController', require('./annotation-viewer-controller')) .controller('AnnotationViewerController', require('./annotation-viewer-controller'))
.controller('StreamController', require('./stream-controller')) .controller('StreamController', require('./stream-controller'))
.controller('WidgetController', require('./widget-controller'))
// The root component for the application // The root component for the application
.directive('hypothesisApp', require('./directive/app')) .directive('hypothesisApp', require('./directive/app'))
...@@ -150,6 +148,7 @@ module.exports = angular.module('h', [ ...@@ -150,6 +148,7 @@ module.exports = angular.module('h', [
.component('searchInput', require('./components/search-input')) .component('searchInput', require('./components/search-input'))
.component('searchStatusBar', require('./components/search-status-bar')) .component('searchStatusBar', require('./components/search-status-bar'))
.component('selectionTabs', require('./components/selection-tabs')) .component('selectionTabs', require('./components/selection-tabs'))
.component('sidebarContent', require('./components/sidebar-content'))
.component('sidebarTutorial', require('./components/sidebar-tutorial').component) .component('sidebarTutorial', require('./components/sidebar-tutorial').component)
.component('shareDialog', require('./components/share-dialog')) .component('shareDialog', require('./components/share-dialog'))
.component('sortDropdown', require('./components/sort-dropdown')) .component('sortDropdown', require('./components/sort-dropdown'))
......
'use strict'; 'use strict';
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 tabs = require('./tabs'); var tabs = require('../tabs');
function firstKey(object) { function firstKey(object) {
for (var k in object) { for (var k in object) {
...@@ -31,10 +31,12 @@ function groupIDFromSelection(selection, results) { ...@@ -31,10 +31,12 @@ function groupIDFromSelection(selection, results) {
} }
// @ngInject // @ngInject
module.exports = function WidgetController( function SidebarContentController(
$scope, analytics, annotationUI, annotationMapper, drafts, features, frameSync, $scope, analytics, annotationUI, annotationMapper, drafts, features, frameSync,
groups, rootThread, settings, streamer, streamFilter, store groups, rootThread, settings, streamer, streamFilter, store
) { ) {
var self = this;
function thread() { function thread() {
return rootThread.thread(annotationUI.getState()); return rootThread.thread(annotationUI.getState());
} }
...@@ -42,13 +44,13 @@ module.exports = function WidgetController( ...@@ -42,13 +44,13 @@ module.exports = function WidgetController(
var unsubscribeAnnotationUI = annotationUI.subscribe(function () { var unsubscribeAnnotationUI = annotationUI.subscribe(function () {
var state = annotationUI.getState(); var state = annotationUI.getState();
$scope.rootThread = thread(); self.rootThread = thread();
$scope.selectedTab = state.selectedTab; self.selectedTab = state.selectedTab;
var separateOrphans = tabs.shouldSeparateOrphans(state); var separateOrphans = tabs.shouldSeparateOrphans(state);
var counts = tabs.counts(state.annotations, separateOrphans); var counts = tabs.counts(state.annotations, separateOrphans);
Object.assign($scope, { Object.assign(self, {
totalNotes: counts.notes, totalNotes: counts.notes,
totalAnnotations: counts.annotations, totalAnnotations: counts.annotations,
totalOrphans: counts.orphans, totalOrphans: counts.orphans,
...@@ -210,7 +212,7 @@ module.exports = function WidgetController( ...@@ -210,7 +212,7 @@ module.exports = function WidgetController(
}); });
// If the user is logged in, we connect nevertheless // If the user is logged in, we connect nevertheless
if ($scope.auth.status === 'logged-in') { if (this.auth.status === 'logged-in') {
streamer.connect(); streamer.connect();
} }
...@@ -257,21 +259,21 @@ module.exports = function WidgetController( ...@@ -257,21 +259,21 @@ module.exports = function WidgetController(
}); });
}, loadAnnotations, true); }, loadAnnotations, true);
$scope.setCollapsed = function (id, collapsed) { this.setCollapsed = function (id, collapsed) {
annotationUI.setCollapsed(id, collapsed); annotationUI.setCollapsed(id, collapsed);
}; };
$scope.forceVisible = function (thread) { this.forceVisible = function (thread) {
annotationUI.setForceVisible(thread.id, true); annotationUI.setForceVisible(thread.id, true);
if (thread.parent) { if (thread.parent) {
annotationUI.setCollapsed(thread.parent.id, false); annotationUI.setCollapsed(thread.parent.id, false);
} }
}; };
$scope.focus = focusAnnotation; this.focus = focusAnnotation;
$scope.scrollTo = scrollToAnnotation; this.scrollTo = scrollToAnnotation;
$scope.selectedAnnotationCount = function () { this.selectedAnnotationCount = function () {
var selection = annotationUI.getState().selectedAnnotationMap; var selection = annotationUI.getState().selectedAnnotationMap;
if (!selection) { if (!selection) {
return 0; return 0;
...@@ -279,16 +281,16 @@ module.exports = function WidgetController( ...@@ -279,16 +281,16 @@ module.exports = function WidgetController(
return Object.keys(selection).length; return Object.keys(selection).length;
}; };
$scope.selectedAnnotationUnavailable = function () { this.selectedAnnotationUnavailable = function () {
var selectedID = firstKey(annotationUI.getState().selectedAnnotationMap); var selectedID = firstKey(annotationUI.getState().selectedAnnotationMap);
return !isLoading() && return !isLoading() &&
!!selectedID && !!selectedID &&
!annotationUI.annotationExists(selectedID); !annotationUI.annotationExists(selectedID);
}; };
$scope.shouldShowLoggedOutMessage = function () { this.shouldShowLoggedOutMessage = function () {
// If user is not logged out, don't show CTA. // If user is not logged out, don't show CTA.
if ($scope.auth.status !== 'logged-out') { if (self.auth.status !== 'logged-out') {
return false; return false;
} }
...@@ -307,7 +309,7 @@ module.exports = function WidgetController( ...@@ -307,7 +309,7 @@ module.exports = function WidgetController(
annotationUI.annotationExists(selectedID); annotationUI.annotationExists(selectedID);
}; };
$scope.isLoading = isLoading; this.isLoading = isLoading;
var visibleCount = memoize(function (thread) { var visibleCount = memoize(function (thread) {
return thread.children.reduce(function (count, child) { return thread.children.reduce(function (count, child) {
...@@ -315,11 +317,21 @@ module.exports = function WidgetController( ...@@ -315,11 +317,21 @@ module.exports = function WidgetController(
}, thread.visible ? 1 : 0); }, thread.visible ? 1 : 0);
}); });
$scope.visibleCount = function () { this.visibleCount = function () {
return visibleCount(thread()); return visibleCount(thread());
}; };
$scope.topLevelThreadCount = function () { this.topLevelThreadCount = function () {
return thread().totalChildren; return thread().totalChildren;
}; };
}
module.exports = {
controller: SidebarContentController,
controllerAs: 'vm',
bindings: {
auth: '<',
search: '<',
},
template: require('../templates/sidebar_content.html'),
}; };
...@@ -5,8 +5,8 @@ var inherits = require('inherits'); ...@@ -5,8 +5,8 @@ var inherits = require('inherits');
var proxyquire = require('proxyquire'); var proxyquire = require('proxyquire');
var EventEmitter = require('tiny-emitter'); var EventEmitter = require('tiny-emitter');
var events = require('../events'); var events = require('../../events');
var noCallThru = require('../../shared/test/util').noCallThru; var noCallThru = require('../../../shared/test/util').noCallThru;
var searchClients; var searchClients;
function FakeSearchClient(searchFn, opts) { function FakeSearchClient(searchFn, opts) {
...@@ -36,10 +36,11 @@ function FakeRootThread() { ...@@ -36,10 +36,11 @@ function FakeRootThread() {
} }
inherits(FakeRootThread, EventEmitter); inherits(FakeRootThread, EventEmitter);
describe('WidgetController', function () { describe('SidebarContentController', function () {
var $rootScope; var $rootScope;
var $scope; var $scope;
var annotationUI; var annotationUI;
var ctrl;
var fakeAnalytics; var fakeAnalytics;
var fakeAnnotationMapper; var fakeAnnotationMapper;
var fakeDrafts; var fakeDrafts;
...@@ -55,11 +56,11 @@ describe('WidgetController', function () { ...@@ -55,11 +56,11 @@ describe('WidgetController', function () {
before(function () { before(function () {
angular.module('h', []) angular.module('h', [])
.service('annotationUI', require('../annotation-ui')) .service('annotationUI', require('../../annotation-ui'))
.controller('WidgetController', proxyquire('../widget-controller', .component('sidebarContent', proxyquire('../sidebar-content',
noCallThru({ noCallThru({
angular: angular, angular: angular,
'./search-client': FakeSearchClient, '../search-client': FakeSearchClient,
}) })
)); ));
}); });
...@@ -135,14 +136,15 @@ describe('WidgetController', function () { ...@@ -135,14 +136,15 @@ describe('WidgetController', function () {
annotationUI.frames.returns(frames); annotationUI.frames.returns(frames);
} }
beforeEach(angular.mock.inject(function ($controller, _annotationUI_, _$rootScope_) { beforeEach(angular.mock.inject(function ($componentController, _annotationUI_, _$rootScope_) {
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$scope.auth = {'status': 'unknown'};
annotationUI = _annotationUI_; annotationUI = _annotationUI_;
annotationUI.frames = sinon.stub().returns([]); annotationUI.frames = sinon.stub().returns([]);
annotationUI.updateFrameAnnotationFetchStatus = sinon.stub(); annotationUI.updateFrameAnnotationFetchStatus = sinon.stub();
$controller('WidgetController', {$scope: $scope}); ctrl = $componentController('sidebarContent', { $scope: $scope }, {
auth: { status: 'unknown' },
});
})); }));
afterEach(function () { afterEach(function () {
...@@ -223,7 +225,7 @@ describe('WidgetController', function () { ...@@ -223,7 +225,7 @@ describe('WidgetController', function () {
}); });
it('selectedAnnotationCount is > 0', function () { it('selectedAnnotationCount is > 0', function () {
assert.equal($scope.selectedAnnotationCount(), 1); assert.equal(ctrl.selectedAnnotationCount(), 1);
}); });
it('switches to the selected annotation\'s group', function () { it('switches to the selected annotation\'s group', function () {
...@@ -253,7 +255,7 @@ describe('WidgetController', function () { ...@@ -253,7 +255,7 @@ describe('WidgetController', function () {
}); });
it('selectedAnnotationCount is 0', function () { it('selectedAnnotationCount is 0', function () {
assert.equal($scope.selectedAnnotationCount(), 0); assert.equal(ctrl.selectedAnnotationCount(), 0);
}); });
it('fetches annotations for the current group', function () { it('fetches annotations for the current group', function () {
...@@ -338,20 +340,20 @@ describe('WidgetController', function () { ...@@ -338,20 +340,20 @@ describe('WidgetController', function () {
it('displays a message if the selection is unavailable', function () { it('displays a message if the selection is unavailable', function () {
annotationUI.selectAnnotations(['missing']); annotationUI.selectAnnotations(['missing']);
$scope.$digest(); $scope.$digest();
assert.isTrue($scope.selectedAnnotationUnavailable()); assert.isTrue(ctrl.selectedAnnotationUnavailable());
}); });
it('does not show a message if the selection is available', function () { it('does not show a message if the selection is available', function () {
annotationUI.addAnnotations([{id: '123'}]); annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable()); assert.isFalse(ctrl.selectedAnnotationUnavailable());
}); });
it('does not a show a message if there is no selection', function () { it('does not a show a message if there is no selection', function () {
annotationUI.selectAnnotations([]); annotationUI.selectAnnotations([]);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable()); assert.isFalse(ctrl.selectedAnnotationUnavailable());
}); });
it('doesn\'t show a message if the document isn\'t loaded yet', function () { it('doesn\'t show a message if the document isn\'t loaded yet', function () {
...@@ -363,56 +365,56 @@ describe('WidgetController', function () { ...@@ -363,56 +365,56 @@ describe('WidgetController', function () {
setFrames([]); setFrames([]);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.selectedAnnotationUnavailable()); assert.isFalse(ctrl.selectedAnnotationUnavailable());
}); });
it('shows logged out message if selection is available', function () { it('shows logged out message if selection is available', function () {
$scope.auth = { ctrl.auth = {
status: 'logged-out', status: 'logged-out',
}; };
annotationUI.addAnnotations([{id: '123'}]); annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
$scope.$digest(); $scope.$digest();
assert.isTrue($scope.shouldShowLoggedOutMessage()); assert.isTrue(ctrl.shouldShowLoggedOutMessage());
}); });
it('does not show loggedout message if selection is unavailable', function () { it('does not show loggedout message if selection is unavailable', function () {
$scope.auth = { ctrl.auth = {
status: 'logged-out', status: 'logged-out',
}; };
annotationUI.selectAnnotations(['missing']); annotationUI.selectAnnotations(['missing']);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse(ctrl.shouldShowLoggedOutMessage());
}); });
it('does not show loggedout message if there is no selection', function () { it('does not show loggedout message if there is no selection', function () {
$scope.auth = { ctrl.auth = {
status: 'logged-out', status: 'logged-out',
}; };
annotationUI.selectAnnotations([]); annotationUI.selectAnnotations([]);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse(ctrl.shouldShowLoggedOutMessage());
}); });
it('does not show loggedout message if user is not logged out', function () { it('does not show loggedout message if user is not logged out', function () {
$scope.auth = { ctrl.auth = {
status: 'logged-in', status: 'logged-in',
}; };
annotationUI.addAnnotations([{id: '123'}]); annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse(ctrl.shouldShowLoggedOutMessage());
}); });
it('does not show loggedout message if not a direct link', function () { it('does not show loggedout message if not a direct link', function () {
$scope.auth = { ctrl.auth = {
status: 'logged-out', status: 'logged-out',
}; };
delete fakeSettings.annotations; delete fakeSettings.annotations;
annotationUI.addAnnotations([{id: '123'}]); annotationUI.addAnnotations([{id: '123'}]);
annotationUI.selectAnnotations(['123']); annotationUI.selectAnnotations(['123']);
$scope.$digest(); $scope.$digest();
assert.isFalse($scope.shouldShowLoggedOutMessage()); assert.isFalse(ctrl.shouldShowLoggedOutMessage());
}); });
}); });
...@@ -440,7 +442,7 @@ describe('WidgetController', function () { ...@@ -440,7 +442,7 @@ describe('WidgetController', function () {
describe('#forceVisible', function () { describe('#forceVisible', function () {
it('shows the thread', function () { it('shows the thread', function () {
var thread = {id: '1'}; var thread = {id: '1'};
$scope.forceVisible(thread); ctrl.forceVisible(thread);
assert.deepEqual(annotationUI.getState().forceVisible, {1: true}); assert.deepEqual(annotationUI.getState().forceVisible, {1: true});
}); });
...@@ -450,7 +452,7 @@ describe('WidgetController', function () { ...@@ -450,7 +452,7 @@ describe('WidgetController', function () {
parent: {id: '3'}, parent: {id: '3'},
}; };
assert.equal(annotationUI.getState().expanded[thread.parent.id], undefined); assert.equal(annotationUI.getState().expanded[thread.parent.id], undefined);
$scope.forceVisible(thread); ctrl.forceVisible(thread);
assert.equal(annotationUI.getState().expanded[thread.parent.id], true); assert.equal(annotationUI.getState().expanded[thread.parent.id], true);
}); });
}); });
...@@ -469,7 +471,7 @@ describe('WidgetController', function () { ...@@ -469,7 +471,7 @@ describe('WidgetController', function () {
}], }],
}); });
$scope.$digest(); $scope.$digest();
assert.equal($scope.visibleCount(), 2); assert.equal(ctrl.visibleCount(), 2);
}); });
}); });
}); });
<selection-tabs <selection-tabs
ng-if="!search.query() && selectedAnnotationCount() === 0" ng-if="!vm.search.query() && vm.selectedAnnotationCount() === 0"
is-waiting-to-anchor-annotations="waitingToAnchorAnnotations" is-waiting-to-anchor-annotations="vm.waitingToAnchorAnnotations"
is-loading="isLoading" is-loading="vm.isLoading"
selected-tab="selectedTab" selected-tab="vm.selectedTab"
total-annotations="totalAnnotations" total-annotations="vm.totalAnnotations"
total-notes="totalNotes" total-notes="vm.totalNotes"
total-orphans="totalOrphans"> total-orphans="vm.totalOrphans">
</selection-tabs> </selection-tabs>
<search-status-bar <search-status-bar
ng-show="!isLoading()" ng-show="!vm.isLoading()"
filter-active="!!search.query()" filter-active="!!vm.search.query()"
filter-match-count="visibleCount()" filter-match-count="vm.visibleCount()"
on-clear-selection="clearSelection()" on-clear-selection="vm.clearSelection()"
search-query="search ? search.query : ''" search-query="vm.search ? vm.search.query : ''"
selection-count="selectedAnnotationCount()" selection-count="vm.selectedAnnotationCount()"
total-count="topLevelThreadCount()" total-count="vm.topLevelThreadCount()"
selected-tab="selectedTab" selected-tab="vm.selectedTab"
total-annotations="totalAnnotations" total-annotations="vm.totalAnnotations"
total-notes="totalNotes"> total-notes="vm.totalNotes">
</search-status-bar> </search-status-bar>
<div class="annotation-unavailable-message" <div class="annotation-unavailable-message"
ng-if="selectedAnnotationUnavailable()"> ng-if="vm.selectedAnnotationUnavailable()">
<div class="annotation-unavailable-message__icon"></div> <div class="annotation-unavailable-message__icon"></div>
<p class="annotation-unavailable-message__label"> <p class="annotation-unavailable-message__label">
<span ng-if="auth.status === 'logged-out'"> <span ng-if="vm.auth.status === 'logged-out'">
This annotation is not available. This annotation is not available.
<br> <br>
You may need to You may need to
<a class="loggedout-message__link" href="" ng-click="login()">log in</a> <a class="loggedout-message__link" href="" ng-click="vm.login()">log in</a>
to see it. to see it.
</span> </span>
<span ng-if="auth.status === 'logged-in'"> <span ng-if="vm.auth.status === 'logged-in'">
You do not have permission to view this annotation. You do not have permission to view this annotation.
</span> </span>
</p> </p>
</div> </div>
<thread-list <thread-list
on-change-collapsed="setCollapsed(id, collapsed)" on-change-collapsed="vm.setCollapsed(id, collapsed)"
on-clear-selection="clearSelection()" on-clear-selection="vm.clearSelection()"
on-focus="focus(annotation)" on-focus="vm.focus(annotation)"
on-force-visible="forceVisible(thread)" on-force-visible="vm.forceVisible(thread)"
on-select="scrollTo(annotation)" on-select="vm.scrollTo(annotation)"
show-document-info="false" show-document-info="false"
thread="rootThread"> thread="vm.rootThread">
</thread-list> </thread-list>
<loggedout-message ng-if="shouldShowLoggedOutMessage()" on-login="login()"> <loggedout-message ng-if="vm.shouldShowLoggedOutMessage()" on-login="vm.login()">
</loggedout-message> </loggedout-message>
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