Merge pull request #697 from sharelatex/pr-user-affilitations

User affiliations
This commit is contained in:
Alasdair Smith 2018-07-02 11:36:04 +01:00 committed by GitHub
commit 04a98c4d91
24 changed files with 3595 additions and 107 deletions

View file

@ -56,6 +56,7 @@ public/js/*.map
public/js/libs/sharejs.js public/js/libs/sharejs.js
public/js/analytics/ public/js/analytics/
public/js/directives/ public/js/directives/
public/js/components/
public/js/es/ public/js/es/
public/js/filters/ public/js/filters/
public/js/ide/ public/js/ide/

View file

@ -68,6 +68,7 @@ module.exports =
shouldAllowEditingDetails: shouldAllowEditingDetails shouldAllowEditingDetails: shouldAllowEditingDetails
languages: Settings.languages, languages: Settings.languages,
accountSettingsTabActive: true accountSettingsTabActive: true
showAffiliationsUI: (req.query?.aff == "true") or false
sessionsPage: (req, res, next) -> sessionsPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req) user = AuthenticationController.getSessionUser(req)

View file

@ -114,7 +114,7 @@ module.exports = class Router
webRouter.post '/user/emails', webRouter.post '/user/emails',
AuthenticationController.requireLogin(), AuthenticationController.requireLogin(),
UserEmailsController.add UserEmailsController.add
webRouter.delete '/user/emails', webRouter.post '/user/emails/delete',
AuthenticationController.requireLogin(), AuthenticationController.requireLogin(),
UserEmailsController.remove UserEmailsController.remove
webRouter.post '/user/emails/default', webRouter.post '/user/emails/default',

View file

@ -4,117 +4,122 @@ block content
.content.content-alt .content.content-alt
.container .container
.row .row
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 .col-md-12.col-lg-10.col-lg-offset-1
.card .card
.page-header .page-header
h1 #{translate("account_settings")} h1 #{translate("account_settings")}
.account-settings(ng-controller="AccountSettingsController", ng-cloak) .account-settings(ng-controller="AccountSettingsController", ng-cloak)
if locals.showAffiliationsUI
include settings/user-affiliations
form-messages(for="settingsForm") form-messages(for="settingsForm")
.alert.alert-success(ng-show="settingsForm.response.success") .alert.alert-success(ng-show="settingsForm.response.success")
| #{translate("thanks_settings_updated")} | #{translate("thanks_settings_updated")}
form-messages(for="changePasswordForm") form-messages(for="changePasswordForm")
.container-fluid
.row .row
.col-md-5 .col-md-5
h3 #{translate("update_account_info")} h3 #{translate("update_account_info")}
form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
input(type="hidden", name="_csrf", value=csrfToken)
if !externalAuthenticationSystemUsed()
.form-group
label(for='email') #{translate("email")}
input.form-control(
type='email',
name='email',
placeholder="email@example.com"
required,
ng-model="email",
ng-init="email = "+JSON.stringify(user.email),
ng-model-options="{ updateOn: 'blur' }"
)
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
| #{translate("must_be_email_address")}
else
// show the email, non-editable
.form-group
label.control-label #{translate("email")}
div.form-control(readonly="true") #{user.email}
if shouldAllowEditingDetails
.form-group
label(for='firstName').control-label #{translate("first_name")}
input.form-control(
type='text',
name='first_name',
value=user.first_name
ng-non-bindable
)
.form-group
label(for='lastName').control-label #{translate("last_name")}
input.form-control(
type='text',
name='last_name',
value=user.last_name
ng-non-bindable
)
.actions
button.btn.btn-primary(
type='submit',
ng-disabled="settingsForm.$invalid"
) #{translate("update")}
else
.form-group
label.control-label #{translate("first_name")}
div.form-control(readonly="true") #{user.first_name}
.form-group
label.control-label #{translate("last_name")}
div.form-control(readonly="true") #{user.last_name}
if !externalAuthenticationSystemUsed()
.col-md-5.col-md-offset-1
h3 #{translate("change_password")}
form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate)
input(type="hidden", name="_csrf", value=csrfToken) input(type="hidden", name="_csrf", value=csrfToken)
if !externalAuthenticationSystemUsed() .form-group
.form-group label(for='currentPassword') #{translate("current_password")}
label(for='email') #{translate("email")} input.form-control(
input.form-control( type='password',
type='email', name='currentPassword',
name='email', placeholder='*********',
placeholder="email@example.com" ng-model="currentPassword",
required, required
ng-model="email", )
ng-init="email = "+JSON.stringify(user.email), span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty")
ng-model-options="{ updateOn: 'blur' }" | #{translate("required")}
) .form-group
span.small.text-primary(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty") label(for='newPassword1') #{translate("new_password")}
| #{translate("must_be_email_address")} input.form-control(
else id='passwordField',
// show the email, non-editable type='password',
.form-group name='newPassword1',
label.control-label #{translate("email")} placeholder='*********',
div.form-control(readonly="true") #{user.email} ng-model="newPassword1",
required,
complex-password
)
span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage")
.form-group
label(for='newPassword2') #{translate("confirm_new_password")}
input.form-control(
type='password',
name='newPassword2',
placeholder='*********',
ng-model="newPassword2",
equals="passwordField"
)
span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty")
| #{translate("doesnt_match")}
span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
| #{translate("invalid_password")}
.actions
button.btn.btn-primary(
type='submit',
ng-disabled="changePasswordForm.$invalid"
) #{translate("change")}
if shouldAllowEditingDetails
.form-group
label(for='firstName').control-label #{translate("first_name")}
input.form-control(
type='text',
name='first_name',
value=user.first_name
ng-non-bindable
)
.form-group
label(for='lastName').control-label #{translate("last_name")}
input.form-control(
type='text',
name='last_name',
value=user.last_name
ng-non-bindable
)
.actions
button.btn.btn-primary(
type='submit',
ng-disabled="settingsForm.$invalid"
) #{translate("update")}
else
.form-group
label.control-label #{translate("first_name")}
div.form-control(readonly="true") #{user.first_name}
.form-group
label.control-label #{translate("last_name")}
div.form-control(readonly="true") #{user.last_name}
if !externalAuthenticationSystemUsed()
.col-md-5.col-md-offset-1
h3 #{translate("change_password")}
form(async-form="changepassword", name="changePasswordForm", action="/user/password/update", method="POST", novalidate)
input(type="hidden", name="_csrf", value=csrfToken)
.form-group
label(for='currentPassword') #{translate("current_password")}
input.form-control(
type='password',
name='currentPassword',
placeholder='*********',
ng-model="currentPassword",
required
)
span.small.text-primary(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty")
| #{translate("required")}
.form-group
label(for='newPassword1') #{translate("new_password")}
input.form-control(
id='passwordField',
type='password',
name='newPassword1',
placeholder='*********',
ng-model="newPassword1",
required,
complex-password
)
span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage")
.form-group
label(for='newPassword2') #{translate("confirm_new_password")}
input.form-control(
type='password',
name='newPassword2',
placeholder='*********',
ng-model="newPassword2",
equals="passwordField"
)
span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty")
| #{translate("doesnt_match")}
span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
| #{translate("invalid_password")}
.actions
button.btn.btn-primary(
type='submit',
ng-disabled="changePasswordForm.$invalid"
) #{translate("change")}
| !{moduleIncludes("userSettings", locals)} | !{moduleIncludes("userSettings", locals)}

View file

@ -0,0 +1,172 @@
form.row(
ng-controller="UserAffiliationsController"
name="affiliationsForm"
)
.col-md-12
h3 Emails and Affiliations
p.small Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account.
table.table.affiliations-table
thead
tr
th.affiliations-table-email Email
th.affiliations-table-institution Institution and role
th.affiliations-table-inline-actions
tbody
tr(
ng-repeat="userEmail in userEmails"
)
td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }}
td
div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }}
div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department")
span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }}
span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") ,
span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }}
td
a(
href
ng-if="!userEmail.default"
ng-click="setDefaultUserEmail(userEmail.email)"
) Make default
br
a(
href
ng-if="!userEmail.default"
ng-click="removeUserEmail(userEmail.email)"
) Remove
tr.affiliations-table-highlighted-row(
ng-if="ui.isLoadingEmails"
)
td.text-center(colspan="3")
i.fa.fa-fw.fa-spin.fa-refresh
|  Loading...
tr.affiliations-table-highlighted-row(
ng-if="!ui.showAddEmailUI && !ui.isLoadingEmails"
)
td(colspan="3")
a(
href
ng-click="showAddEmailForm()"
) Add another email
tr.affiliations-table-highlighted-row(
ng-if="ui.showAddEmailUI"
)
td
.affiliations-form-group
input-suggestions(
ng-model="newAffiliation.email"
ng-model-options="{ allowInvalid: true }"
get-suggestion="getEmailSuggestion(userInput)"
on-blur="handleEmailInputBlur()"
input-id="affilitations-email"
input-name="affilitationsEmail"
input-placeholder="e.g. johndoe@mit.edu"
input-type="email"
input-required="true"
)
td
.affiliations-table-label(
ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI"
)
| {{ newAffiliation.university.name }} (
a(
href
ng-click="selectUniversityManually();"
) change
| )
.affiliations-table-label(
ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI"
) Start by adding your email address.
.affiliations-table-label(
ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI"
)
| Is your email affiliated with an institution?
br
a(
href
ng-click="selectUniversityManually();"
) Let us know
.affiliations-form-group(
ng-if="ui.showManualUniversitySelectionUI"
)
ui-select(
ng-model="newAffiliation.country"
)
ui-select-match(
placeholder="Country"
) {{ $select.selected.name }}
ui-select-choices(
repeat="country in countries | filter: $select.search"
)
span(
ng-bind="country.name"
s)
.affiliations-form-group(
ng-if="ui.showManualUniversitySelectionUI"
)
ui-select(
ng-model="newAffiliation.university"
ng-disabled="!newAffiliation.country"
tagging="addUniversityToSelection"
tagging-label="false"
)
ui-select-match(
placeholder="Institution"
) {{ $select.selected.name }}
ui-select-choices(
repeat="university in universities | filter: $select.search"
)
span(
ng-bind="university.name"
)
.affiliations-form-group(
ng-if="ui.isValidEmail && newAffiliation.university"
)
ui-select(
ng-model="newAffiliation.role"
tagging
tagging-label="false"
)
ui-select-match(
placeholder="Role"
) {{ $select.selected }}
ui-select-choices(
repeat="role in roles | filter: $select.search"
)
span(
ng-bind="role"
)
.affiliations-form-group(
ng-if="ui.isValidEmail && newAffiliation.university"
)
ui-select(
ng-model="newAffiliation.department"
tagging
tagging-label="false"
)
ui-select-match(
placeholder="Department"
) {{ $select.selected }}
ui-select-choices(
repeat="department in departments | filter: $select.search"
)
span(
ng-bind="department"
)
td
button.btn.btn-primary(
ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail"
ng-click="addNewEmail()"
)
span(
ng-if="!ui.isAddingNewEmail"
) Add new email
span(
ng-if="ui.isAddingNewEmail"
)
i.fa.fa-fw.fa-spin.fa-refresh
|  Adding...
hr

View file

@ -15,6 +15,11 @@ httpAuthUsers[httpAuthUser] = httpAuthPass
sessionSecret = "secret-please-change" sessionSecret = "secret-please-change"
v1Api =
url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000"
user: 'overleaf'
pass: 'password'
module.exports = settings = module.exports = settings =
allowAnonymousReadAndWriteSharing: allowAnonymousReadAndWriteSharing:
@ -157,9 +162,9 @@ module.exports = settings =
thirdpartyreferences: thirdpartyreferences:
url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046" url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046"
v1: v1:
url: "http://#{process.env['V1_HOST'] or 'localhost'}:5000" url: v1Api.url
user: 'overleaf' user: v1Api.user
pass: 'password' pass: v1Api.pass
templates: templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2" user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
@ -420,7 +425,9 @@ module.exports = settings =
redirects: redirects:
"/templates/index": "/templates/" "/templates/index": "/templates/"
proxyUrls: {} proxyUrls:
'/institutions/list': { baseUrl: v1Api.url, path: '/universities/list' }
'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
reloadModuleViewsOnEachRequest: true reloadModuleViewsOnEachRequest: true

View file

@ -17,14 +17,22 @@ define [
"ErrorCatcher" "ErrorCatcher"
"localStorage" "localStorage"
"ngTagsInput" "ngTagsInput"
]).config ($qProvider, sixpackProvider, $httpProvider)-> "ui.select"
]).config ($qProvider, sixpackProvider, $httpProvider, uiSelectConfig) ->
$qProvider.errorOnUnhandledRejections(false) $qProvider.errorOnUnhandledRejections(false)
uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
sixpackProvider.setOptions({ sixpackProvider.setOptions({
debug: false debug: false
baseUrl: window.sharelatex.sixpackDomain baseUrl: window.sharelatex.sixpackDomain
client_id: window.user_id client_id: window.user_id
}) })
App.run ($templateCache) ->
# UI Select templates are hard-coded and use Glyphicon icons (which we don't import).
# The line below simply overrides the hard-coded template with our own, which is
# basically the same but using Font Awesome icons.
$templateCache.put "bootstrap/match.tpl.html", "<div class=\"ui-select-match\" ng-hide=\"$select.open && $select.searchEnabled\" ng-disabled=\"$select.disabled\" ng-class=\"{\'btn-default-focus\':$select.focus}\"><span tabindex=\"-1\" class=\"btn btn-default form-control ui-select-toggle\" aria-label=\"{{ $select.baseTitle }} activate\" ng-disabled=\"$select.disabled\" ng-click=\"$select.activate()\" style=\"outline: 0;\"><span ng-show=\"$select.isEmpty()\" class=\"ui-select-placeholder text-muted\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" class=\"ui-select-match-text pull-left\" ng-class=\"{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}\" ng-transclude=\"\"></span> <i class=\"caret pull-right\" ng-click=\"$select.toggle($event)\"></i> <a ng-show=\"$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)\" aria-label=\"{{ $select.baseTitle }} clear\" style=\"margin-right: 10px\" ng-click=\"$select.clear($event)\" class=\"btn btn-xs btn-link pull-right\"><i class=\"fa fa-times\" aria-hidden=\"true\"></i></a></span></div>"
sl_debugging = window.location?.search?.match(/debug=true/)? sl_debugging = window.location?.search?.match(/debug=true/)?
window.sl_console = window.sl_console =
log: (args...) -> console.log(args...) if sl_debugging log: (args...) -> console.log(args...) if sl_debugging

View file

@ -0,0 +1,74 @@
define [
"base"
], (App) ->
inputSuggestionsController = ($scope, $element, $attrs, Keys) ->
ctrl = @
ctrl.showHint = false
ctrl.hasFocus = false
ctrl.handleFocus = () ->
ctrl.hasFocus = true
ctrl.suggestion = null
ctrl.handleBlur = () ->
ctrl.showHint = false
ctrl.hasFocus = false
ctrl.suggestion = null
ctrl.onBlur()
ctrl.handleKeyDown = ($event) ->
if ($event.which == Keys.TAB or $event.which == Keys.ENTER) and ctrl.suggestion? and ctrl.suggestion != ""
$event.preventDefault()
ctrl.localNgModel += ctrl.suggestion
ctrl.suggestion = null
ctrl.showHint = false
$scope.$watch "$ctrl.localNgModel", (newVal, oldVal) ->
if ctrl.hasFocus and newVal != oldVal
ctrl.suggestion = null
ctrl.showHint = false
ctrl.getSuggestion({ userInput: newVal })
.then (suggestion) ->
if suggestion? and newVal == ctrl.localNgModel
ctrl.showHint = true
ctrl.suggestion = suggestion.replace newVal, ""
.catch () -> ctrl.suggestion = null
return
App.component "inputSuggestions", {
bindings:
localNgModel: "=ngModel"
localNgModelOptions: "=?ngModelOptions"
getSuggestion: "&"
onBlur: "&?"
inputId: "@?"
inputName: "@?"
inputPlaceholder: "@?"
inputType: "@?"
inputRequired: "=?"
controller: inputSuggestionsController
template: [
'<div class="input-suggestions">',
'<div class="form-control input-suggestions-shadow">',
'<span ng-bind="$ctrl.localNgModel"',
' class="input-suggestions-shadow-existing"',
' ng-show="$ctrl.showHint">',
'</span>',
'<span ng-bind="$ctrl.suggestion"',
' class="input-suggestions-shadow-suggested"',
' ng-show="$ctrl.showHint">',
'</span>',
'</div>',
'<input type="text"',
' class="form-control input-suggestions-main"',
' ng-focus="$ctrl.handleFocus()"',
' ng-keyDown="$ctrl.handleKeyDown($event)"',
' ng-blur="$ctrl.handleBlur()"',
' ng-model="$ctrl.localNgModel"',
' ng-model-options="$ctrl.localNgModelOptions"',
' ng-model-options="{ debounce: 50 }"',
' ng-attr-id="{{ ::$ctrl.inputId }}"',
' ng-attr-placeholder="{{ ::$ctrl.inputPlaceholder }}"',
' ng-attr-type="{{ ::$ctrl.inputType }}"',
' ng-attr-name="{{ ::$ctrl.inputName }}"',
' ng-required="::$ctrl.inputRequired">',
'</div>'
].join ""
}

View file

@ -11,5 +11,6 @@ define [
"libs/sixpack" "libs/sixpack"
"libs/angular-sixpack" "libs/angular-sixpack"
"libs/ng-tags-input-3.0.0" "libs/ng-tags-input-3.0.0"
"libs/select/select"
], () -> ], () ->

View file

@ -20,6 +20,9 @@ define [
"main/subscription/team-invite-controller" "main/subscription/team-invite-controller"
"main/contact-us" "main/contact-us"
"main/learn" "main/learn"
"main/affiliations/controllers/UserAffiliationsController"
"main/affiliations/factories/UserAffiliationsDataService"
"main/keys"
"analytics/AbTestingManager" "analytics/AbTestingManager"
"directives/asyncForm" "directives/asyncForm"
"directives/stopPropagation" "directives/stopPropagation"
@ -34,6 +37,7 @@ define [
"services/queued-http" "services/queued-http"
"services/validateCaptcha" "services/validateCaptcha"
"filters/formatDate" "filters/formatDate"
"components/inputSuggestions"
"__MAIN_CLIENTSIDE_INCLUDES__" "__MAIN_CLIENTSIDE_INCLUDES__"
], () -> ], () ->
angular.module('SharelatexApp').config( angular.module('SharelatexApp').config(

View file

@ -0,0 +1,166 @@
define [
"base"
], (App) ->
App.controller "UserAffiliationsController", ["$scope", "UserAffiliationsDataService", "$q", "_", ($scope, UserAffiliationsDataService, $q, _) ->
$scope.userEmails = []
$scope.countries = []
$scope.universities = []
$scope.roles = []
$scope.departments = []
_defaultDepartments = []
LOCAL_AND_DOMAIN_REGEX = /([^@]+)@(.+)/
EMAIL_REGEX = /^([A-Za-z0-9_\-\.]+)@([^\.]+)\.([A-Za-z0-9_\-\.]+)([^\.])$/
_matchLocalAndDomain = (userEmailInput) ->
match = userEmailInput?.match LOCAL_AND_DOMAIN_REGEX
if match?
{ local: match[1], domain: match[2] }
else
{ local: null, domain: null }
$scope.addUniversityToSelection = (universityName) ->
{ name: universityName, isUserSuggested: true }
$scope.getEmailSuggestion = (userInput) ->
userInputLocalAndDomain = _matchLocalAndDomain(userInput)
$scope.ui.isValidEmail = EMAIL_REGEX.test userInput
$scope.ui.isBlacklistedEmail = false
$scope.ui.showManualUniversitySelectionUI = false
if userInputLocalAndDomain.domain?
$scope.ui.isBlacklistedEmail = UserAffiliationsDataService.isDomainBlacklisted userInputLocalAndDomain.domain
UserAffiliationsDataService.getUniversityDomainFromPartialDomainInput(userInputLocalAndDomain.domain)
.then (universityDomain) ->
currentUserInputLocalAndDomain = _matchLocalAndDomain $scope.newAffiliation.email
if currentUserInputLocalAndDomain.domain == universityDomain.hostname
$scope.newAffiliation.university = universityDomain.university
$scope.newAffiliation.department = universityDomain.department
else
$scope.newAffiliation.university = null
$scope.newAffiliation.department = null
$q.resolve "#{userInputLocalAndDomain.local}@#{universityDomain.hostname}"
.catch () ->
$scope.newAffiliation.university = null
$scope.newAffiliation.department = null
$q.reject null
else
$scope.newAffiliation.university = null
$scope.newAffiliation.department = null
$q.reject null
$scope.selectUniversityManually = () ->
$scope.newAffiliation.university = null
$scope.newAffiliation.department = null
$scope.ui.showManualUniversitySelectionUI = true
$scope.showAddEmailForm = () ->
$scope.ui.showAddEmailUI = true
$scope.addNewEmail = () ->
$scope.ui.isAddingNewEmail = true
if !$scope.newAffiliation.university?
addEmailPromise = UserAffiliationsDataService
.addUserEmail $scope.newAffiliation.email
else
if $scope.newAffiliation.university.isUserSuggested
addEmailPromise = UserAffiliationsDataService
.addUserAffiliationWithUnknownUniversity(
$scope.newAffiliation.email,
$scope.newAffiliation.university.name,
$scope.newAffiliation.country.code,
$scope.newAffiliation.role,
$scope.newAffiliation.department
)
else
addEmailPromise = UserAffiliationsDataService
.addUserAffiliation(
$scope.newAffiliation.email,
$scope.newAffiliation.university.id
$scope.newAffiliation.role,
$scope.newAffiliation.department
)
addEmailPromise.then () ->
_reset()
_getUserEmails()
$scope.setDefaultUserEmail = (email) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.setDefaultUserEmail email
.then () -> _getUserEmails()
$scope.removeUserEmail = (email) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.removeUserEmail email
.then () -> _getUserEmails()
$scope.getDepartments = () ->
if $scope.newAffiliation.university?.departments.length > 0
_.uniq $scope.newAffiliation.university.departments
else
UserAffiliationsDataService.getDefaultDepartmentHints()
_reset = () ->
$scope.newAffiliation =
email: ""
country: null
university: null
role: null
department: null
$scope.ui =
showManualUniversitySelectionUI: false
isLoadingEmails: false
isAddingNewEmail: false
showAddEmailUI: false
isValidEmail: false
isBlacklistedEmail: false
_reset()
# Populates the emails table
_getUserEmails = () ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.getUserEmails()
.then (emails) ->
$scope.userEmails = emails
$scope.ui.isLoadingEmails = false
_getUserEmails()
# Populates the countries dropdown
UserAffiliationsDataService
.getCountries()
.then (countries) -> $scope.countries = countries
# Populates the roles dropdown
UserAffiliationsDataService
.getDefaultRoleHints()
.then (roles) -> $scope.roles = roles
# Fetches the default department hints
UserAffiliationsDataService
.getDefaultDepartmentHints()
.then (departments) ->
_defaultDepartments = departments
# Populates the universities dropdown (after selecting a country)
$scope.$watch "newAffiliation.country", (newSelectedCountry, prevSelectedCountry) ->
if newSelectedCountry? and newSelectedCountry != prevSelectedCountry
$scope.newAffiliation.university = null
$scope.newAffiliation.role = null
$scope.newAffiliation.department = null
UserAffiliationsDataService
.getUniversitiesFromCountry(newSelectedCountry)
.then (universities) -> $scope.universities = universities
# Populates the departments dropdown (after selecting a university)
$scope.$watch "newAffiliation.university", (newSelectedUniversity, prevSelectedUniversity) ->
if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity
if newSelectedUniversity.departments?.length > 0
$scope.departments = _.uniq newSelectedUniversity.departments
else
$scope.departments = _defaultDepartments
]

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
define [
"base"
], (App) ->
App.constant "Keys",
ENTER : 13
TAB : 9
ESCAPE : 27
SPACE : 32
BACKSPACE : 8
UP : 38
DOWN : 40
LEFT : 37
RIGHT : 39
PERIOD : 190
COMMA : 188
END : 35

View file

@ -0,0 +1,362 @@
/*!
* ui-select
* http://github.com/angular-ui/ui-select
* Version: 0.19.7 - 2017-04-15T14:28:36.790Z
* License: MIT
*/
/* Style when highlighting a search. */
.ui-select-highlight {
font-weight: bold;
}
.ui-select-offscreen {
clip: rect(0 0 0 0) !important;
width: 1px !important;
height: 1px !important;
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
position: absolute !important;
outline: 0 !important;
left: 0px !important;
top: 0px !important;
}
.ui-select-choices-row:hover {
background-color: #f5f5f5;
}
/* Select2 theme */
/* Mark invalid Select2 */
.ng-dirty.ng-invalid > a.select2-choice {
border-color: #D44950;
}
.select2-result-single {
padding-left: 0;
}
.select2-locked > .select2-search-choice-close{
display:none;
}
.select-locked > .ui-select-match-close{
display:none;
}
body > .select2-container.open {
z-index: 9999; /* The z-index Select2 applies to the select2-drop */
}
/* Handle up direction Select2 */
.ui-select-container[theme="select2"].direction-up .ui-select-match,
.ui-select-container.select2.direction-up .ui-select-match {
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown,
.ui-select-container.select2.direction-up .ui-select-dropdown {
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-width: 1px; /* FIXME hardcoded value :-/ */
border-top-style: solid;
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
margin-top: -4px; /* FIXME hardcoded value :-/ */
}
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown .select2-search,
.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search {
margin-top: 4px; /* FIXME hardcoded value :-/ */
}
.ui-select-container[theme="select2"].direction-up.select2-dropdown-open .ui-select-match,
.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match {
border-bottom-color: #5897fb;
}
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden,
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input{
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border:0;
}
/* Selectize theme */
/* Helper class to show styles when focus */
.selectize-input.selectize-focus{
border-color: #007FBB !important;
}
/* Fix input width for Selectize theme */
.selectize-control.single > .selectize-input > input {
width: 100%;
}
/* Fix line break when there's at least one item selected with the Selectize theme */
.selectize-control.multi > .selectize-input > input {
margin: 0 !important;
}
/* Fix dropdown width for Selectize theme */
.selectize-control > .selectize-dropdown {
width: 100%;
}
/* Mark invalid Selectize */
.ng-dirty.ng-invalid > div.selectize-input {
border-color: #D44950;
}
/* Handle up direction Selectize */
.ui-select-container[theme="selectize"].direction-up .ui-select-dropdown {
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
margin-top: -2px; /* FIXME hardcoded value :-/ */
}
.ui-select-container[theme="selectize"] input.ui-select-search-hidden{
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border:0;
width: 0;
}
/* Bootstrap theme */
/* Helper class to show styles when focus */
.btn-default-focus {
color: #333;
background-color: #EBEBEB;
border-color: #ADADAD;
text-decoration: none;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.ui-select-bootstrap .ui-select-toggle {
position: relative;
}
.ui-select-bootstrap .ui-select-toggle > .caret {
position: absolute;
height: 10px;
top: 50%;
right: 10px;
margin-top: -2px;
}
/* Fix Bootstrap dropdown position when inside a input-group */
.input-group > .ui-select-bootstrap.dropdown {
/* Instead of relative */
position: static;
}
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control {
border-radius: 4px; /* FIXME hardcoded value :-/ */
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up {
border-radius: 4px !important; /* FIXME hardcoded value :-/ */
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.ui-select-bootstrap .ui-select-search-hidden{
opacity: 0;
height: 0;
min-height: 0;
padding: 0;
margin: 0;
border:0;
}
.ui-select-bootstrap > .ui-select-match > .btn{
/* Instead of center because of .btn */
text-align: left !important;
}
.ui-select-bootstrap > .ui-select-match > .caret {
position: absolute;
top: 45%;
right: 15px;
}
/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */
.ui-select-bootstrap > .ui-select-choices ,.ui-select-bootstrap > .ui-select-no-choice {
width: 100%;
height: auto;
max-height: 200px;
overflow-x: hidden;
margin-top: -1px;
}
body > .ui-select-bootstrap.open {
z-index: 1000; /* Standard Bootstrap dropdown z-index */
}
.ui-select-multiple.ui-select-bootstrap {
height: auto;
padding: 3px 3px 0 3px;
}
.ui-select-multiple.ui-select-bootstrap input.ui-select-search {
background-color: transparent !important; /* To prevent double background when disabled */
border: none;
outline: none;
height: 1.666666em;
margin-bottom: 3px;
}
.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {
font-size: 1.6em;
line-height: 0.75;
}
.ui-select-multiple.ui-select-bootstrap .ui-select-match-item {
outline: 0;
margin: 0 3px 3px 0;
}
.ui-select-multiple .ui-select-match-item {
position: relative;
}
.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close {
pointer-events: none;
}
.ui-select-multiple:hover .ui-select-match-item.dropping-before:before {
content: "";
position: absolute;
top: 0;
right: 100%;
height: 100%;
margin-right: 2px;
border-left: 1px solid #428bca;
}
.ui-select-multiple:hover .ui-select-match-item.dropping-after:after {
content: "";
position: absolute;
top: 0;
left: 100%;
height: 100%;
margin-left: 2px;
border-right: 1px solid #428bca;
}
.ui-select-bootstrap .ui-select-choices-row>span {
cursor: pointer;
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus {
text-decoration: none;
color: #262626;
background-color: #f5f5f5;
}
.ui-select-bootstrap .ui-select-choices-row.active>span {
color: #fff;
text-decoration: none;
outline: 0;
background-color: #428bca;
}
.ui-select-bootstrap .ui-select-choices-row.disabled>span,
.ui-select-bootstrap .ui-select-choices-row.active.disabled>span {
color: #777;
cursor: not-allowed;
background-color: #fff;
}
/* fix hide/show angular animation */
.ui-select-match.ng-hide-add,
.ui-select-search.ng-hide-add {
display: none !important;
}
/* Mark invalid Bootstrap */
.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {
border-color: #D44950;
}
/* Handle up direction Bootstrap */
.ui-select-container[theme="bootstrap"].direction-up .ui-select-dropdown {
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
}
.ui-select-bootstrap .ui-select-match-text {
width: 100%;
padding-right: 1em;
}
.ui-select-bootstrap .ui-select-match-text span {
display: inline-block;
width: 100%;
overflow: hidden;
}
.ui-select-bootstrap .ui-select-toggle > a.btn {
position: absolute;
height: 10px;
right: 10px;
margin-top: -2px;
}
/* Spinner */
.ui-select-refreshing.glyphicon {
position: absolute;
right: 0;
padding: 8px 27px;
}
@-webkit-keyframes ui-select-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes ui-select-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
.ui-select-spin {
-webkit-animation: ui-select-spin 2s infinite linear;
animation: ui-select-spin 2s infinite linear;
}
.ui-select-refreshing.ng-animate {
-webkit-animation: none 0s;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -41,6 +41,8 @@
@import "components/close.less"; @import "components/close.less";
@import "components/fineupload.less"; @import "components/fineupload.less";
@import "components/hover.less"; @import "components/hover.less";
@import "components/ui-select.less";
@import "components/input-suggestions.less";
// Components w/ JavaScript // Components w/ JavaScript
@import "components/modals.less"; @import "components/modals.less";
@ -87,4 +89,5 @@
@import "../js/libs/pdfListView/TextLayer.css"; @import "../js/libs/pdfListView/TextLayer.css";
@import "../js/libs/pdfListView/AnnotationsLayer.css"; @import "../js/libs/pdfListView/AnnotationsLayer.css";
@import "../js/libs/pdfListView/HighlightsLayer.css"; @import "../js/libs/pdfListView/HighlightsLayer.css";
@import "../js/libs/select/select.css";
@import "vendor/codemirror.css"; @import "vendor/codemirror.css";

View file

@ -2,6 +2,9 @@
.alert { .alert {
margin-bottom: 0; margin-bottom: 0;
} }
h3 {
margin-top: 0;
}
} }
#delete-account-modal { #delete-account-modal {
@ -10,3 +13,29 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
.affiliations-table {
table-layout: fixed;
}
.affiliations-table-email {
width: 40%;
}
.affiliations-table-institution {
width: 40%;
}
.affiliations-table-inline-actions {
width: 20%;
}
.affiliations-table-highlighted-row {
background-color: tint(@content-alt-bg-color, 6%);
}
.affiliations-form-group {
margin-top: @table-cell-padding;
&:first-child {
margin-top: 0;
}
}
.affiliations-table-label {
padding-top: 4px;
}

View file

@ -0,0 +1,21 @@
.input-suggestions {
position: relative;
height: @input-height-base;
}
.input-suggestions-main {
position: absolute;
top: 0;
background-color: transparent;
}
.input-suggestions-shadow {
background-color: @input-bg;
padding-top: 4px;
}
.input-suggestions-shadow-existing {
color: transparent;
}
.input-suggestions-shadow-suggested {
color: lighten(@input-color, 25%);
}

View file

@ -0,0 +1,60 @@
.ui-select-bootstrap > .ui-select-choices,
.ui-select-bootstrap > .ui-select-no-choice {
width: auto;
max-width: 400px;
}
.dropdown-menu .ui-select-choices-row {
padding: 4px 0;
> .ui-select-choices-row-inner {
overflow: hidden;
text-overflow: ellipsis;
}
}
.ui-select-placeholder,
.ui-select-match-text {
overflow: hidden;
text-overflow: ellipsis;
font-weight: normal;
}
.ui-select-bootstrap {
&:focus {
outline: none;
}
> .ui-select-match {
&:focus {
outline: none;
}
&.btn-default-focus {
outline: 0;
box-shadow: none;
background-color: transparent;
> .btn {
border-color: @input-border-focus;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px fade(@input-border-focus, 60%);
}
}
> .btn {
color: @input-color;
background-color: @input-bg;
border: 1px solid @input-border;
&[disabled] {
cursor: not-allowed;
background-color: @input-bg-disabled;
opacity: 1;
}
}
}
}
.ui-select-container[tagging] {
.ui-select-toggle {
cursor: text;
> i.caret.pull-right {
display: none;
}
}
}

View file

@ -65,6 +65,8 @@
@btn-info-bg : @ol-blue; @btn-info-bg : @ol-blue;
@btn-info-border : transparent; @btn-info-border : transparent;
@padding-xs-horizontal : 8px;
// Alerts // Alerts
@alert-padding : 15px; @alert-padding : 15px;
@alert-border-radius : @border-radius-base; @alert-border-radius : @border-radius-base;