Commit 4c0cdba3 authored by Robert Knight's avatar Robert Knight

Merge pull request #2636 from hypothesis/replace-profile-form

Replace client-side profile form
parents 17954cf0 7d30a9ea
var angular = require('angular');
// @ngInject
function AccountController($scope, $filter, auth, flash, formRespond, identity,
session) {
var personaFilter = $filter('persona');
$scope.subscriptionDescription = {
reply: 'Someone replies to one of my annotations'
};
function onSuccess(form, response) {
// Fire flash messages
for (var type in response.flash) {
response.flash[type].map(function (message) {
flash[type](message);
});
}
form.$setPristine();
var formModel = form.$name.slice(0, -4);
// Reset form fields
$scope[formModel] = {};
// Update status button
$scope.$broadcast('formState', form.$name, 'success');
$scope.email = response.email;
};
function onDelete(form, response) {
identity.logout();
onSuccess(form, response);
};
function onError(form, response) {
if (response.status >= 400 && response.status < 500) {
formRespond(form, response.data.errors);
} else {
if (response.data.flash) {
for (type in response.data.flash) {
response.data.flash[type].map(function (message) {
flash[type](message);
});
}
} else {
flash.error('Sorry, we were unable to perform your request');
}
}
// Update status button
$scope.$broadcast('formState', form.$name, '');
};
$scope.tab = 'Account';
session.profile().$promise.then(function(result) {
$scope.subscriptions = result.subscriptions;
$scope.email = result.email;
});
// Data for each of the forms
$scope.editProfile = {};
$scope.changePassword = {};
$scope.deleteAccount = {};
$scope.delete = function(form) {
// If the password is correct, the account is deleted.
// The extension is then removed from the page.
// Confirmation of success is given.
if (!form.$valid) {
return;
}
var username = personaFilter(auth.user);
var packet = {
username: username,
pwd: form.pwd.$modelValue
};
var successHandler = angular.bind(null, onDelete, form);
var errorHandler = angular.bind(null, onError, form);
var promise = session.disable_user(packet).$promise;
return promise.then(successHandler, errorHandler);
};
$scope.submit = function(form) {
formRespond(form);
if (!form.$valid) {
return;
}
var username = personaFilter(auth.user);
var packet = {
username: username,
pwd: form.pwd.$modelValue,
password: form.password.$modelValue
};
var successHandler = angular.bind(null, onSuccess, form);
var errorHandler = angular.bind(null, onError, form);
// Update status button
$scope.$broadcast('formState', form.$name, 'loading');
var promise = session.edit_profile(packet).$promise;
return promise.then(successHandler, errorHandler);
};
$scope.changeEmailSubmit = function(form) {
formRespond(form);
if (!form.$valid) {
return;
}
var username = personaFilter(auth.user);
var packet = {
username: username,
pwd: form.pwd.$modelValue,
email: form.email.$modelValue,
emailAgain: form.emailAgain.$modelValue
};
var successHandler = angular.bind(null, onSuccess, form);
var errorHandler = angular.bind(null, onError, form);
// Update status button
$scope.$broadcast('formState', form.$name, 'loading');
var promise = session.edit_profile(packet).$promise;
return promise.then(successHandler, errorHandler);
};
$scope.updated = function(index, form) {
var packet = {
username: auth.user,
subscriptions: JSON.stringify($scope.subscriptions[index])
};
var successHandler = angular.bind(null, onSuccess, form);
var errorHandler = angular.bind(null, onError, form);
var promise = session.edit_profile(packet).$promise;
return promise.then(successHandler, errorHandler);
};
}
module.exports = AccountController;
...@@ -86,7 +86,6 @@ module.exports = angular.module('h', [ ...@@ -86,7 +86,6 @@ module.exports = angular.module('h', [
'angulartics' 'angulartics'
'angulartics.google.analytics' 'angulartics.google.analytics'
'angular-jwt' 'angular-jwt'
'bootstrap'
'ngAnimate' 'ngAnimate'
'ngResource' 'ngResource'
'ngRoute' 'ngRoute'
...@@ -98,7 +97,6 @@ module.exports = angular.module('h', [ ...@@ -98,7 +97,6 @@ module.exports = angular.module('h', [
]) ])
.controller('AppController', require('./app-controller')) .controller('AppController', require('./app-controller'))
.controller('AccountController', require('./account-controller'))
.controller('AnnotationUIController', require('./annotation-ui-controller')) .controller('AnnotationUIController', require('./annotation-ui-controller'))
.controller('AnnotationViewerController', require('./annotation-viewer-controller')) .controller('AnnotationViewerController', require('./annotation-viewer-controller'))
.controller('AuthController', require('./auth-controller')) .controller('AuthController', require('./auth-controller'))
...@@ -116,10 +114,7 @@ module.exports = angular.module('h', [ ...@@ -116,10 +114,7 @@ module.exports = angular.module('h', [
.directive('statusButton', require('./directive/status-button')) .directive('statusButton', require('./directive/status-button'))
.directive('thread', require('./directive/thread')) .directive('thread', require('./directive/thread'))
.directive('threadFilter', require('./directive/thread-filter')) .directive('threadFilter', require('./directive/thread-filter'))
.directive('match', require('./directive/match'))
.directive('spinner', require('./directive/spinner')) .directive('spinner', require('./directive/spinner'))
.directive('tabbable', require('./directive/tabbable'))
.directive('tabReveal', require('./directive/tab-reveal'))
.directive('shareDialog', require('./directive/share-dialog')) .directive('shareDialog', require('./directive/share-dialog'))
.directive('windowScroll', require('./directive/window-scroll')) .directive('windowScroll', require('./directive/window-scroll'))
.directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn')) .directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn'))
......
module.exports = ->
link: (scope, elem, attr, input) ->
validate = ->
scope.$evalAsync ->
input.$setValidity('match', scope.match == input.$modelValue)
elem.on('keyup', validate)
scope.$watch('match', validate)
scope:
match: '='
restrict: 'A'
require: 'ngModel'
module.exports = ['$parse', ($parse) ->
compile: (tElement, tAttrs, transclude) ->
panes = []
hiddenPanesGet = $parse tAttrs.tabReveal
pre: (scope, iElement, iAttrs, [ngModel, tabbable] = controller) ->
# Hijack the tabbable controller's addPane so that the visibility of the
# secret ones can be managed. This avoids traversing the DOM to find
# the tab panes.
addPane = tabbable.addPane
tabbable.addPane = (element, attr) =>
removePane = addPane.call tabbable, element, attr
panes.push
element: element
attr: attr
=>
for i, pane of panes
if pane.element is element
panes.splice i, 1
break
removePane()
post: (scope, iElement, iAttrs, [ngModel, tabbable] = controller) ->
tabs = angular.element(iElement.children()[0].childNodes)
render = angular.bind ngModel, ngModel.$render
ngModel.$render = ->
render()
hiddenPanes = hiddenPanesGet scope
return unless angular.isArray hiddenPanes
for i, pane of panes
value = pane.attr.value || pane.attr.title
if value == ngModel.$viewValue
pane.element.css 'display', ''
angular.element(tabs[i]).css 'display', ''
else if value in hiddenPanes
pane.element.css 'display', 'none'
angular.element(tabs[i]).css 'display', 'none'
require: ['ngModel', 'tabbable']
]
# Extend the tabbable directive from angular-bootstrap with autofocus
module.exports = tabbable = ['$timeout', ($timeout) ->
link: (scope, elem, attrs, ctrl) ->
return unless ctrl
render = ctrl.$render
ctrl.$render = ->
render.call(ctrl)
$timeout ->
elem
.find(':input')
.filter(':visible:first')
.focus()
, false
require: '?ngModel'
restrict: 'C'
]
{module, inject} = angular.mock
describe 'match', ->
$compile = null
$element = null
$isolateScope = null
$scope = null
before ->
angular.module('h', [])
.directive('match', require('../match'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
beforeEach ->
$scope.model = {a: 1, b: 1}
$element = $compile('<input name="confirmation" ng-model="model.b" match="model.a" />')($scope)
$isolateScope = $element.isolateScope()
$scope.$digest()
it 'is valid if both properties have the same value', ->
controller = $element.controller('ngModel')
assert.isFalse(controller.$error.match)
it 'is invalid if the local property differs', ->
$isolateScope.match = 2
$isolateScope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
it 'is invalid if the matched property differs', ->
$scope.model.a = 2
$scope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
it 'is invalid if the input itself is changed', ->
$element.val('2').trigger('input').keyup()
$scope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
...@@ -29,7 +29,6 @@ module.exports = function(config) { ...@@ -29,7 +29,6 @@ module.exports = function(config) {
'../../../node_modules/angular-route/angular-route.js', '../../../node_modules/angular-route/angular-route.js',
'../../../node_modules/angular-sanitize/angular-sanitize.js', '../../../node_modules/angular-sanitize/angular-sanitize.js',
'../../../node_modules/ng-tags-input/build/ng-tags-input.min.js', '../../../node_modules/ng-tags-input/build/ng-tags-input.min.js',
'vendor/angular-bootstrap-tabbable.js',
'vendor/katex.js', 'vendor/katex.js',
// Test deps // Test deps
......
{inject, module} = angular.mock
describe 'h:AccountController', ->
$scope = null
fakeFlash = null
fakeSession = null
fakeIdentity = null
fakeFormRespond = null
fakeAuth = null
editProfilePromise = null
disableUserPromise = null
profilePromise = null
createController = null
sandbox = null
before ->
angular.module('h', [])
.controller('AccountController', require('../account-controller'))
beforeEach module('h')
beforeEach module ($provide, $filterProvider) ->
sandbox = sinon.sandbox.create()
fakeSession = {}
fakeFlash =
success: sandbox.spy()
info: sandbox.spy()
warning: sandbox.spy()
error: sandbox.spy()
fakeIdentity =
logout: sandbox.spy()
fakeFormRespond = sandbox.spy()
fakeAuth =
user: 'egon@columbia.edu'
$filterProvider.register 'persona', ->
sandbox.stub().returns('STUBBED_PERSONA_FILTER')
$provide.value 'session', fakeSession
$provide.value 'flash', fakeFlash
$provide.value 'identity', fakeIdentity
$provide.value 'formRespond', fakeFormRespond
$provide.value 'auth', fakeAuth
return
beforeEach inject ($rootScope, $q, $controller) ->
$scope = $rootScope.$new()
disableUserPromise = {then: sandbox.stub()}
editProfilePromise = {then: sandbox.stub()}
profilePromise = {then: sandbox.stub()}
fakeSession.edit_profile = sandbox.stub().returns($promise: editProfilePromise)
fakeSession.disable_user = sandbox.stub().returns($promise: disableUserPromise)
fakeSession.profile = sandbox.stub().returns($promise: profilePromise)
createController = ->
$controller('AccountController', {$scope: $scope})
afterEach ->
sandbox.restore()
describe '.submit', ->
createFakeForm = (overrides={}) ->
defaults =
$name: 'changePasswordForm'
$valid: true
$setPristine: sandbox.spy()
pwd: $modelValue: 'gozer'
password: $modelValue: 'paranormal'
angular.extend(defaults, overrides)
it 'updates the password on the backend', ->
fakeForm = createFakeForm()
controller = createController()
$scope.submit(fakeForm)
assert.calledWith(fakeSession.edit_profile, {
username: 'STUBBED_PERSONA_FILTER'
pwd: 'gozer'
password: 'paranormal'
})
it 'clears the fields', ->
controller = createController()
$scope.changePassword = {pwd: 'password', password: 'password'}
fakeForm = createFakeForm()
# Resolve the request.
editProfilePromise.then.yields(flash: {
success: ['Your profile has been updated.']
})
$scope.submit(fakeForm)
assert.deepEqual($scope.changePassword, {})
it 'updates the error fields on bad response', ->
fakeForm = createFakeForm()
controller = createController()
$scope.submit(fakeForm)
# Resolve the request.
editProfilePromise.then.callArg 1,
status: 400
data:
errors:
pwd: 'this is wrong'
assert.calledWith fakeFormRespond, fakeForm,
pwd: 'this is wrong'
it 'displays a flash message on success', ->
fakeForm = createFakeForm()
# Resolve the request.
editProfilePromise.then.yields(flash: {
success: ['Your profile has been updated.']
})
controller = createController()
$scope.submit(fakeForm)
assert.calledWith(fakeFlash.success, 'Your profile has been updated.')
it 'displays a flash message if a server error occurs', ->
fakeForm = createFakeForm()
controller = createController()
$scope.submit(fakeForm)
# Resolve the request.
editProfilePromise.then.callArg 1,
status: 500
data:
flash:
error: ['Something bad happened']
assert.calledWith(fakeFlash.error, 'Something bad happened')
it 'displays a fallback flash message if none are present', ->
fakeForm = createFakeForm()
controller = createController()
$scope.submit(fakeForm)
# Resolve the request.
editProfilePromise.then.callArg 1,
status: 500
data: {}
assert.calledWith(fakeFlash.error,
'Sorry, we were unable to perform your request')
describe '.delete', ->
createFakeForm = (overrides={}) ->
defaults =
$name: 'deleteAccountForm'
$valid: true
$setPristine: sandbox.spy()
pwd: $modelValue: 'paranormal'
angular.extend(defaults, overrides)
it 'disables the user account', ->
fakeForm = createFakeForm()
controller = createController()
$scope.delete(fakeForm)
assert.calledWith fakeSession.disable_user,
username: 'STUBBED_PERSONA_FILTER'
pwd: 'paranormal'
it 'logs the user out of the application', ->
fakeForm = createFakeForm()
controller = createController()
$scope.delete(fakeForm)
# Resolve the request.
disableUserPromise.then.callArg 0,
status: 200
assert.calledWith(fakeIdentity.logout)
it 'clears the password field', ->
controller = createController()
fakeForm = createFakeForm()
$scope.deleteAccount = {pwd: ''}
$scope.delete(fakeForm)
disableUserPromise.then.callArg 0,
status: 200
assert.deepEqual($scope.deleteAccount, {})
it 'updates the error fields on bad response', ->
fakeForm = createFakeForm()
controller = createController()
$scope.delete(fakeForm)
# Resolve the request.
disableUserPromise.then.callArg 1,
status: 400
data:
errors:
pwd: 'this is wrong'
assert.calledWith fakeFormRespond, fakeForm,
pwd: 'this is wrong'
it 'displays a flash message if a server error occurs', ->
fakeForm = createFakeForm()
controller = createController()
$scope.delete(fakeForm)
# Resolve the request.
disableUserPromise.then.callArg 1,
status: 500
data:
flash:
error: ['Something bad happened']
assert.calledWith(fakeFlash.error, 'Something bad happened')
it 'displays a fallback toast message if none are present', ->
fakeForm = createFakeForm()
controller = createController()
$scope.delete(fakeForm)
# Resolve the request.
disableUserPromise.then.callArg 1,
status: 500
data: {}
assert.calledWith(fakeFlash.error,
'Sorry, we were unable to perform your request')
describe "h:AccountController", ->
before(->
try
# If this runs without error then the h module has already been defined
# by an earlier top-level describe() in this file.
angular.module("h")
catch error
# The h module hasn't been defined yet, so we need to define it
# (this happens when it.only() is used in this describe()).
angular.module("h", [])
.controller('AccountController', require('../account-controller'))
)
beforeEach module('h')
# Return the $controller service from Angular.
getControllerService = ->
$controller = null
inject((_$controller_) ->
$controller = _$controller_
)
return $controller
# Return the $rootScope service from Angular.
getRootScope = ->
$rootScope = null
inject((_$rootScope_) ->
$rootScope = _$rootScope_
)
return $rootScope
# Return a minimal stub version of h's session service.
getStubSession = ({profile, edit_profile}) ->
return {
profile: -> profile or {$promise: Promise.resolve({})}
edit_profile: edit_profile or -> {$promise: Promise.resolve({})}
}
# Return a minimal stub version of the object that AccountController's
# changeEmailSubmit() method receives when the user submits the changeEmailForm.
getStubChangeEmailForm = ({email, emailAgain, password}) ->
return {
$name: "changeEmailForm"
email:
$modelValue: email
$setValidity: ->
emailAgain:
$modelValue: emailAgain
$setValidity: ->
pwd:
$modelValue: password
$setValidity: ->
$valid: true
$setPristine: ->
$setValidity: ->
}
# Return an AccountController instance and stub services.
createAccountController = ({$scope, $filter, auth, flash, formRespond,
identity, session}) ->
locals = {
$scope: $scope or getRootScope().$new()
$filter: $filter or -> -> {}
auth: auth or {}
flash: flash or {}
formRespond: formRespond or ->
identity: identity or {}
session: session or getStubSession({})
}
locals["ctrl"] = getControllerService()("AccountController", locals)
return locals
###
The controller sets $scope.email to the user's current email address on
controller initialization. The templates use this for the placeholder
value of the email input fields.
###
it "adds the current email address to the scope when initialized", ->
# The controller actually calls session.profile() on init which returns
# a promise, and when that promise resolves it uses the value to set
# $scope.email. So we need to stub that promise here.
profilePromise = Promise.resolve({
email: "test_user@test_email.com"
})
{$scope} = createAccountController(
session: {profile: -> {$promise: profilePromise}})
profilePromise.then(->
assert $scope.email == "test_user@test_email.com"
)
describe "changeEmail", ->
it "calls sesson.edit_profile() with the right data on form submission", ->
new_email_addr = "new_email_address@test.com"
# Stub the session.edit_profile() function.
edit_profile = sinon.stub()
edit_profile.returns({$promise: Promise.resolve({})})
{$scope} = createAccountController(
session: getStubSession(edit_profile: edit_profile)
# Simulate a logged-in user with username "joeuser"
$filter: -> -> "joeuser")
form = getStubChangeEmailForm(
email: new_email_addr, emailAgain: new_email_addr, password: "pass")
$scope.changeEmailSubmit(form).then(->
assert edit_profile.calledWithExactly({
username: "joeuser"
pwd: "pass"
email: new_email_addr
emailAgain: new_email_addr
})
)
it "updates placeholder after successfully changing the email address", ->
new_email_addr = "new_email_address@test.com"
{$scope} = createAccountController(
# AccountController expects session.edit_profile() to respond with the
# newly saved email address.
session: getStubSession(
edit_profile: -> {
$promise: Promise.resolve({email: new_email_addr})
}
)
)
form = getStubChangeEmailForm(
email: new_email_addr, emailAgain: new_email_addr, password: "pass")
$scope.changeEmailSubmit(form).then(->
assert $scope.email == new_email_addr
)
it "shows an error if the emails don't match", ->
server_response = {
status: 400,
statusText: "Bad Request"
data:
errors:
emailAgain: "The emails must match."
}
{$scope} = createAccountController(
formRespond: require("../form-respond")()
session: getStubSession(
edit_profile: -> {$promise: Promise.reject(server_response)}
)
)
form = getStubChangeEmailForm(
email: "my_new_email_address@yahoo.com"
emailAgain: "a_different_email_address@bluebottle.com"
pwd: "pass")
$scope.changeEmailSubmit(form).then(->
assert form.emailAgain.responseErrorMessage == "The emails must match."
)
it "broadcasts 'formState' 'changeEmailForm' 'loading' on submit", ->
{$scope} = createAccountController({})
$scope.$broadcast = sinon.stub()
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form)
assert $scope.$broadcast.calledWithExactly(
"formState", "changeEmailForm", "loading")
it "broadcasts 'formState' 'changeEmailForm' 'success' on success", ->
{$scope} = createAccountController({})
$scope.$broadcast = sinon.stub()
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form).then(->
assert $scope.$broadcast.calledWithExactly(
"formState", "changeEmailForm", "success")
)
it "broadcasts 'formState' 'changeEmailForm' '' on error", ->
{$scope} = createAccountController(
flash: {error: ->}
session: getStubSession(
edit_profile: -> {$promise: Promise.reject({data: {}})}
)
)
$scope.$broadcast = sinon.stub()
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form).then(->
assert $scope.$broadcast.calledWithExactly(
"formState", "changeEmailForm", "")
)
it "shows an error if the password is wrong", ->
# Mock of the server response you get when you enter the wrong password.
server_response = {
data:
errors:
pwd: "Invalid password"
status: 401
statusText: "Unauthorized"
}
{$scope} = createAccountController(
formRespond: require("../form-respond")()
session: getStubSession(
edit_profile: -> {$promise: Promise.reject(server_response)}
)
)
form = getStubChangeEmailForm(
email: "new_email_address@test.com",
emailAgain: "new_email_address@test.com", password: "pass")
$scope.changeEmailSubmit(form).then(->
assert form.pwd.responseErrorMessage == "Invalid password"
)
/**
* @license AngularJS v1.1.4
* (c) 2010-2012 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
'use strict';
var directive = {};
directive.tabbable = function() {
return {
restrict: 'C',
compile: function(element) {
var navTabs = angular.element('<ul class="nav nav-tabs"></ul>'),
tabContent = angular.element('<div class="tab-content"></div>');
tabContent.append(element.contents());
element.append(navTabs).append(tabContent);
},
controller: ['$scope', '$element', function($scope, $element) {
var navTabs = $element.contents().eq(0),
ngModel = $element.controller('ngModel') || {},
tabs = [],
selectedTab;
ngModel.$render = function() {
var $viewValue = this.$viewValue;
if (selectedTab ? (selectedTab.value != $viewValue) : $viewValue) {
if(selectedTab) {
selectedTab.paneElement.removeClass('active');
selectedTab.tabElement.removeClass('active');
selectedTab = null;
}
if($viewValue) {
for(var i = 0, ii = tabs.length; i < ii; i++) {
if ($viewValue == tabs[i].value) {
selectedTab = tabs[i];
break;
}
}
if (selectedTab) {
selectedTab.paneElement.addClass('active');
selectedTab.tabElement.addClass('active');
}
}
}
};
this.addPane = function(element, attr) {
var li = angular.element('<li><a href></a></li>'),
a = li.find('a'),
tab = {
paneElement: element,
paneAttrs: attr,
tabElement: li
};
tabs.push(tab);
attr.$observe('value', update)();
attr.$observe('title', function(){ update(); a.text(tab.title); })();
function update() {
tab.title = attr.title;
tab.value = attr.value || attr.title;
if (!ngModel.$setViewValue && (!ngModel.$viewValue || tab == selectedTab)) {
// we are not part of angular
ngModel.$viewValue = tab.value;
}
ngModel.$render();
}
navTabs.append(li);
li.bind('click', function(event) {
event.preventDefault();
event.stopPropagation();
if (ngModel.$setViewValue) {
$scope.$apply(function() {
ngModel.$setViewValue(tab.value);
ngModel.$render();
});
} else {
// we are not part of angular
ngModel.$viewValue = tab.value;
ngModel.$render();
}
});
return function() {
tab.tabElement.remove();
for(var i = 0, ii = tabs.length; i < ii; i++ ) {
if (tab == tabs[i]) {
tabs.splice(i, 1);
}
}
};
}
}]
};
};
directive.tabPane = function() {
return {
require: '^tabbable',
restrict: 'C',
link: function(scope, element, attrs, tabsCtrl) {
element.bind('$remove', tabsCtrl.addPane(element, attrs));
}
};
};
angular.module('bootstrap', []).directive(directive);
})(window, window.angular);
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
position: relative; position: relative;
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
margin-top: 0; margin-top: 1.5em;
margin-bottom: 1.5em; margin-bottom: 1.5em;
span { span {
...@@ -41,6 +41,18 @@ ...@@ -41,6 +41,18 @@
margin-bottom: 1em; margin-bottom: 1em;
} }
.form-flash {
background: $color-dove-gray;
color: $white;
width: 100%;
font-weight: bold;
margin: 1em 0 0 0;
padding: 0;
display: inline-block; // disable container margin collapse
p { margin: 1em; }
}
.form-input, .form-input,
.form-label { .form-label {
width: 100%; width: 100%;
...@@ -53,6 +65,10 @@ ...@@ -53,6 +65,10 @@
margin-bottom: .4em; margin-bottom: .4em;
} }
.form-label--light {
font-weight: normal;
}
.form-hint { .form-hint {
font-size: .833em; font-size: .833em;
margin-left: .25em; margin-left: .25em;
......
<div class="tab-pane" title="Account">
<form class="account-form form"
name="changeEmailForm"
ng-submit="changeEmailSubmit(changeEmailForm)"
novalidate form-validate>
<h2 class="form-heading"><span>Change Your Email Address</span></h2>
<p class="form-description">Your current email address is: <strong ng-bind="email"></strong>.</p>
<div class="form-field">
<label class="form-label" for="field-email">New Email Address:</label>
<input id="field-email" class="form-input" type="email" name="email" required ng-model="changeEmail.email" />
<ul class="form-error-list">
<li class="form-error" ng-show="changeEmailForm.email.$error.required">Please enter your new email address.</li>
<li class="form-error" ng-show="changeEmailForm.email.$error.email">Please enter a valid email address.</li>
<li class="form-error" ng-show="changeEmailForm.email.$error.response">{{changeEmailForm.email.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-field">
<label class="form-label" for="field-emailAgain">Enter Your New Email Address Again:</label>
<input id="field-emailAgain" class="form-input" type="email" name="emailAgain" required ng-model="changeEmail.emailAgain" />
<ul class="form-error-list">
<li class="form-error" ng-show="changeEmailForm.emailAgain.$error.required">Please enter your new email address twice.</li>
<li class="form-error" ng-show="changeEmailForm.emailAgain.$error.email">Please enter a valid email address.</li>
<li class="form-error" ng-show="changeEmailForm.emailAgain.$error.response">{{changeEmailForm.emailAgain.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-field">
<label class="form-label" for="field-pwd">Password:</label>
<input id="field-pwd" class="form-input" type="password" name="pwd" required ng-model="changeEmail.pwd" />
<ul class="form-error-list">
<li class="form-error" ng-show="changeEmailForm.pwd.$error.required">Please enter your password.</li>
<li class="form-error" ng-show="changeEmailForm.pwd.$error.minlength">Your password does not match the one we have on record.</li>
<li class="form-error" ng-show="changeEmailForm.pwd.$error.response">{{changeEmailForm.pwd.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-actions">
<div class="form-actions-buttons">
<button class="btn" type="submit"
status-button="changeEmailForm">Update</button>
</div>
</div>
</form>
<form class="account-form form"
name="changePasswordForm"
ng-submit="submit(changePasswordForm)"
novalidate form-validate>
<h2 class="form-heading"><span>Change Your Password</span></h2>
<div class="form-field">
<label class="form-label" for="field-old-password">Current Password:</label>
<input id="field-old-password" class="form-input" type="password" name="pwd" required ng-model="changePassword.pwd" />
<ul class="form-error-list">
<li class="form-error" ng-show="changePasswordForm.pwd.$error.required">Please enter your current password.</li>
<li class="form-error" ng-show="changePasswordForm.pwd.$error.minlength">Your password does not match the one we have on record.</li>
<li class="form-error" ng-show="changePasswordForm.pwd.$error.response">{{changePasswordForm.pwd.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-field">
<label class="form-label" for="field-new-password">New Password:</label>
<input id="field-new-password" class="form-input" type="password" name="password" required ng-model="changePassword.password" />
<ul class="form-error-list">
<li class="form-error" ng-show="changePasswordForm.password.$error.required">Please enter a password.</li>
<li class="form-error" ng-show="changePasswordForm.password.$error.minlength">Passwords must be at least 2 characters.</li>
<li class="form-error" ng-show="changePasswordForm.password.$error.response">{{changePasswordForm.password.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-field">
<label class="form-label" for="field-confirm-password">Confirm Password:</label>
<input id="field-confirm-password" class="form-input" type="password" name="confirmPassword" ng-model="changePassword.confirmPassword" match="changePassword.password" required>
<ul class="form-error-list">
<li class="form-error" ng-show="changePasswordForm.confirmPassword.$error.required">Please confirm your new password.</li>
<li class="form-error" ng-show="changePasswordForm.confirmPassword.$error.minlength">Passwords must be at least 2 characters.</li>
<li class="form-error" ng-show="changePasswordForm.confirmPassword.$error.match">Passwords do not match.</li>
</ul>
</div>
<div class="form-actions">
<div class="form-actions-buttons">
<button class="btn" type="submit"
status-button="changePasswordForm">Update</button>
</div>
</div>
</form>
<form class="account-form form" name="deleteAccountForm" ng-submit="delete(deleteAccountForm)" novalidate form-validate>
<h2 class="form-heading"><span>Delete Account</span></h2>
<p class="form-description">This will delete your user account. If you would like to delete your annotations, do so before continuing or email us at <a href="mailto:support@hypothes.is">support@hypothes.is</a>.</p>
<div class="form-field">
<label class="form-label" for="confirm-account-deletion">Confirm Password:</label>
<input id="confirm-account-deletion" class="form-input" type="password" name="pwd" ng-model="deleteAccount.pwd" required>
<ul class="form-error-list">
<li class="form-error" ng-show="deleteAccountForm.pwd.$error.required">Please enter your password to confirm</li>
<li class="form-error" ng-show="deleteAccountForm.pwd.$error.response">{{deleteAccountForm.pwd.responseErrorMessage}}</li>
</ul>
</div>
<div class="form-actions">
<div class="form-actions-buttons">
<button class="btn btn-danger" type="submit"
status-button="deleteAccountForm">Delete Account</button>
</div>
</div>
</form>
</div>
<div class="tab-pane" title="Notifications">
<form class="account-form form" name="notificationsForm">
<p class="form-description">Receive notification emails when:</p>
<div class="form-field form-checkbox-list">
<div class="form-checkbox-item" ng-repeat="subscription in subscriptions">
<input id="checkbox-{{$index}}" type="checkbox" ng-model="subscription.active" ng-change="updated($index, notificationsForm)" />
<label class="form-label" for="checkbox-{{$index}}">{{subscriptionDescription[subscription.type]}}</label>
</div>
</div>
</form>
</div>
...@@ -3,31 +3,34 @@ ...@@ -3,31 +3,34 @@
role="button" role="button"
title="Close" title="Close"
ng-click="shareDialog.visible = false"></i> ng-click="shareDialog.visible = false"></i>
<div class="form-vertical tabbable"> <div class="form-vertical">
<div class="form tab-pane" data-title="Share"> <ul class="nav nav-tabs">
<p>Share the link below to show anyone these annotations and invite them to contribute their own.</p> <li class="active"><a href="">Share</a></li>
<p><input id="via" </ul>
class="form-input" <div class="tab-content">
type="text" <p>Share the link below to show anyone these annotations and invite them to contribute their own.</p>
ng-value="viaPageLink" <p><input id="via"
readonly /></p> class="form-input"
<p class="share-link-icons"> type="text"
<a href="//twitter.com/intent/tweet?url={{viaPageLink}}" ng-value="viaPageLink"
target="_blank" readonly /></p>
title="Tweet link" <p class="share-link-icons">
class="share-link-icon h-icon-twitter"></a> <a href="//twitter.com/intent/tweet?url={{viaPageLink}}"
<a href="//www.facebook.com/sharer/sharer.php?u={{viaPageLink}}" target="_blank"
target="_blank" title="Tweet link"
title="Share on Facebook" class="share-link-icon h-icon-twitter"></a>
class="share-link-icon h-icon-facebook"></a> <a href="//www.facebook.com/sharer/sharer.php?u={{viaPageLink}}"
<a href="//plus.google.com/share?url={{viaPageLink}}" target="_blank"
target="_blank" title="Share on Facebook"
title="Post on Google Plus" class="share-link-icon h-icon-facebook"></a>
class="share-link-icon h-icon-google-plus"></a> <a href="//plus.google.com/share?url={{viaPageLink}}"
<a href="mailto:?subject=Let's%20Annotate&amp;body={{viaPageLink}}" target="_blank"
title="Share via email" title="Post on Google Plus"
class="share-link-icon h-icon-mail"></a> class="share-link-icon h-icon-google-plus"></a>
</p> <a href="mailto:?subject=Let's%20Annotate&amp;body={{viaPageLink}}"
</div> title="Share via email"
class="share-link-icon h-icon-mail"></a>
</p>
</div>
</div> </div>
</div> </div>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
{{account.username}}<span class="provider" ng-show="authUser">/{{account.provider}}</span><i class="h-icon-arrow-drop-down"></i> {{account.username}}<span class="provider" ng-show="authUser">/{{account.provider}}</span><i class="h-icon-arrow-drop-down"></i>
</span> </span>
<ul class="dropdown-menu pull-right" role="menu"> <ul class="dropdown-menu pull-right" role="menu">
<li ng-show="authUser"><a class="dropdown-menu__link" href="" ng-click="accountDialog.visible = true">Account</a></li> <li ng-show="authUser"><a class="dropdown-menu__link" href="/profile" target="_blank">Account</a></li>
<li><a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a></li> <li><a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a></li>
<li><a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a></li> <li><a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a></li>
<li ng-show="authUser"><a class="dropdown-menu__link" href="/stream?q=user:{{account.username}}" <li ng-show="authUser"><a class="dropdown-menu__link" href="/stream?q=user:{{account.username}}"
...@@ -77,9 +77,9 @@ ...@@ -77,9 +77,9 @@
class="dropdown-menu__link" class="dropdown-menu__link"
title="View all your annotations" title="View all your annotations"
target="_blank">{{account.username}}</a></li> target="_blank">{{account.username}}</a></li>
<li ng-show="authUser"><a href="" <li ng-show="authUser"><a href="/profile"
class="dropdown-menu__link" target="_blank"
ng-click="accountDialog.visible = true"><!-- nospace class="dropdown-menu__link"><!-- nospace
!-->Account settings</a></li> !-->Account settings</a></li>
<li><a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a></li> <li><a class="dropdown-menu__link" href="/docs/help" target="_blank">Help</a></li>
<li><a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a></li> <li><a class="dropdown-menu__link" href="mailto:support@hypothes.is">Feedback</a></li>
......
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