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
......
This diff is collapsed.
/**
* @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