Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
4c0cdba3
Commit
4c0cdba3
authored
Oct 21, 2015
by
Robert Knight
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2636 from hypothesis/replace-profile-form
Replace client-side profile form
parents
17954cf0
7d30a9ea
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
50 additions
and
1017 deletions
+50
-1017
account-controller.js
h/static/scripts/account-controller.js
+0
-147
app.coffee
h/static/scripts/app.coffee
+0
-5
match.coffee
h/static/scripts/directive/match.coffee
+0
-12
tab-reveal.coffee
h/static/scripts/directive/tab-reveal.coffee
+0
-41
tabbable.coffee
h/static/scripts/directive/tabbable.coffee
+0
-16
match-test.coffee
h/static/scripts/directive/test/match-test.coffee
+0
-49
karma.config.js
h/static/scripts/karma.config.js
+0
-1
account-controller-test.coffee
h/static/scripts/test/account-controller-test.coffee
+0
-466
angular-bootstrap-tabbable.js
h/static/scripts/vendor/angular-bootstrap-tabbable.js
+0
-120
forms.scss
h/static/styles/forms.scss
+17
-1
account.html
h/templates/client/settings/account.html
+0
-118
notifications.html
h/templates/client/settings/notifications.html
+0
-11
share_dialog.html
h/templates/client/share_dialog.html
+29
-26
top_bar.html
h/templates/client/top_bar.html
+4
-4
No files found.
h/static/scripts/account-controller.js
deleted
100644 → 0
View file @
17954cf0
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
;
h/static/scripts/app.coffee
View file @
4c0cdba3
...
@@ -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'
))
...
...
h/static/scripts/directive/match.coffee
deleted
100644 → 0
View file @
17954cf0
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'
h/static/scripts/directive/tab-reveal.coffee
deleted
100644 → 0
View file @
17954cf0
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'
]
]
h/static/scripts/directive/tabbable.coffee
deleted
100644 → 0
View file @
17954cf0
# 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'
]
h/static/scripts/directive/test/match-test.coffee
deleted
100644 → 0
View file @
17954cf0
{
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
)
h/static/scripts/karma.config.js
View file @
4c0cdba3
...
@@ -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
...
...
h/static/scripts/test/account-controller-test.coffee
deleted
100644 → 0
View file @
17954cf0
{
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"
)
h/static/scripts/vendor/angular-bootstrap-tabbable.js
deleted
100644 → 0
View file @
17954cf0
/**
* @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
);
h/static/styles/forms.scss
View file @
4c0cdba3
...
@@ -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
;
...
...
h/templates/client/settings/account.html
deleted
100644 → 0
View file @
17954cf0
<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>
h/templates/client/settings/notifications.html
deleted
100644 → 0
View file @
17954cf0
<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>
h/templates/client/share_dialog.html
View file @
4c0cdba3
...
@@ -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&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&body={{viaPageLink}}"
</div>
title=
"Share via email"
class=
"share-link-icon h-icon-mail"
></a>
</p>
</div>
</div>
</div>
</div>
</div>
h/templates/client/top_bar.html
View file @
4c0cdba3
...
@@ -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__li
nk"
target=
"_bla
nk"
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>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment