Commit b39a0869 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #317 from hypothesis/hypothesis-app-component

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