Commit b566849d authored by Nick Stenning's avatar Nick Stenning

Move signin control into its own directive

This commit moves the signin and account control (and dropdown) into its
own directive.

To make it easier to understand what's going on at load, I've also
changed the signalling of user signed-in/signed-out state to an explicit
`status` field on the `auth` object, rather than relying on a tristate
of {undefined, null, user object}. This results in templates that are
marginally more verbose, but substantially clearer in intent.

Lastly, I've moved the computation of the `username` and `provider`
properties into AppController so that it doesn't need a `$scope.$watch`.
parent b753477e
angular = require('angular') angular = require('angular')
events = require('./events'); events = require('./events')
parseAccountID = require('./filter/persona').parseAccountID
module.exports = class AppController module.exports = class AppController
this.$inject = [ this.$inject = [
...@@ -15,9 +16,11 @@ module.exports = class AppController ...@@ -15,9 +16,11 @@ module.exports = class AppController
) -> ) ->
$controller('AnnotationUIController', {$scope}) $controller('AnnotationUIController', {$scope})
# This stores information the current userid. # This stores information about the current user's authentication status.
# It is initially undefined until resolved. # When the controller instantiates we do not yet know if the user is
$scope.auth = user: undefined # 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'}
# Allow all child scopes to look up feature flags as: # Allow all child scopes to look up feature flags as:
# #
...@@ -53,9 +56,27 @@ module.exports = class AppController ...@@ -53,9 +56,27 @@ module.exports = class AppController
); );
identity.watch({ identity.watch({
onlogin: (identity) -> $scope.auth.user = auth.userid(identity) onlogin: (identity) ->
onlogout: -> $scope.auth.user = null # Hide the account dialog
onready: -> $scope.auth.user ?= null $scope.accountDialog.visible = false
# Update the current logged-in user information
userid = auth.userid(identity)
parsed = parseAccountID(userid)
angular.copy({
status: 'signed-in',
userid: userid,
username: parsed.username,
provider: parsed.provider,
}, $scope.auth)
onlogout: ->
angular.copy({status: 'signed-out'}, $scope.auth)
onready: ->
# If their status is still 'unknown', then `onlogin` wasn't called and
# we know the current user isn't signed in.
if $scope.auth.status == 'unknown'
angular.copy({status: 'signed-out'}, $scope.auth)
if isFirstRun
$scope.login()
}) })
$scope.$watch 'sort.name', (name) -> $scope.$watch 'sort.name', (name) ->
...@@ -76,20 +97,14 @@ module.exports = class AppController ...@@ -76,20 +97,14 @@ module.exports = class AppController
options: $scope.sort.options, options: $scope.sort.options,
} }
$scope.$watch 'auth.user', (newVal, oldVal) -> # Start the login flow. This will present the user with the login dialog.
return if newVal is oldVal
if isFirstRun and not (newVal or oldVal)
$scope.login()
else
$scope.accountDialog.visible = false
$scope.login = -> $scope.login = ->
$scope.accountDialog.visible = true $scope.accountDialog.visible = true
identity.request({ identity.request({
oncancel: -> $scope.accountDialog.visible = false oncancel: -> $scope.accountDialog.visible = false
}) })
# Log the user out.
$scope.logout = -> $scope.logout = ->
return unless drafts.discard() return unless drafts.discard()
$scope.accountDialog.visible = false $scope.accountDialog.visible = false
......
...@@ -121,6 +121,7 @@ module.exports = angular.module('h', [ ...@@ -121,6 +121,7 @@ module.exports = angular.module('h', [
.directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn')) .directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn'))
.directive('publishAnnotationBtn', require('./directive/publish-annotation-btn')) .directive('publishAnnotationBtn', require('./directive/publish-annotation-btn'))
.directive('searchStatusBar', require('./directive/search-status-bar')) .directive('searchStatusBar', require('./directive/search-status-bar'))
.directive('signinControl', require('./directive/signin-control'))
.directive('sortDropdown', require('./directive/sort-dropdown')) .directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('topBar', require('./directive/top-bar')) .directive('topBar', require('./directive/top-bar'))
......
'use strict';
module.exports = function () {
return {
restrict: 'E',
scope: {
/**
* An object representing the current authentication status.
*/
auth: '=',
/**
* Called when the user clicks on the "Sign in" text.
*/
onLogin: '&',
/**
* Called when the user clicks on the "Sign out" text.
*/
onLogout: '&',
/**
* Whether or not to use the new design for the control.
*
* FIXME: should be removed when the old design is deprecated.
*/
newStyle: '=',
},
templateUrl: 'signin_control.html',
};
};
var parseAccountID = require('../filter/persona').parseAccountID; 'use strict';
module.exports = function () { module.exports = function () {
return { return {
link: function (scope) {
scope.$watch('authUser', function () {
scope.account = parseAccountID(scope.authUser);
});
},
restrict: 'E', restrict: 'E',
scope: { scope: {
authUser: '=', auth: '=',
groupsEnabled: '=', groupsEnabled: '=',
isSidebar: '=', isSidebar: '=',
onLogin: '&', onLogin: '&',
...@@ -23,4 +18,4 @@ module.exports = function () { ...@@ -23,4 +18,4 @@ module.exports = function () {
}, },
templateUrl: 'top_bar.html', templateUrl: 'top_bar.html',
}; };
} };
...@@ -105,24 +105,37 @@ describe 'AppController', -> ...@@ -105,24 +105,37 @@ describe 'AppController', ->
createController() createController()
assert.calledOnce(fakeIdentity.watch) assert.calledOnce(fakeIdentity.watch)
it 'sets the user to null when the identity has been checked', -> it 'auth.status is "unknown" on startup', ->
createController()
assert.equal($scope.auth.status, 'unknown')
it 'sets auth.status to "signed-out" when the identity has been checked but the user is not authenticated', ->
createController() createController()
{onready} = fakeIdentity.watch.args[0][0] {onready} = fakeIdentity.watch.args[0][0]
onready() onready()
assert.isNull($scope.auth.user) assert.equal($scope.auth.status, 'signed-out')
it 'sets auth.status to "signed-in" when the identity has been checked and the user is authenticated', ->
createController()
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe')
{onlogin} = fakeIdentity.watch.args[0][0]
onlogin('test-assertion')
assert.equal($scope.auth.status, 'signed-in')
it 'sets auth.user to the authorized user at login', -> it 'sets userid, username, and provider properties at login', ->
createController() createController()
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe') fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe')
{onlogin} = fakeIdentity.watch.args[0][0] {onlogin} = fakeIdentity.watch.args[0][0]
onlogin('test-assertion') onlogin('test-assertion')
assert.equal($scope.auth.user, 'acct:hey@joe') assert.equal($scope.auth.userid, 'acct:hey@joe')
assert.equal($scope.auth.username, 'hey')
assert.equal($scope.auth.provider, 'joe')
it 'sets auth.user to null at logout', -> it 'sets auth.status to "signed-out" at logout', ->
createController() createController()
{onlogout} = fakeIdentity.watch.args[0][0] {onlogout} = fakeIdentity.watch.args[0][0]
onlogout() onlogout()
assert.strictEqual($scope.auth.user, null) assert.equal($scope.auth.status, "signed-out")
it 'does not show login form for logged in users', -> it 'does not show login form for logged in users', ->
createController() createController()
......
<!-- If we don't yet know the authenticated user -->
<span ng-if="auth.status === 'unknown'"></span>
<!-- If the user is signed out -->
<span><a href=""
ng-click="onLogin()"
ng-if="auth.status === 'signed-out'">Sign in</a><span>
<!-- New controls -->
<div ng-if="newStyle"
class="pull-right user-picker"
dropdown
keyboard-nav>
<a role="button"
class="top-bar__btn"
data-toggle="dropdown"
dropdown-toggle
title="{{auth.username}}">
<i class="h-icon-account"></i><!--
!--><i class="h-icon-arrow-drop-down top-bar__dropdown-arrow"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu" ng-if="newStyle">
<li class="dropdown-menu__row" ng-show="auth.status === 'signed-in'">
<a href="/stream?q=user:{{auth.username}}"
class="dropdown-menu__link"
title="View all your annotations"
target="_blank">{{auth.username}}</a>
</li>
<li class="dropdown-menu__row" ng-show="auth.status === 'signed-in'">
<a class="dropdown-menu__link" href="/profile" target="_blank">Account settings</a>
</li>
<li class="dropdown-menu__row">
<a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a>
</li>
<li class="dropdown-menu__row">
<a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a>
</li>
<li class="dropdown-menu__row" ng-show="auth.status === 'signed-in'">
<a class="dropdown-menu__link dropdown-menu__link--subtle"
href="" ng-click="onLogout()">Sign out</a>
</li>
</ul>
</div>
<!-- Old controls -->
<div ng-if="!newStyle"
class="pull-right user-picker"
dropdown
keyboard-nav>
<span role="button" data-toggle="dropdown" dropdown-toggle>
{{auth.username}}<!--
--><span class="provider"
ng-if="auth.provider">/{{auth.provider}}</span><!--
--><i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li class="dropdown-menu__row" ng-show="auth.status === 'signed-in'">
<a class="dropdown-menu__link" href="/profile" target="_blank">Account</a>
</li>
<li class="dropdown-menu__row" >
<a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a>
</li>
<li class="dropdown-menu__row" >
<a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a>
</li>
<li class="dropdown-menu__row" ng-show="auth.status === 'signed-in'">
<a class="dropdown-menu__link" href="/stream?q=user:{{auth.username}}"
target="_blank">My Annotations</a>
</li>
<li class="dropdown-menu__row" ng-show="auth.status === 'signed-in'">
<a class="dropdown-menu__link" href="" ng-click="onLogout()">Sign out</a>
</li>
</ul>
</div>
...@@ -9,43 +9,20 @@ ...@@ -9,43 +9,20 @@
title="Share this page"> title="Share this page">
<i class="h-icon-share"></i> <i class="h-icon-share"></i>
</button> </button>
<simple-search
<simple-search class="simple-search" class="simple-search"
query="searchController.query" query="searchController.query"
on-search="searchController.update(query)" on-search="searchController.update(query)"
on-clear="searchController.clear()" on-clear="searchController.clear()"
always-expanded="true"></simple-search> always-expanded="true">
</simple-search>
<div class="top-bar__expander"></div> <div class="top-bar__expander"></div>
<signin-control
<div ng-switch="authUser"> auth="auth"
<span ng-switch-when="undefined"></span> new-style="false"
<a href="" ng-click="onLogin()" ng-switch-when="null">Sign in</a> on-login="onLogin()"
<div class="pull-right user-picker" dropdown keyboard-nav> on-logout="onLogout()">
<span role="button" </signin-control>
data-toggle="dropdown" dropdown-toggle>
{{account.username}}<span class="provider" ng-show="authUser">/{{account.provider}}</span><i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li class="dropdown-menu__row" ng-show="authUser">
<a class="dropdown-menu__link" href="/profile" target="_blank">Account</a>
</li>
<li class="dropdown-menu__row" >
<a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a>
</li>
<li class="dropdown-menu__row" >
<a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a>
</li>
<li class="dropdown-menu__row" ng-show="authUser">
<a class="dropdown-menu__link" href="/stream?q=user:{{account.username}}"
target="_blank">My Annotations</a>
</li>
<li class="dropdown-menu__row" ng-show="authUser">
<a class="dropdown-menu__link" href="" ng-click="onLogout()">Sign out</a>
</li>
</ul>
</div>
</div>
</div> </div>
<!-- New design for the top bar. <!-- New design for the top bar.
This is part of the groups roll-out This is part of the groups roll-out
...@@ -54,14 +31,15 @@ ...@@ -54,14 +31,15 @@
the stream view. the stream view.
!--> !-->
<div class="top-bar__inner content" ng-if="groupsEnabled"> <div class="top-bar__inner content" ng-if="groupsEnabled">
<group-list class="group-list"> <group-list class="group-list"></group-list>
</group-list>
<div class="top-bar__expander"></div> <div class="top-bar__expander"></div>
<simple-search class="simple-search" <simple-search
class="simple-search"
query="searchController.query" query="searchController.query"
on-search="searchController.update(query)" on-search="searchController.update(query)"
on-clear="searchController.clear()" on-clear="searchController.clear()"
title="Filter the annotation list"></simple-search> title="Filter the annotation list">
</simple-search>
<sort-dropdown <sort-dropdown
sort-options="sortOptions" sort-options="sortOptions"
sort-by="sortBy" sort-by="sortBy"
...@@ -74,39 +52,11 @@ ...@@ -74,39 +52,11 @@
title="Share this page"> title="Share this page">
<i class="h-icon-share"></i> <i class="h-icon-share"></i>
</a> </a>
<div ng-switch="authUser"> <signin-control
<span ng-switch-when="undefined"></span> auth="auth"
<a href="" ng-click="onLogin()" ng-switch-when="null">Sign in</a> new-style="true"
<div class="pull-right user-picker" dropdown keyboard-nav> on-login="onLogin()"
<a role="button" on-logout="onLogout()">
class="top-bar__btn" </signin-control>
data-toggle="dropdown" dropdown-toggle
title="{{account.username}}">
<i class="h-icon-account"></i><!-- nospace
!--><i class="h-icon-arrow-drop-down top-bar__dropdown-arrow"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li class="dropdown-menu__row" ng-show="authUser">
<a href="/stream?q=user:{{account.username}}"
class="dropdown-menu__link"
title="View all your annotations"
target="_blank">{{account.username}}</a>
</li>
<li class="dropdown-menu__row" ng-show="authUser">
<a class="dropdown-menu__link" href="/profile" target="_blank">Account settings</a>
</li>
<li class="dropdown-menu__row">
<a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a>
</li>
<li class="dropdown-menu__row">
<a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a>
</li>
<li class="dropdown-menu__row" ng-show="authUser">
<a class="dropdown-menu__link dropdown-menu__link--subtle"
href="" ng-click="onLogout()">Sign out</a>
</li>
</ul>
</div>
</div>
</div> </div>
</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