Commit 9bd20062 authored by Robert Knight's avatar Robert Knight

Convert the `<hypothesis-app>` directive to a component

parent 53cebeca
...@@ -23,54 +23,55 @@ function authStateFromUserID(userid) { ...@@ -23,54 +23,55 @@ function authStateFromUserID(userid) {
} }
// @ngInject // @ngInject
module.exports = function AppController( function HypothesisAppController(
$document, $location, $rootScope, $route, $scope, $document, $location, $rootScope, $route, $scope,
$window, analytics, annotationUI, auth, bridge, drafts, features, frameSync, groups, $window, analytics, annotationUI, auth, bridge, drafts, features, frameSync, groups,
serviceUrl, session, settings, streamer serviceUrl, session, settings, streamer
) { ) {
var self = this;
// This stores information about the current user's authentication status. // This stores information about the current user's authentication status.
// When the controller instantiates we do not yet know if the user is // When the controller instantiates we do not yet know if the user is
// logged-in or not, so it has an initial status of 'unknown'. This can be // logged-in or not, so it has an initial status of 'unknown'. This can be
// used by templates to show an intermediate or loading state. // used by templates to show an intermediate or loading state.
$scope.auth = {status: 'unknown'}; this.auth = {status: 'unknown'};
// Allow all child scopes to look up feature flags as: // Allow all child scopes to look up feature flags as:
// //
// if ($scope.feature('foo')) { ... } // if ($scope.feature('foo')) { ... }
$scope.feature = features.flagEnabled; this.feature = features.flagEnabled;
// Allow all child scopes access to the session // Allow all child scopes access to the session
$scope.session = session; this.session = session;
// App dialogs // App dialogs
$scope.accountDialog = {visible: false}; this.accountDialog = {visible: false};
$scope.shareDialog = {visible: false}; this.shareDialog = {visible: false};
$scope.helpPanel = {visible: false}; this.helpPanel = {visible: false};
// Check to see if we're in the sidebar, or on a standalone page such as // Check to see if we're in the sidebar, or on a standalone page such as
// the stream page or an individual annotation page. // the stream page or an individual annotation page.
$scope.isSidebar = $window.top !== $window; this.isSidebar = $window.top !== $window;
if ($scope.isSidebar) { if (this.isSidebar) {
frameSync.connect(); frameSync.connect();
} }
$scope.serviceUrl = serviceUrl; this.serviceUrl = serviceUrl;
$scope.sortKey = function () { this.sortKey = function () {
return annotationUI.getState().sortKey; return annotationUI.getState().sortKey;
}; };
$scope.sortKeysAvailable = function () { this.sortKeysAvailable = function () {
return annotationUI.getState().sortKeysAvailable; return annotationUI.getState().sortKeysAvailable;
}; };
$scope.setSortKey = annotationUI.setSortKey; this.setSortKey = annotationUI.setSortKey;
// Reload the view when the user switches accounts // Reload the view when the user switches accounts
$scope.$on(events.USER_CHANGED, function (event, data) { $scope.$on(events.USER_CHANGED, function (event, data) {
$scope.auth = authStateFromUserID(data.userid); self.auth = authStateFromUserID(data.userid);
$scope.accountDialog.visible = false; self.accountDialog.visible = false;
if (!data || !data.initialLoad) { if (!data || !data.initialLoad) {
$route.reload(); $route.reload();
...@@ -81,10 +82,10 @@ module.exports = function AppController( ...@@ -81,10 +82,10 @@ module.exports = function AppController(
// When the authentication status of the user is known, // When the authentication status of the user is known,
// update the auth info in the top bar and show the login form // update the auth info in the top bar and show the login form
// after first install of the extension. // after first install of the extension.
$scope.auth = authStateFromUserID(state.userid); self.auth = authStateFromUserID(state.userid);
if (!state.userid && settings.openLoginForm) { if (!state.userid && settings.openLoginForm) {
$scope.login(); self.login();
} }
}); });
...@@ -98,23 +99,23 @@ module.exports = function AppController( ...@@ -98,23 +99,23 @@ module.exports = function AppController(
} }
// Start the login flow. This will present the user with the login dialog. // Start the login flow. This will present the user with the login dialog.
$scope.login = function () { this.login = function () {
if (serviceConfig(settings)) { if (serviceConfig(settings)) {
bridge.call(bridgeEvents.DO_LOGIN); bridge.call(bridgeEvents.DO_LOGIN);
return; return;
} }
$scope.accountDialog.visible = true; self.accountDialog.visible = true;
scrollToView('login-form'); scrollToView('login-form');
}; };
$scope.signUp = function(){ this.signUp = function(){
analytics.track(analytics.events.SIGN_UP_REQUESTED); analytics.track(analytics.events.SIGN_UP_REQUESTED);
}; };
// Display the dialog for sharing the current page // Display the dialog for sharing the current page
$scope.share = function () { this.share = function () {
$scope.shareDialog.visible = true; this.shareDialog.visible = true;
scrollToView('share-dialog'); scrollToView('share-dialog');
}; };
...@@ -133,7 +134,7 @@ module.exports = function AppController( ...@@ -133,7 +134,7 @@ module.exports = function AppController(
}; };
// Log the user out. // Log the user out.
$scope.logout = function () { this.logout = function () {
if (!promptToLogout()) { if (!promptToLogout()) {
return; return;
} }
...@@ -141,11 +142,11 @@ module.exports = function AppController( ...@@ -141,11 +142,11 @@ module.exports = function AppController(
$rootScope.$emit(events.ANNOTATION_DELETED, draft); $rootScope.$emit(events.ANNOTATION_DELETED, draft);
}); });
drafts.discard(); drafts.discard();
$scope.accountDialog.visible = false; this.accountDialog.visible = false;
session.logout(); session.logout();
}; };
$scope.search = { this.search = {
query: function () { query: function () {
return annotationUI.getState().filterQuery; return annotationUI.getState().filterQuery;
}, },
...@@ -154,6 +155,12 @@ module.exports = function AppController( ...@@ -154,6 +155,12 @@ module.exports = function AppController(
}, },
}; };
$scope.countPendingUpdates = streamer.countPendingUpdates; this.countPendingUpdates = streamer.countPendingUpdates;
$scope.applyPendingUpdates = streamer.applyPendingUpdates; this.applyPendingUpdates = streamer.applyPendingUpdates;
}
module.exports = {
controller: HypothesisAppController,
controllerAs: 'vm',
template: require('./templates/app.html'),
}; };
...@@ -60,24 +60,22 @@ function configureLocation($locationProvider) { ...@@ -60,24 +60,22 @@ function configureLocation($locationProvider) {
// @ngInject // @ngInject
function configureRoutes($routeProvider) { function configureRoutes($routeProvider) {
// The `vm.{auth,search}` properties used in these templates come from the
// `<hypothesis-app>` component which hosts the router's container element.
$routeProvider.when('/a/:id', $routeProvider.when('/a/:id',
{ {
template: '<annotation-viewer-content search="search"></annotation-viewer-content>', template: '<annotation-viewer-content search="vm.search"></annotation-viewer-content>',
reloadOnSearch: false, reloadOnSearch: false,
resolve: resolve, resolve: resolve,
}); });
$routeProvider.when('/stream', $routeProvider.when('/stream',
{ {
template: '<stream-content search="search"></stream-content>', template: '<stream-content search="vm.search"></stream-content>',
reloadOnSearch: false, reloadOnSearch: false,
resolve: resolve, resolve: resolve,
}); });
$routeProvider.otherwise({ $routeProvider.otherwise({
// Trivial template for use until the other controllers are also converted template: '<sidebar-content search="vm.search" auth="vm.auth"></sidebar-content>',
// to components and we can remove the router entirely.
//
// The "search" and "auth" properties are provided by "AppController".
template: '<sidebar-content search="search" auth="auth"></sidebar-content>',
reloadOnSearch: false, reloadOnSearch: false,
resolve: resolve, resolve: resolve,
}); });
...@@ -128,7 +126,7 @@ module.exports = angular.module('h', [ ...@@ -128,7 +126,7 @@ module.exports = angular.module('h', [
]) ])
// The root component for the application // The root component for the application
.directive('hypothesisApp', require('./directive/app')) .component('hypothesisApp', require('./app-controller'))
// UI components // UI components
.component('annotation', require('./components/annotation').component) .component('annotation', require('./components/annotation').component)
......
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
var AppController = require('../app-controller'); var AppController = require('../app-controller');
module.exports = function () { module.exports = {
return { controllerAs: 'vm',
restrict: 'E',
controller: AppController, controller: AppController,
scope: {},
template: require('../templates/app.html'), template: require('../templates/app.html'),
};
}; };
<div class="app-content-wrapper" h-branding="appBackgroundColor"> <div class="app-content-wrapper" h-branding="appBackgroundColor">
<top-bar <top-bar
auth="auth" auth="vm.auth"
on-login="login()" on-login="vm.login()"
on-sign-up="signUp()" on-sign-up="vm.signUp()"
on-logout="logout()" on-logout="vm.logout()"
on-share-page="share()" on-share-page="vm.share()"
on-show-help-panel="helpPanel.visible = true" on-show-help-panel="vm.helpPanel.visible = true"
is-sidebar="::isSidebar" is-sidebar="::vm.isSidebar"
pending-update-count="countPendingUpdates()" pending-update-count="vm.countPendingUpdates()"
on-apply-pending-updates="applyPendingUpdates()" on-apply-pending-updates="vm.applyPendingUpdates()"
search-controller="search" search-controller="vm.search"
sort-key="sortKey()" sort-key="vm.sortKey()"
sort-keys-available="sortKeysAvailable()" sort-keys-available="vm.sortKeysAvailable()"
on-change-sort-key="setSortKey(sortKey)"> on-change-sort-key="vm.setSortKey(sortKey)">
</top-bar> </top-bar>
<div class="create-account-banner" ng-if="isSidebar && auth.status === 'logged-out'" ng-cloak> <div class="create-account-banner" ng-if="vm.isSidebar && vm.auth.status === 'logged-out'" ng-cloak>
To annotate this document To annotate this document
<a href="{{ serviceUrl('signup') }}" target="_blank"> <a href="{{ vm.serviceUrl('signup') }}" target="_blank">
create a free account create a free account
</a> </a>
or <a href="" ng-click="login()">log in</a> or <a href="" ng-click="vm.login()">log in</a>
</div> </div>
<div class="content" ng-cloak> <div class="content" ng-cloak>
<login-form <login-form
ng-if="accountDialog.visible" ng-if="vm.accountDialog.visible"
on-close="accountDialog.visible = false"> on-close="vm.accountDialog.visible = false">
</login-form> </login-form>
<sidebar-tutorial ng-if="isSidebar"></sidebar-tutorial> <sidebar-tutorial ng-if="vm.isSidebar"></sidebar-tutorial>
<share-dialog <share-dialog
ng-if="shareDialog.visible" ng-if="vm.shareDialog.visible"
on-close="shareDialog.visible = false"> on-close="vm.shareDialog.visible = false">
</share-dialog> </share-dialog>
<help-panel ng-if="helpPanel.visible" <help-panel ng-if="vm.helpPanel.visible"
on-close="helpPanel.visible = false" on-close="vm.helpPanel.visible = false"
auth="auth"> auth="vm.auth">
</help-panel> </help-panel>
<main ng-view=""></main> <main ng-view=""></main>
</div> </div>
......
...@@ -7,8 +7,8 @@ var events = require('../events'); ...@@ -7,8 +7,8 @@ var events = require('../events');
var bridgeEvents = require('../../shared/bridge-events'); var bridgeEvents = require('../../shared/bridge-events');
var util = require('../../shared/test/util'); var util = require('../../shared/test/util');
describe('AppController', function () { describe('hypothesisApp', function () {
var $controller = null; var $componentController = null;
var $scope = null; var $scope = null;
var $rootScope = null; var $rootScope = null;
var fakeAnnotationMetadata = null; var fakeAnnotationMetadata = null;
...@@ -35,7 +35,7 @@ describe('AppController', function () { ...@@ -35,7 +35,7 @@ describe('AppController', function () {
var createController = function (locals) { var createController = function (locals) {
locals = locals || {}; locals = locals || {};
locals.$scope = $scope; locals.$scope = $scope;
return $controller('AppController', locals); return $componentController('hypothesisApp', locals);
}; };
beforeEach(function () { beforeEach(function () {
...@@ -49,14 +49,14 @@ describe('AppController', function () { ...@@ -49,14 +49,14 @@ describe('AppController', function () {
fakeServiceConfig = sandbox.stub(); fakeServiceConfig = sandbox.stub();
var AppController = proxyquire('../app-controller', util.noCallThru({ var component = proxyquire('../app-controller', util.noCallThru({
'angular': angular, 'angular': angular,
'./annotation-metadata': fakeAnnotationMetadata, './annotation-metadata': fakeAnnotationMetadata,
'./service-config': fakeServiceConfig, './service-config': fakeServiceConfig,
})); }));
angular.module('h', []) angular.module('h', [])
.controller('AppController', AppController); .component('hypothesisApp', component);
}); });
beforeEach(angular.mock.module('h')); beforeEach(angular.mock.module('h'));
...@@ -140,8 +140,8 @@ describe('AppController', function () { ...@@ -140,8 +140,8 @@ describe('AppController', function () {
$provide.value('$window', fakeWindow); $provide.value('$window', fakeWindow);
})); }));
beforeEach(angular.mock.inject(function (_$controller_, _$rootScope_) { beforeEach(angular.mock.inject(function (_$componentController_, _$rootScope_) {
$controller = _$controller_; $componentController = _$componentController_;
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
$scope = $rootScope.$new(); $scope = $rootScope.$new();
})); }));
...@@ -154,14 +154,14 @@ describe('AppController', function () { ...@@ -154,14 +154,14 @@ describe('AppController', function () {
it('is false if the window is the top window', function () { it('is false if the window is the top window', function () {
fakeWindow.top = fakeWindow; fakeWindow.top = fakeWindow;
createController(); var ctrl = createController();
assert.isFalse($scope.isSidebar); assert.isFalse(ctrl.isSidebar);
}); });
it('is true if the window is not the top window', function () { it('is true if the window is not the top window', function () {
fakeWindow.top = {}; fakeWindow.top = {};
createController(); var ctrl = createController();
assert.isTrue($scope.isSidebar); assert.isTrue(ctrl.isSidebar);
}); });
}); });
...@@ -178,14 +178,14 @@ describe('AppController', function () { ...@@ -178,14 +178,14 @@ describe('AppController', function () {
}); });
it('auth.status is "unknown" on startup', function () { it('auth.status is "unknown" on startup', function () {
createController(); var ctrl = createController();
assert.equal($scope.auth.status, 'unknown'); assert.equal(ctrl.auth.status, 'unknown');
}); });
it('sets auth.status to "logged-out" if userid is null', function () { it('sets auth.status to "logged-out" if userid is null', function () {
createController(); var ctrl = createController();
return fakeSession.load().then(function () { return fakeSession.load().then(function () {
assert.equal($scope.auth.status, 'logged-out'); assert.equal(ctrl.auth.status, 'logged-out');
}); });
}); });
...@@ -193,9 +193,9 @@ describe('AppController', function () { ...@@ -193,9 +193,9 @@ describe('AppController', function () {
fakeSession.load = function () { fakeSession.load = function () {
return Promise.resolve({userid: 'acct:jim@hypothes.is'}); return Promise.resolve({userid: 'acct:jim@hypothes.is'});
}; };
createController(); var ctrl = createController();
return fakeSession.load().then(function () { return fakeSession.load().then(function () {
assert.equal($scope.auth.status, 'logged-in'); assert.equal(ctrl.auth.status, 'logged-in');
}); });
}); });
...@@ -203,22 +203,22 @@ describe('AppController', function () { ...@@ -203,22 +203,22 @@ describe('AppController', function () {
fakeSession.load = function () { fakeSession.load = function () {
return Promise.resolve({userid: 'acct:jim@hypothes.is'}); return Promise.resolve({userid: 'acct:jim@hypothes.is'});
}; };
createController(); var ctrl = createController();
return fakeSession.load().then(function () { return fakeSession.load().then(function () {
assert.equal($scope.auth.userid, 'acct:jim@hypothes.is'); assert.equal(ctrl.auth.userid, 'acct:jim@hypothes.is');
assert.equal($scope.auth.username, 'jim'); assert.equal(ctrl.auth.username, 'jim');
assert.equal($scope.auth.provider, 'hypothes.is'); assert.equal(ctrl.auth.provider, 'hypothes.is');
}); });
}); });
it('updates auth when the logged-in user changes', function () { it('updates auth when the logged-in user changes', function () {
createController(); var ctrl = createController();
return fakeSession.load().then(function () { return fakeSession.load().then(function () {
$scope.$broadcast(events.USER_CHANGED, { $scope.$broadcast(events.USER_CHANGED, {
initialLoad: false, initialLoad: false,
userid: 'acct:john@hypothes.is', userid: 'acct:john@hypothes.is',
}); });
assert.deepEqual($scope.auth, { assert.deepEqual(ctrl.auth, {
status: 'logged-in', status: 'logged-in',
userid: 'acct:john@hypothes.is', userid: 'acct:john@hypothes.is',
username: 'john', username: 'john',
...@@ -227,19 +227,19 @@ describe('AppController', function () { ...@@ -227,19 +227,19 @@ describe('AppController', function () {
}); });
}); });
it('exposes the serviceUrl on the scope', function () { it('exposes the serviceUrl on the controller', function () {
createController(); var ctrl = createController();
assert.equal($scope.serviceUrl, fakeServiceUrl); assert.equal(ctrl.serviceUrl, fakeServiceUrl);
}); });
it('does not show login form for logged in users', function () { it('does not show login form for logged in users', function () {
createController(); var ctrl = createController();
assert.isFalse($scope.accountDialog.visible); assert.isFalse(ctrl.accountDialog.visible);
}); });
it('does not show the share dialog at start', function () { it('does not show the share dialog at start', function () {
createController(); var ctrl = createController();
assert.isFalse($scope.shareDialog.visible); assert.isFalse(ctrl.shareDialog.visible);
}); });
it('does not reload the view when the logged-in user changes on first load', function () { it('does not reload the view when the logged-in user changes on first load', function () {
...@@ -257,8 +257,8 @@ describe('AppController', function () { ...@@ -257,8 +257,8 @@ describe('AppController', function () {
}); });
it('tracks sign up requests in analytics', function () { it('tracks sign up requests in analytics', function () {
createController(); var ctrl = createController();
$scope.signUp(); ctrl.signUp();
assert.calledWith(fakeAnalytics.track, fakeAnalytics.events.SIGN_UP_REQUESTED); assert.calledWith(fakeAnalytics.track, fakeAnalytics.events.SIGN_UP_REQUESTED);
}); });
...@@ -266,9 +266,9 @@ describe('AppController', function () { ...@@ -266,9 +266,9 @@ describe('AppController', function () {
it('shows the login dialog if not using a third-party service', function () { it('shows the login dialog if not using a third-party service', function () {
// If no third-party annotation service is in use then it should show the // If no third-party annotation service is in use then it should show the
// built-in login dialog. // built-in login dialog.
createController(); var ctrl = createController();
$scope.login(); ctrl.login();
assert.equal($scope.accountDialog.visible, true); assert.equal(ctrl.accountDialog.visible, true);
}); });
it('sends DO_LOGIN if a third-party service is in use', function () { it('sends DO_LOGIN if a third-party service is in use', function () {
...@@ -277,9 +277,9 @@ describe('AppController', function () { ...@@ -277,9 +277,9 @@ describe('AppController', function () {
// (so that the partner site we're embedded in can do its own login // (so that the partner site we're embedded in can do its own login
// thing). // thing).
fakeServiceConfig.returns({}); fakeServiceConfig.returns({});
createController(); var ctrl = createController();
$scope.login(); ctrl.login();
assert.equal(fakeBridge.call.callCount, 1); assert.equal(fakeBridge.call.callCount, 1);
assert.isTrue(fakeBridge.call.calledWithExactly(bridgeEvents.DO_LOGIN)); assert.isTrue(fakeBridge.call.calledWithExactly(bridgeEvents.DO_LOGIN));
...@@ -288,24 +288,24 @@ describe('AppController', function () { ...@@ -288,24 +288,24 @@ describe('AppController', function () {
describe('#share()', function () { describe('#share()', function () {
it('shows the share dialog', function () { it('shows the share dialog', function () {
createController(); var ctrl = createController();
$scope.share(); ctrl.share();
assert.equal($scope.shareDialog.visible, true); assert.equal(ctrl.shareDialog.visible, true);
}); });
}); });
describe('#logout()', function () { describe('#logout()', function () {
it('calls session.logout()', function () { it('calls session.logout()', function () {
createController(); var ctrl = createController();
$scope.logout(); ctrl.logout();
assert.called(fakeSession.logout); assert.called(fakeSession.logout);
}); });
it('prompts the user if there are drafts', function () { it('prompts the user if there are drafts', function () {
fakeDrafts.count.returns(1); fakeDrafts.count.returns(1);
createController(); var ctrl = createController();
$scope.logout(); ctrl.logout();
assert.equal(fakeWindow.confirm.callCount, 1); assert.equal(fakeWindow.confirm.callCount, 1);
}); });
...@@ -314,10 +314,10 @@ describe('AppController', function () { ...@@ -314,10 +314,10 @@ describe('AppController', function () {
fakeDrafts.unsaved = sandbox.stub().returns( fakeDrafts.unsaved = sandbox.stub().returns(
['draftOne', 'draftTwo', 'draftThree'] ['draftOne', 'draftTwo', 'draftThree']
); );
createController(); var ctrl = createController();
$rootScope.$emit = sandbox.stub(); $rootScope.$emit = sandbox.stub();
$scope.logout(); ctrl.logout();
assert($rootScope.$emit.calledThrice); assert($rootScope.$emit.calledThrice);
assert.deepEqual( assert.deepEqual(
...@@ -329,39 +329,39 @@ describe('AppController', function () { ...@@ -329,39 +329,39 @@ describe('AppController', function () {
}); });
it('discards draft annotations', function () { it('discards draft annotations', function () {
createController(); var ctrl = createController();
$scope.logout(); ctrl.logout();
assert(fakeDrafts.discard.calledOnce); assert(fakeDrafts.discard.calledOnce);
}); });
it('does not emit "annotationDeleted" if the user cancels the prompt', function () { it('does not emit "annotationDeleted" if the user cancels the prompt', function () {
createController(); var ctrl = createController();
fakeDrafts.count.returns(1); fakeDrafts.count.returns(1);
$rootScope.$emit = sandbox.stub(); $rootScope.$emit = sandbox.stub();
fakeWindow.confirm.returns(false); fakeWindow.confirm.returns(false);
$scope.logout(); ctrl.logout();
assert($rootScope.$emit.notCalled); assert($rootScope.$emit.notCalled);
}); });
it('does not discard drafts if the user cancels the prompt', function () { it('does not discard drafts if the user cancels the prompt', function () {
createController(); var ctrl = createController();
fakeDrafts.count.returns(1); fakeDrafts.count.returns(1);
fakeWindow.confirm.returns(false); fakeWindow.confirm.returns(false);
$scope.logout(); ctrl.logout();
assert(fakeDrafts.discard.notCalled); assert(fakeDrafts.discard.notCalled);
}); });
it('does not prompt if there are no drafts', function () { it('does not prompt if there are no drafts', function () {
createController(); var ctrl = createController();
fakeDrafts.count.returns(0); fakeDrafts.count.returns(0);
$scope.logout(); ctrl.logout();
assert.equal(fakeWindow.confirm.callCount, 0); assert.equal(fakeWindow.confirm.callCount, 0);
}); });
......
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