mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into pr-v2-light-theme
This commit is contained in:
commit
a15706ce24
148 changed files with 4354 additions and 1220 deletions
127
services/web/Jenkinsfile
vendored
127
services/web/Jenkinsfile
vendored
|
@ -1,19 +1,37 @@
|
|||
String cron_string = BRANCH_NAME == "master" ? "@daily" : ""
|
||||
|
||||
pipeline {
|
||||
|
||||
|
||||
agent any
|
||||
|
||||
|
||||
environment {
|
||||
HOME = "/tmp"
|
||||
GIT_PROJECT = "web-sharelatex-internal"
|
||||
JENKINS_WORKFLOW = "web-sharelatex-internal"
|
||||
TARGET_URL = "${env.JENKINS_URL}blue/organizations/jenkins/${JENKINS_WORKFLOW}/detail/$BRANCH_NAME/$BUILD_NUMBER/pipeline"
|
||||
GIT_API_URL = "https://api.github.com/repos/sharelatex/${GIT_PROJECT}/statuses/$GIT_COMMIT"
|
||||
}
|
||||
|
||||
|
||||
triggers {
|
||||
pollSCM('* * * * *')
|
||||
cron(cron_string)
|
||||
}
|
||||
|
||||
|
||||
stages {
|
||||
stage('Pre') {
|
||||
steps {
|
||||
withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
|
||||
sh "curl $GIT_API_URL \
|
||||
--data '{ \
|
||||
\"state\" : \"pending\", \
|
||||
\"target_url\": \"$TARGET_URL\", \
|
||||
\"description\": \"Your build is underway\", \
|
||||
\"context\": \"ci/jenkins\" }' \
|
||||
-u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install modules') {
|
||||
steps {
|
||||
sshagent (credentials: ['GIT_DEPLOY_KEY']) {
|
||||
|
@ -21,7 +39,7 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Install') {
|
||||
agent {
|
||||
docker {
|
||||
|
@ -66,16 +84,39 @@ pipeline {
|
|||
sh 'make --no-print-directory lint'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Unit Test') {
|
||||
agent {
|
||||
docker {
|
||||
image 'node:6.9.5'
|
||||
reuseNode true
|
||||
|
||||
stage('Test and Minify') {
|
||||
parallel {
|
||||
stage('Unit Test') {
|
||||
agent {
|
||||
docker {
|
||||
image 'node:6.9.5'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'make --no-print-directory test_unit MOCHA_ARGS="--reporter tap"'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Acceptance Test') {
|
||||
steps {
|
||||
// Spawns its own docker containers
|
||||
sh 'make --no-print-directory test_acceptance MOCHA_ARGS="--reporter tap"'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Minify') {
|
||||
agent {
|
||||
docker {
|
||||
image 'node:6.9.5'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'WEBPACK_ENV=production make minify'
|
||||
}
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'make --no-print-directory test_unit MOCHA_ARGS="--reporter tap"'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,26 +126,7 @@ pipeline {
|
|||
sh 'make --no-print-directory test_frontend'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Acceptance Test') {
|
||||
steps {
|
||||
// Spawns its own docker containers
|
||||
sh 'make --no-print-directory test_acceptance MOCHA_ARGS="--reporter tap"'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Minify') {
|
||||
agent {
|
||||
docker {
|
||||
image 'node:6.9.5'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'WEBPACK_ENV=production make minify'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Package') {
|
||||
steps {
|
||||
sh 'rm -rf ./node_modules/grunt*'
|
||||
|
@ -113,7 +135,7 @@ pipeline {
|
|||
sh 'tar -czf build.tar.gz --exclude=build.tar.gz --exclude-vcs .'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Publish') {
|
||||
steps {
|
||||
withAWS(credentials:'S3_CI_BUILDS_AWS_KEYS', region:"${S3_REGION_BUILD_ARTEFACTS}") {
|
||||
|
@ -123,8 +145,8 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
stage('Sync OSS') {
|
||||
when {
|
||||
branch 'master'
|
||||
|
@ -136,29 +158,50 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
post {
|
||||
always {
|
||||
sh 'make clean_ci'
|
||||
}
|
||||
|
||||
success {
|
||||
withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
|
||||
sh "curl $GIT_API_URL \
|
||||
--data '{ \
|
||||
\"state\" : \"success\", \
|
||||
\"target_url\": \"$TARGET_URL\", \
|
||||
\"description\": \"Your build succeeded!\", \
|
||||
\"context\": \"ci/jenkins\" }' \
|
||||
-u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
||||
failure {
|
||||
mail(from: "${EMAIL_ALERT_FROM}",
|
||||
to: "${EMAIL_ALERT_TO}",
|
||||
mail(from: "${EMAIL_ALERT_FROM}",
|
||||
to: "${EMAIL_ALERT_TO}",
|
||||
subject: "Jenkins build failed: ${JOB_NAME}:${BUILD_NUMBER}",
|
||||
body: "Build: ${BUILD_URL}")
|
||||
withCredentials([usernamePassword(credentialsId: 'GITHUB_INTEGRATION', usernameVariable: 'GH_AUTH_USERNAME', passwordVariable: 'GH_AUTH_PASSWORD')]) {
|
||||
sh "curl $GIT_API_URL \
|
||||
--data '{ \
|
||||
\"state\" : \"failure\", \
|
||||
\"target_url\": \"$TARGET_URL\", \
|
||||
\"description\": \"Your build failed\", \
|
||||
\"context\": \"ci/jenkins\" }' \
|
||||
-u $GH_AUTH_USERNAME:$GH_AUTH_PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// The options directive is for configuration that applies to the whole job.
|
||||
options {
|
||||
// Only build one at a time
|
||||
disableConcurrentBuilds()
|
||||
|
||||
|
||||
// we'd like to make sure remove old builds, so we don't fill up our storage!
|
||||
buildDiscarder(logRotator(numToKeepStr:'50'))
|
||||
|
||||
|
||||
// And we'd really like to be sure that this build doesn't hang forever, so let's time it out after:
|
||||
timeout(time: 30, unit: 'MINUTES')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
AuthenticationManager = require ("./AuthenticationManager")
|
||||
LoginRateLimiter = require("../Security/LoginRateLimiter")
|
||||
UserGetter = require "../User/UserGetter"
|
||||
UserUpdater = require "../User/UserUpdater"
|
||||
Metrics = require('metrics-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
|
@ -62,41 +61,63 @@ module.exports = AuthenticationController =
|
|||
if err?
|
||||
return next(err)
|
||||
if user # `user` is either a user object or false
|
||||
redir = AuthenticationController._getRedirectFromSession(req) || "/project"
|
||||
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
|
||||
if err?
|
||||
return next(err)
|
||||
AuthenticationController._clearRedirectFromSession(req)
|
||||
res.json {redir: redir}
|
||||
AuthenticationController.finishLogin(user, req, res, next)
|
||||
else
|
||||
res.json message: info
|
||||
if info.redir?
|
||||
res.json {redir: info.redir}
|
||||
else
|
||||
res.json message: info
|
||||
)(req, res, next)
|
||||
|
||||
finishLogin: (user, req, res, next) ->
|
||||
redir = AuthenticationController._getRedirectFromSession(req) || "/project"
|
||||
AuthenticationController._loginAsyncHandlers(req, user)
|
||||
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
|
||||
if err?
|
||||
return next(err)
|
||||
AuthenticationController._clearRedirectFromSession(req)
|
||||
if req.headers?['accept']?.match(/^application\/json.*$/)
|
||||
res.json {redir: redir}
|
||||
else
|
||||
res.redirect(redir)
|
||||
|
||||
doPassportLogin: (req, username, password, done) ->
|
||||
email = username.toLowerCase()
|
||||
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
|
||||
return done(err) if err?
|
||||
if !isAllowed
|
||||
logger.log email:email, "too many login requests"
|
||||
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
|
||||
AuthenticationManager.authenticate email: email, password, (error, user) ->
|
||||
return done(error) if error?
|
||||
if user?
|
||||
# async actions
|
||||
UserHandler.setupLoginData(user, ()->)
|
||||
LoginRateLimiter.recordSuccessfulLogin(email)
|
||||
AuthenticationController._recordSuccessfulLogin(user._id)
|
||||
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
|
||||
Analytics.identifyUser(user._id, req.sessionID)
|
||||
logger.log email: email, user_id: user._id.toString(), "successful log in"
|
||||
req.session.justLoggedIn = true
|
||||
# capture the request ip for use when creating the session
|
||||
user._login_req_ip = req.ip
|
||||
return done(null, user)
|
||||
else
|
||||
AuthenticationController._recordFailedLogin()
|
||||
logger.log email: email, "failed log in"
|
||||
return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
|
||||
Modules = require "../../infrastructure/Modules"
|
||||
Modules.hooks.fire 'preDoPassportLogin', req, email, (err, infoList) ->
|
||||
return next(err) if err?
|
||||
info = infoList.find((i) => i?)
|
||||
if info?
|
||||
return done(null, false, info)
|
||||
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
|
||||
return done(err) if err?
|
||||
if !isAllowed
|
||||
logger.log email:email, "too many login requests"
|
||||
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
|
||||
AuthenticationManager.authenticate email: email, password, (error, user) ->
|
||||
return done(error) if error?
|
||||
if user?
|
||||
# async actions
|
||||
return done(null, user)
|
||||
else
|
||||
AuthenticationController._recordFailedLogin()
|
||||
logger.log email: email, "failed log in"
|
||||
return done(
|
||||
null,
|
||||
false,
|
||||
{text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}
|
||||
)
|
||||
|
||||
_loginAsyncHandlers: (req, user) ->
|
||||
UserHandler.setupLoginData(user, ()->)
|
||||
LoginRateLimiter.recordSuccessfulLogin(user.email)
|
||||
AuthenticationController._recordSuccessfulLogin(user._id)
|
||||
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
|
||||
Analytics.identifyUser(user._id, req.sessionID)
|
||||
logger.log email: user.email, user_id: user._id.toString(), "successful log in"
|
||||
req.session.justLoggedIn = true
|
||||
# capture the request ip for use when creating the session
|
||||
user._login_req_ip = req.ip
|
||||
|
||||
setInSessionUser: (req, props) ->
|
||||
for key, value of props
|
||||
|
|
|
@ -49,7 +49,7 @@ module.exports = (backendGroup)->
|
|||
return callback()
|
||||
serverId = @_parseServerIdFromResponse(response)
|
||||
if !serverId? # We don't get a cookie back if it hasn't changed
|
||||
return callback()
|
||||
return rclient.expire(@buildKey(project_id), Settings.clsiCookie.ttl, callback)
|
||||
if rclient_secondary?
|
||||
@_setServerIdInRedis rclient_secondary, project_id, serverId
|
||||
@_setServerIdInRedis rclient, project_id, serverId, (err) ->
|
||||
|
|
|
@ -7,8 +7,8 @@ ProjectGetter = require("../Project/ProjectGetter")
|
|||
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
|
||||
logger = require "logger-sharelatex"
|
||||
Url = require("url")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")()
|
||||
NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")("newBackendcloud")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
|
||||
NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi_new?.backendGroupName)
|
||||
ClsiStateManager = require("./ClsiStateManager")
|
||||
_ = require("underscore")
|
||||
async = require("async")
|
||||
|
@ -100,6 +100,7 @@ module.exports = ClsiManager =
|
|||
timer = new Metrics.Timer("compile.currentBackend")
|
||||
request opts, (err, response, body)->
|
||||
timer.done()
|
||||
Metrics.inc "compile.currentBackend.response.#{response?.statusCode}"
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi"
|
||||
return callback(err)
|
||||
|
@ -111,13 +112,14 @@ module.exports = ClsiManager =
|
|||
newBackend: (cb)->
|
||||
startTime = new Date()
|
||||
ClsiManager._makeNewBackendRequest project_id, opts, (err, response, body)->
|
||||
Metrics.inc "compile.newBackend.response.#{response?.statusCode}"
|
||||
cb(err, {response:response, body:body, finishTime:new Date() - startTime})
|
||||
}, (err, results)->
|
||||
timeDifference = results.newBackend?.finishTime - results.currentBackend?.finishTime
|
||||
statusCodeSame = results.newBackend?.response?.statusCode == results.currentBackend?.response?.statusCode
|
||||
currentCompileTime = results.currentBackend?.finishTime
|
||||
newBackendCompileTime = results.newBackend?.finishTime
|
||||
logger.log {statusCodeSame, timeDifference, currentCompileTime, newBackendCompileTime}, "both clsi requests returned"
|
||||
logger.log {statusCodeSame, timeDifference, currentCompileTime, newBackendCompileTime, project_id}, "both clsi requests returned"
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ Settings = require "settings-sharelatex"
|
|||
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
RateLimiter = require("../../infrastructure/RateLimiter")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")()
|
||||
ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
|
||||
Path = require("path")
|
||||
|
||||
module.exports = CompileController =
|
||||
|
|
|
@ -163,6 +163,13 @@ module.exports = EditorController =
|
|||
EditorRealTimeController.emitToRoom project_id, 'compilerUpdated', compiler
|
||||
callback()
|
||||
|
||||
setImageName : (project_id, imageName, callback = (err) ->) ->
|
||||
ProjectOptionsHandler.setImageName project_id, imageName, (err) ->
|
||||
return callback(err) if err?
|
||||
logger.log imageName:imageName, project_id:project_id, "setting imageName"
|
||||
EditorRealTimeController.emitToRoom project_id, 'imageNameUpdated', imageName
|
||||
callback()
|
||||
|
||||
setSpellCheckLanguage : (project_id, languageCode, callback = (err) ->) ->
|
||||
ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err) ->
|
||||
return callback(err) if err?
|
||||
|
|
|
@ -68,6 +68,20 @@ V1ConnectionError = (message) ->
|
|||
return error
|
||||
V1ConnectionError.prototype.__proto___ = Error.prototype
|
||||
|
||||
UnconfirmedEmailError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "UnconfirmedEmailError"
|
||||
error.__proto__ = UnconfirmedEmailError.prototype
|
||||
return error
|
||||
UnconfirmedEmailError.prototype.__proto___ = Error.prototype
|
||||
|
||||
EmailExistsError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "EmailExistsError"
|
||||
error.__proto__ = EmailExistsError.prototype
|
||||
return error
|
||||
EmailExistsError.prototype.__proto___ = Error.prototype
|
||||
|
||||
module.exports = Errors =
|
||||
NotFoundError: NotFoundError
|
||||
ServiceNotConfiguredError: ServiceNotConfiguredError
|
||||
|
@ -79,3 +93,5 @@ module.exports = Errors =
|
|||
V1HistoryNotSyncedError: V1HistoryNotSyncedError
|
||||
ProjectHistoryDisabledError: ProjectHistoryDisabledError
|
||||
V1ConnectionError: V1ConnectionError
|
||||
UnconfirmedEmailError: UnconfirmedEmailError
|
||||
EmailExistsError: EmailExistsError
|
||||
|
|
|
@ -37,3 +37,11 @@ module.exports =
|
|||
status_detail: parsed_export.status_detail
|
||||
}
|
||||
res.send export_json: json
|
||||
|
||||
exportZip: (req, res) ->
|
||||
{export_id} = req.params
|
||||
AuthenticationController.getLoggedInUserId(req)
|
||||
ExportsHandler.fetchZip export_id, (err, export_zip_url) ->
|
||||
return err if err?
|
||||
|
||||
res.redirect export_zip_url
|
||||
|
|
|
@ -57,6 +57,9 @@ module.exports = ExportsHandler = self =
|
|||
historyId: project.overleaf?.history?.id
|
||||
historyVersion: historyVersion
|
||||
v1ProjectId: project.overleaf?.id
|
||||
metadata:
|
||||
compiler: project.compiler
|
||||
imageName: project.imageName
|
||||
user:
|
||||
id: user_id
|
||||
firstName: user.first_name
|
||||
|
@ -115,3 +118,19 @@ module.exports = ExportsHandler = self =
|
|||
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
|
||||
logger.err err:err, export:export_id, "v1 export returned failure status code: #{res.statusCode}"
|
||||
callback err
|
||||
|
||||
fetchZip: (export_id, callback=(err, zip_url) ->) ->
|
||||
console.log("#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url")
|
||||
request.get {
|
||||
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url"
|
||||
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
|
||||
}, (err, res, body) ->
|
||||
if err?
|
||||
logger.err err:err, export:export_id, "error making request to v1 export"
|
||||
callback err
|
||||
else if 200 <= res.statusCode < 300
|
||||
callback null, body
|
||||
else
|
||||
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
|
||||
logger.err err:err, export:export_id, "v1 export zip fetch returned failure status code: #{res.statusCode}"
|
||||
callback err
|
||||
|
|
|
@ -21,6 +21,13 @@ module.exports = HistoryController =
|
|||
req.useProjectHistory = false
|
||||
next()
|
||||
|
||||
ensureProjectHistoryEnabled: (req, res, next = (error) ->) ->
|
||||
if req.useProjectHistory?
|
||||
next()
|
||||
else
|
||||
logger.log {project_id}, "project history not enabled"
|
||||
res.sendStatus(404)
|
||||
|
||||
proxyToHistoryApi: (req, res, next = (error) ->) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
|
||||
|
@ -41,22 +48,17 @@ module.exports = HistoryController =
|
|||
user_id = AuthenticationController.getLoggedInUserId req
|
||||
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
|
||||
logger.log url: url, "proxying to history api"
|
||||
request {
|
||||
HistoryController._makeRequest {
|
||||
url: url
|
||||
method: req.method
|
||||
json: true
|
||||
headers:
|
||||
"X-User-Id": user_id
|
||||
}, (error, response, body) ->
|
||||
}, (error, body) ->
|
||||
return next(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
HistoryManager.injectUserDetails body, (error, data) ->
|
||||
return next(error) if error?
|
||||
res.json data
|
||||
else
|
||||
error = new Error("history api responded with non-success code: #{response.statusCode}")
|
||||
logger.error err: error, user_id: user_id, "error proxying request to history api"
|
||||
next(error)
|
||||
HistoryManager.injectUserDetails body, (error, data) ->
|
||||
return next(error) if error?
|
||||
res.json data
|
||||
|
||||
buildHistoryServiceUrl: (useProjectHistory) ->
|
||||
# choose a history service, either document-level (trackchanges)
|
||||
|
@ -98,3 +100,46 @@ module.exports = HistoryController =
|
|||
doc_id: doc._id
|
||||
}
|
||||
|
||||
getLabels: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
HistoryController._makeRequest {
|
||||
method: "GET"
|
||||
url: "#{settings.apis.project_history.url}/project/#{project_id}/labels"
|
||||
json: true
|
||||
}, (error, labels) ->
|
||||
return next(error) if error?
|
||||
res.json labels
|
||||
|
||||
createLabel: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
{comment, version} = req.body
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
HistoryController._makeRequest {
|
||||
method: "POST"
|
||||
url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels"
|
||||
json: {comment, version}
|
||||
}, (error, label) ->
|
||||
return next(error) if error?
|
||||
res.json label
|
||||
|
||||
deleteLabel: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
label_id = req.params.label_id
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
HistoryController._makeRequest {
|
||||
method: "DELETE"
|
||||
url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}"
|
||||
}, (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
||||
_makeRequest: (options, callback) ->
|
||||
request options, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
callback(null, body)
|
||||
else
|
||||
error = new Error("history api responded with non-success code: #{response.statusCode}")
|
||||
logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}"
|
||||
callback(error)
|
||||
|
|
|
@ -3,12 +3,20 @@ metrics = require("metrics-sharelatex")
|
|||
settings = require "settings-sharelatex"
|
||||
request = require "request"
|
||||
|
||||
module.exports = UserAffiliationsManager =
|
||||
getAffiliations: (userId, callback = (error, body) ->) ->
|
||||
module.exports = InstitutionsAPI =
|
||||
getInstitutionAffiliations: (institutionId, callback = (error, body) ->) ->
|
||||
makeAffiliationRequest {
|
||||
method: 'GET'
|
||||
path: "/api/v2/institutions/#{institutionId.toString()}/affiliations"
|
||||
defaultErrorMessage: "Couldn't get institution affiliations"
|
||||
}, callback
|
||||
|
||||
|
||||
getUserAffiliations: (userId, callback = (error, body) ->) ->
|
||||
makeAffiliationRequest {
|
||||
method: 'GET'
|
||||
path: "/api/v2/users/#{userId.toString()}/affiliations"
|
||||
defaultErrorMessage: "Couldn't get affiliations"
|
||||
defaultErrorMessage: "Couldn't get user affiliations"
|
||||
}, callback
|
||||
|
||||
|
||||
|
@ -72,15 +80,18 @@ makeAffiliationRequest = (requestOptions, callback = (error) ->) ->
|
|||
errorMessage = "#{response.statusCode}: #{body.errors}"
|
||||
else
|
||||
errorMessage = "#{requestOptions.defaultErrorMessage}: #{response.statusCode}"
|
||||
|
||||
logger.err path: requestOptions.path, body: requestOptions.body, errorMessage
|
||||
return callback(new Error(errorMessage))
|
||||
|
||||
callback(null, body)
|
||||
|
||||
[
|
||||
'getAffiliations',
|
||||
'getInstitutionAffiliations'
|
||||
'getUserAffiliations',
|
||||
'addAffiliation',
|
||||
'removeAffiliation',
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod(
|
||||
UserAffiliationsManager, method, 'mongo.UserAffiliationsManager', logger
|
||||
InstitutionsAPI, method, 'mongo.InstitutionsAPI', logger
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
InstitutionsGetter = require './InstitutionsGetter'
|
||||
PlansLocator = require '../Subscription/PlansLocator'
|
||||
Settings = require 'settings-sharelatex'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
module.exports = InstitutionsFeatures =
|
||||
getInstitutionsFeatures: (userId, callback = (error, features) ->) ->
|
||||
InstitutionsFeatures.hasLicence userId, (error, hasLicence) ->
|
||||
return callback error if error?
|
||||
return callback(null, {}) unless hasLicence
|
||||
plan = PlansLocator.findLocalPlanInSettings Settings.institutionPlanCode
|
||||
callback(null, plan?.features or {})
|
||||
|
||||
|
||||
hasLicence: (userId, callback = (error, hasLicence) ->) ->
|
||||
InstitutionsGetter.getConfirmedInstitutions userId, (error, institutions) ->
|
||||
return callback error if error?
|
||||
|
||||
hasLicence = institutions.some (institution) ->
|
||||
institution.licence and institution.licence != 'free'
|
||||
|
||||
callback(null, hasLicence)
|
|
@ -0,0 +1,14 @@
|
|||
UserGetter = require '../User/UserGetter'
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
module.exports = InstitutionsGetter =
|
||||
getConfirmedInstitutions: (userId, callback = (error, institutions) ->) ->
|
||||
UserGetter.getUserFullEmails userId, (error, emailsData) ->
|
||||
return callback error if error?
|
||||
|
||||
confirmedInstitutions = emailsData.filter (emailData) ->
|
||||
emailData.confirmedAt? and emailData.affiliation?.institution?
|
||||
.map (emailData) ->
|
||||
emailData.affiliation?.institution
|
||||
|
||||
callback(null, confirmedInstitutions)
|
|
@ -0,0 +1,17 @@
|
|||
logger = require 'logger-sharelatex'
|
||||
async = require 'async'
|
||||
db = require("../../infrastructure/mongojs").db
|
||||
ObjectId = require("../../infrastructure/mongojs").ObjectId
|
||||
{ getInstitutionAffiliations } = require('./InstitutionsAPI')
|
||||
FeaturesUpdater = require('../Subscription/FeaturesUpdater')
|
||||
|
||||
ASYNC_LIMIT = 10
|
||||
module.exports = InstitutionsManager =
|
||||
upgradeInstitutionUsers: (institutionId, callback = (error) ->) ->
|
||||
getInstitutionAffiliations institutionId, (error, affiliations) ->
|
||||
return callback(error) if error
|
||||
async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, callback
|
||||
|
||||
refreshFeatures = (affiliation, callback) ->
|
||||
userId = ObjectId(affiliation.user_id)
|
||||
FeaturesUpdater.refreshFeatures(userId, true, callback)
|
|
@ -1,37 +1,68 @@
|
|||
async = require('async')
|
||||
Request = require('request')
|
||||
logger = require 'logger-sharelatex'
|
||||
Settings = require 'settings-sharelatex'
|
||||
crypto = require('crypto')
|
||||
Mailchimp = require('mailchimp-api-v3')
|
||||
|
||||
if !Settings.mailchimp?.api_key?
|
||||
logger.info "Using newsletter provider: none"
|
||||
mailchimp =
|
||||
request: (opts, cb)-> cb()
|
||||
else
|
||||
logger.info "Using newsletter provider: mailchimp"
|
||||
mailchimp = new Mailchimp(Settings.mailchimp?.api_key)
|
||||
|
||||
module.exports =
|
||||
|
||||
subscribe: (user, callback = () ->)->
|
||||
if !Settings.markdownmail?
|
||||
logger.warn "No newsletter provider configured so not subscribing user"
|
||||
return callback()
|
||||
logger.log user:user, email:user.email, "trying to subscribe user to the mailing list"
|
||||
options = buildOptions(user, true)
|
||||
Request.post options, (err, response, body)->
|
||||
logger.log body:body, user:user, "finished attempting to subscribe the user to the news letter"
|
||||
logger.log options:options, user:user, email:user.email, "trying to subscribe user to the mailing list"
|
||||
mailchimp.request options, (err)->
|
||||
if err?
|
||||
logger.err err:err, "error subscribing person to newsletter"
|
||||
else
|
||||
logger.log user:user, "finished subscribing user to the newsletter"
|
||||
callback(err)
|
||||
|
||||
unsubscribe: (user, callback = () ->)->
|
||||
if !Settings.markdownmail?
|
||||
logger.warn "No newsletter provider configured so not unsubscribing user"
|
||||
return callback()
|
||||
logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list"
|
||||
options = buildOptions(user, false)
|
||||
Request.post options, (err, response, body)->
|
||||
logger.log err:err, body:body, email:user.email, "compled newsletter unsubscribe attempt"
|
||||
mailchimp.request options, (err)->
|
||||
if err?
|
||||
logger.err err:err, "error unsubscribing person to newsletter"
|
||||
else
|
||||
logger.log user:user, "finished unsubscribing user to the newsletter"
|
||||
callback(err)
|
||||
|
||||
changeEmail: (oldEmail, newEmail, callback = ()->)->
|
||||
options = buildOptions({email:oldEmail})
|
||||
delete options.body.status
|
||||
options.body.email_address = newEmail
|
||||
mailchimp.request options, (err)->
|
||||
# if the user has unsubscribed mailchimp will error on email address change
|
||||
if err? and err?.message.indexOf("could not be validated") == -1
|
||||
logger.err err:err, "error changing email in newsletter"
|
||||
return callback(err)
|
||||
else
|
||||
logger.log "finished changing email in the newsletter"
|
||||
return callback()
|
||||
|
||||
hashEmail = (email)->
|
||||
crypto.createHash('md5').update(email.toLowerCase()).digest("hex")
|
||||
|
||||
buildOptions = (user, is_subscribed)->
|
||||
options =
|
||||
json:
|
||||
secret_token: Settings.markdownmail.secret
|
||||
name: "#{user.first_name} #{user.last_name}"
|
||||
email: user.email
|
||||
subscriber_list_id: Settings.markdownmail.list_id
|
||||
is_subscribed: is_subscribed
|
||||
url: "https://www.markdownmail.io/lists/subscribe"
|
||||
timeout: 30 * 1000
|
||||
return options
|
||||
status = if is_subscribed then "subscribed" else "unsubscribed"
|
||||
subscriber_hash = hashEmail(user.email)
|
||||
opts =
|
||||
method: "PUT"
|
||||
path: "/lists/#{Settings.mailchimp?.list_id}/members/#{subscriber_hash}"
|
||||
body:
|
||||
status_if_new: status
|
||||
status: status
|
||||
email_address:user.email
|
||||
merge_fields:
|
||||
FNAME: user.first_name
|
||||
LNAME: user.last_name
|
||||
MONGO_ID:user._id
|
||||
return opts
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ Modules = require '../../infrastructure/Modules'
|
|||
ProjectEntityHandler = require './ProjectEntityHandler'
|
||||
crypto = require 'crypto'
|
||||
{ V1ConnectionError } = require '../Errors/Errors'
|
||||
Features = require('../../infrastructure/Features')
|
||||
|
||||
module.exports = ProjectController =
|
||||
|
||||
|
@ -48,6 +49,10 @@ module.exports = ProjectController =
|
|||
jobs.push (callback) ->
|
||||
editorController.setCompiler project_id, req.body.compiler, callback
|
||||
|
||||
if req.body.imageName?
|
||||
jobs.push (callback) ->
|
||||
editorController.setImageName project_id, req.body.imageName, callback
|
||||
|
||||
if req.body.name?
|
||||
jobs.push (callback) ->
|
||||
editorController.renameProject project_id, req.body.name, callback
|
||||
|
@ -344,9 +349,9 @@ module.exports = ProjectController =
|
|||
themes: THEME_LIST
|
||||
maxDocLength: Settings.max_doc_length
|
||||
useV2History: !!project.overleaf?.history?.display
|
||||
showRichText: req.query?.rt == 'true'
|
||||
richTextEnabled: Features.hasFeature('rich-text')
|
||||
showTestControls: req.query?.tc == 'true' || user.isAdmin
|
||||
showPublishModal: req.query?.pm == 'true'
|
||||
allowedImageNames: Settings.allowedImageNames || []
|
||||
timer.done()
|
||||
|
||||
_buildProjectList: (allProjects, v1Projects = [])->
|
||||
|
|
|
@ -55,7 +55,8 @@ module.exports = ProjectCreationHandler =
|
|||
if Settings.apis?.project_history?.displayHistoryForNewProjects
|
||||
project.overleaf.history.display = true
|
||||
if Settings.currentImageName?
|
||||
project.imageName = Settings.currentImageName
|
||||
# avoid clobbering any imageName already set in attributes (e.g. importedImageName)
|
||||
project.imageName ?= Settings.currentImageName
|
||||
project.rootFolder[0] = rootFolder
|
||||
User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
|
||||
project.spellCheckLanguage = user.ace.spellCheckLanguage
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
_ = require("underscore")
|
||||
|
||||
Path = require 'path'
|
||||
|
||||
module.exports = ProjectEditorHandler =
|
||||
trackChangesAvailable: false
|
||||
|
@ -20,6 +20,7 @@ module.exports = ProjectEditorHandler =
|
|||
members: []
|
||||
invites: invites
|
||||
tokens: project.tokens
|
||||
imageName: if project.imageName? then Path.basename(project.imageName) else undefined
|
||||
|
||||
if !result.invites?
|
||||
result.invites = []
|
||||
|
|
|
@ -17,6 +17,16 @@ module.exports =
|
|||
if callback?
|
||||
callback()
|
||||
|
||||
setImageName : (project_id, imageName, callback = ()->)->
|
||||
logger.log project_id:project_id, imageName:imageName, "setting the imageName"
|
||||
imageName = imageName.toLowerCase()
|
||||
if ! _.some(settings.allowedImageNames, (allowed) -> imageName is allowed.imageName)
|
||||
return callback()
|
||||
conditions = {_id:project_id}
|
||||
update = {imageName: settings.imageRoot + '/' + imageName}
|
||||
Project.update conditions, update, {}, (err)->
|
||||
if callback?
|
||||
callback()
|
||||
|
||||
setSpellCheckLanguage: (project_id, languageCode, callback = ()->)->
|
||||
logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language"
|
||||
|
|
|
@ -7,6 +7,7 @@ Settings = require("settings-sharelatex")
|
|||
logger = require("logger-sharelatex")
|
||||
ReferalFeatures = require("../Referal/ReferalFeatures")
|
||||
V1SubscriptionManager = require("./V1SubscriptionManager")
|
||||
InstitutionsFeatures = require '../Institutions/InstitutionsFeatures'
|
||||
|
||||
oneMonthInSeconds = 60 * 60 * 24 * 30
|
||||
|
||||
|
@ -21,9 +22,11 @@ module.exports = FeaturesUpdater =
|
|||
if error?
|
||||
logger.err {err: error, user_id}, "error notifying v1 about updated features"
|
||||
|
||||
|
||||
jobs =
|
||||
individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb
|
||||
groupFeatureSets: (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb
|
||||
institutionFeatures:(cb) -> InstitutionsFeatures.getInstitutionsFeatures user_id, cb
|
||||
v1Features: (cb) -> FeaturesUpdater._getV1Features user_id, cb
|
||||
bonusFeatures: (cb) -> ReferalFeatures.getBonusFeatures user_id, cb
|
||||
async.series jobs, (err, results)->
|
||||
|
@ -32,9 +35,9 @@ module.exports = FeaturesUpdater =
|
|||
"error getting subscription or group for refreshFeatures"
|
||||
return callback(err)
|
||||
|
||||
{individualFeatures, groupFeatureSets, v1Features, bonusFeatures} = results
|
||||
logger.log {user_id, individualFeatures, groupFeatureSets, v1Features, bonusFeatures}, 'merging user features'
|
||||
featureSets = groupFeatureSets.concat [individualFeatures, v1Features, bonusFeatures]
|
||||
{individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures} = results
|
||||
logger.log {user_id, individualFeatures, groupFeatureSets, institutionFeatures, v1Features, bonusFeatures}, 'merging user features'
|
||||
featureSets = groupFeatureSets.concat [individualFeatures, institutionFeatures, v1Features, bonusFeatures]
|
||||
features = _.reduce(featureSets, FeaturesUpdater._mergeFeatures, Settings.defaultFeatures)
|
||||
|
||||
logger.log {user_id, features}, 'updating user features'
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = LimitationsManager =
|
|||
if user.features? and user.features.collaborators?
|
||||
callback null, user.features.collaborators
|
||||
else
|
||||
callback null, Settings.defaultPlanCode.collaborators
|
||||
callback null, Settings.defaultFeatures.collaborators
|
||||
|
||||
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
|
||||
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
|
||||
|
@ -89,13 +89,13 @@ module.exports = LimitationsManager =
|
|||
|
||||
return currentTotal >= subscription.membersLimit
|
||||
|
||||
hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)->
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
|
||||
hasGroupMembersLimitReached: (subscriptionId, callback = (err, limitReached, subscription)->)->
|
||||
SubscriptionLocator.getSubscription subscriptionId, (err, subscription)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting users subscription"
|
||||
logger.err err:err, subscriptionId: subscriptionId, "error getting subscription"
|
||||
return callback(err)
|
||||
if !subscription?
|
||||
logger.err user_id:user_id, "no subscription found for user"
|
||||
logger.err subscriptionId: subscriptionId, "no subscription found"
|
||||
return callback("no subscription found")
|
||||
|
||||
limitReached = LimitationsManager.teamHasReachedMemberLimit(subscription)
|
||||
|
|
|
@ -9,9 +9,6 @@ module.exports = SubscriptionDomainHandler =
|
|||
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
|
||||
return licence
|
||||
|
||||
rejectInvitationToGroup: (user, subscription, callback)->
|
||||
removeUserFromGroup(subscription.admin_id, user._id, callback)
|
||||
|
||||
getDomainLicencePage: (user)->
|
||||
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
|
||||
if licence?.verifyEmail
|
||||
|
|
|
@ -9,46 +9,56 @@ async = require("async")
|
|||
|
||||
module.exports =
|
||||
|
||||
addUserToGroup: (req, res)->
|
||||
addUserToGroup: (req, res, next)->
|
||||
adminUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
newEmail = req.body?.email?.toLowerCase()?.trim()
|
||||
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
|
||||
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
|
||||
if err?
|
||||
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
|
||||
return res.sendStatus 500
|
||||
result =
|
||||
user:user
|
||||
if err and err.limitReached
|
||||
result.limitReached = true
|
||||
res.json(result)
|
||||
|
||||
removeUserFromGroup: (req, res)->
|
||||
getManagedSubscription adminUserId, (error, subscription) ->
|
||||
return next(error) if error?
|
||||
|
||||
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
|
||||
|
||||
SubscriptionGroupHandler.addUserToGroup subscription._id, newEmail, (err, user)->
|
||||
if err?
|
||||
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
|
||||
return res.sendStatus 500
|
||||
result =
|
||||
user:user
|
||||
if err and err.limitReached
|
||||
result.limitReached = true
|
||||
res.json(result)
|
||||
|
||||
removeUserFromGroup: (req, res, next)->
|
||||
adminUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
userToRemove_id = req.params.user_id
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
getManagedSubscription adminUserId, (error, subscription) ->
|
||||
return next(error) if error?
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
|
||||
SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
|
||||
removeSelfFromGroup: (req, res)->
|
||||
removeSelfFromGroup: (req, res, next)->
|
||||
adminUserId = req.query.admin_user_id
|
||||
userToRemove_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
getManagedSubscription adminUserId, (error, subscription) ->
|
||||
return next(error) if error?
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
|
||||
SubscriptionGroupHandler.removeUserFromGroup subscription._id, userToRemove_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
|
||||
renderSubscriptionGroupAdminPage: (req, res)->
|
||||
renderSubscriptionGroupAdminPage: (req, res, next)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
|
||||
getManagedSubscription user_id, (error, subscription)->
|
||||
return next(error) if error?
|
||||
if !subscription?.groupPlan
|
||||
return res.redirect("/user/subscription")
|
||||
SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)->
|
||||
SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)->
|
||||
res.render "subscriptions/group_admin",
|
||||
title: 'group_admin'
|
||||
users: users
|
||||
|
@ -57,10 +67,11 @@ module.exports =
|
|||
exportGroupCsv: (req, res)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user_id: user_id, "exporting group csv"
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
|
||||
getManagedSubscription user_id, (err, subscription)->
|
||||
return next(error) if error?
|
||||
if !subscription.groupPlan
|
||||
return res.redirect("/")
|
||||
SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)->
|
||||
SubscriptionGroupHandler.getPopulatedListOfMembers subscription._id, (err, users)->
|
||||
groupCsv = ""
|
||||
for user in users
|
||||
groupCsv += user.email + "\n"
|
||||
|
@ -70,3 +81,13 @@ module.exports =
|
|||
)
|
||||
res.contentType('text/csv')
|
||||
res.send(groupCsv)
|
||||
|
||||
|
||||
getManagedSubscription = (managerId, callback) ->
|
||||
SubscriptionLocator.findManagedSubscription managerId, (err, subscription)->
|
||||
if subscription?
|
||||
logger.log managerId: managerId, "got managed subscription"
|
||||
else
|
||||
err ||= new Error("No subscription found managed by user #{managerId}")
|
||||
|
||||
return callback(err, subscription)
|
||||
|
|
|
@ -14,19 +14,19 @@ NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
|||
|
||||
module.exports = SubscriptionGroupHandler =
|
||||
|
||||
addUserToGroup: (adminUserId, newEmail, callback)->
|
||||
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group"
|
||||
LimitationsManager.hasGroupMembersLimitReached adminUserId, (err, limitReached, subscription)->
|
||||
addUserToGroup: (subscriptionId, newEmail, callback)->
|
||||
logger.log subscriptionId:subscriptionId, newEmail:newEmail, "adding user to group"
|
||||
LimitationsManager.hasGroupMembersLimitReached subscriptionId, (err, limitReached, subscription)->
|
||||
if err?
|
||||
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error checking if limit reached for group plan"
|
||||
logger.err err:err, subscriptionId:subscriptionId, newEmail:newEmail, "error checking if limit reached for group plan"
|
||||
return callback(err)
|
||||
if limitReached
|
||||
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
|
||||
logger.err subscriptionId:subscriptionId, newEmail:newEmail, "group subscription limit reached not adding user to group"
|
||||
return callback(limitReached:limitReached)
|
||||
UserGetter.getUserByAnyEmail newEmail, (err, user)->
|
||||
return callback(err) if err?
|
||||
if user?
|
||||
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
|
||||
SubscriptionUpdater.addUserToGroup subscriptionId, user._id, (err)->
|
||||
if err?
|
||||
logger.err err:err, "error adding user to group"
|
||||
return callback(err)
|
||||
|
@ -34,30 +34,25 @@ module.exports = SubscriptionGroupHandler =
|
|||
userViewModel = buildUserViewModel(user)
|
||||
callback(err, userViewModel)
|
||||
else
|
||||
TeamInvitesHandler.createInvite adminUserId, newEmail, (err) ->
|
||||
TeamInvitesHandler.createInvite subscriptionId, newEmail, (err) ->
|
||||
return callback(err) if err?
|
||||
userViewModel = buildEmailInviteViewModel(newEmail)
|
||||
callback(err, userViewModel)
|
||||
|
||||
removeUserFromGroup: (adminUser_id, userToRemove_id, callback)->
|
||||
SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback
|
||||
removeUserFromGroup: (subscriptionId, userToRemove_id, callback)->
|
||||
SubscriptionUpdater.removeUserFromGroup subscriptionId, userToRemove_id, callback
|
||||
|
||||
replaceUserReferencesInGroups: (oldId, newId, callback) ->
|
||||
Subscription.update {admin_id: oldId}, {admin_id: newId}, (error) ->
|
||||
callback(error) if error?
|
||||
|
||||
# Mongo won't let us pull and addToSet in the same query, so do it in
|
||||
# two. Note we need to add first, since the query is based on the old user.
|
||||
query = { member_ids: oldId }
|
||||
addNewUserUpdate = $addToSet: { member_ids: newId }
|
||||
removeOldUserUpdate = $pull: { member_ids: oldId }
|
||||
replaceInArray Subscription, "manager_ids", oldId, newId, (error) ->
|
||||
callback(error) if error?
|
||||
|
||||
Subscription.update query, addNewUserUpdate, { multi: true }, (error) ->
|
||||
return callback(error) if error?
|
||||
Subscription.update query, removeOldUserUpdate, { multi: true }, callback
|
||||
replaceInArray Subscription, "member_ids", oldId, newId, callback
|
||||
|
||||
getPopulatedListOfMembers: (adminUser_id, callback)->
|
||||
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
|
||||
getPopulatedListOfMembers: (subscriptionId, callback)->
|
||||
SubscriptionLocator.getSubscription subscriptionId, (err, subscription)->
|
||||
users = []
|
||||
|
||||
for email in subscription.invited_emails or []
|
||||
|
@ -87,6 +82,24 @@ module.exports = SubscriptionGroupHandler =
|
|||
logger.log user_id:user_id, subscription_id:subscription_id, partOfGroup:partOfGroup, "checking if user is part of a group"
|
||||
callback(err, partOfGroup)
|
||||
|
||||
replaceInArray = (model, property, oldValue, newValue, callback) ->
|
||||
logger.log "Replacing #{oldValue} with #{newValue} in #{property} of #{model}"
|
||||
|
||||
# Mongo won't let us pull and addToSet in the same query, so do it in
|
||||
# two. Note we need to add first, since the query is based on the old user.
|
||||
query = {}
|
||||
query[property] = oldValue
|
||||
|
||||
setNewValue = {}
|
||||
setNewValue[property] = newValue
|
||||
|
||||
setOldValue = {}
|
||||
setOldValue[property] = oldValue
|
||||
|
||||
model.update query, { $addToSet: setNewValue }, { multi: true }, (error) ->
|
||||
return callback(error) if error?
|
||||
model.update query, { $pull: setOldValue }, { multi: true }, callback
|
||||
|
||||
buildUserViewModel = (user)->
|
||||
u =
|
||||
email: user.email
|
||||
|
|
|
@ -2,7 +2,7 @@ Subscription = require('../../models/Subscription').Subscription
|
|||
logger = require("logger-sharelatex")
|
||||
ObjectId = require('mongoose').Types.ObjectId
|
||||
|
||||
module.exports =
|
||||
module.exports = SubscriptionLocator =
|
||||
|
||||
getUsersSubscription: (user_or_id, callback)->
|
||||
if user_or_id? and user_or_id._id?
|
||||
|
@ -14,6 +14,10 @@ module.exports =
|
|||
logger.log user_id:user_id, "got users subscription"
|
||||
callback(err, subscription)
|
||||
|
||||
findManagedSubscription: (managerId, callback)->
|
||||
logger.log managerId: managerId, "finding managed subscription"
|
||||
Subscription.findOne manager_ids: managerId, callback
|
||||
|
||||
getMemberSubscriptions: (user_or_id, callback) ->
|
||||
if user_or_id? and user_or_id._id?
|
||||
user_id = user_or_id._id
|
||||
|
|
|
@ -25,13 +25,13 @@ module.exports = SubscriptionUpdater =
|
|||
return callback(err) if err?
|
||||
SubscriptionUpdater._updateSubscriptionFromRecurly recurlySubscription, subscription, callback
|
||||
|
||||
addUserToGroup: (adminUserId, userId, callback)->
|
||||
@addUsersToGroup(adminUserId, [userId], callback)
|
||||
addUserToGroup: (subscriptionId, userId, callback)->
|
||||
@addUsersToGroup(subscriptionId, [userId], callback)
|
||||
|
||||
addUsersToGroup: (adminUserId, memberIds, callback)->
|
||||
logger.log adminUserId: adminUserId, memberIds: memberIds, "adding members into mongo subscription"
|
||||
addUsersToGroup: (subscriptionId, memberIds, callback)->
|
||||
logger.log subscriptionId: subscriptionId, memberIds: memberIds, "adding members into mongo subscription"
|
||||
searchOps =
|
||||
admin_id: adminUserId
|
||||
_id: subscriptionId
|
||||
insertOperation =
|
||||
{ $push: { member_ids: { $each: memberIds } } }
|
||||
|
||||
|
@ -46,9 +46,9 @@ module.exports = SubscriptionUpdater =
|
|||
async.map userIds, FeaturesUpdater.refreshFeatures, callback
|
||||
|
||||
|
||||
removeUserFromGroup: (adminUser_id, user_id, callback)->
|
||||
removeUserFromGroup: (subscriptionId, user_id, callback)->
|
||||
searchOps =
|
||||
admin_id: adminUser_id
|
||||
_id: subscriptionId
|
||||
removeOperation =
|
||||
"$pull": {member_ids:user_id}
|
||||
Subscription.update searchOps, removeOperation, (err)->
|
||||
|
@ -71,23 +71,23 @@ module.exports = SubscriptionUpdater =
|
|||
|
||||
_createNewSubscription: (adminUser_id, callback)->
|
||||
logger.log adminUser_id:adminUser_id, "creating new subscription"
|
||||
subscription = new Subscription(admin_id:adminUser_id)
|
||||
subscription = new Subscription(admin_id:adminUser_id, manager_ids: [adminUser_id])
|
||||
subscription.freeTrial.allowed = false
|
||||
subscription.save (err)->
|
||||
callback err, subscription
|
||||
|
||||
_updateSubscriptionFromRecurly: (recurlySubscription, subscription, callback)->
|
||||
logger.log recurlySubscription:recurlySubscription, subscription:subscription, "updaing subscription"
|
||||
plan = PlansLocator.findLocalPlanInSettings(recurlySubscription.plan.plan_code)
|
||||
if recurlySubscription.state == "expired"
|
||||
subscription.recurlySubscription_id = undefined
|
||||
subscription.planCode = Settings.defaultPlanCode
|
||||
else
|
||||
subscription.recurlySubscription_id = recurlySubscription.uuid
|
||||
subscription.freeTrial.expiresAt = undefined
|
||||
subscription.freeTrial.planCode = undefined
|
||||
subscription.freeTrial.allowed = true
|
||||
subscription.planCode = recurlySubscription.plan.plan_code
|
||||
return SubscriptionUpdater.deleteSubscription subscription._id, callback
|
||||
subscription.recurlySubscription_id = recurlySubscription.uuid
|
||||
subscription.freeTrial.expiresAt = undefined
|
||||
subscription.freeTrial.planCode = undefined
|
||||
subscription.freeTrial.allowed = true
|
||||
subscription.planCode = recurlySubscription.plan.plan_code
|
||||
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
|
||||
if !plan?
|
||||
return callback(new Error("plan code not found: #{subscription.planCode}"))
|
||||
if plan.groupPlan
|
||||
subscription.groupPlan = true
|
||||
subscription.membersLimit = plan.membersLimit
|
||||
|
|
|
@ -76,7 +76,7 @@ module.exports = TeamInvitesHandler =
|
|||
return callback(err) if err?
|
||||
return callback(new Errors.NotFoundError('invite not found')) unless invite?
|
||||
|
||||
SubscriptionUpdater.addUserToGroup subscription.admin_id, userId, (err) ->
|
||||
SubscriptionUpdater.addUserToGroup subscription._id, userId, (err) ->
|
||||
return callback(err) if err?
|
||||
|
||||
removeInviteFromTeam(subscription.id, invite.email, callback)
|
||||
|
|
|
@ -13,6 +13,7 @@ UserSessionsManager = require("./UserSessionsManager")
|
|||
UserUpdater = require("./UserUpdater")
|
||||
SudoModeHandler = require('../SudoMode/SudoModeHandler')
|
||||
settings = require "settings-sharelatex"
|
||||
Errors = require "../Errors/Errors"
|
||||
|
||||
module.exports = UserController =
|
||||
|
||||
|
@ -100,7 +101,7 @@ module.exports = UserController =
|
|||
UserUpdater.changeEmailAddress user_id, newEmail, (err)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address"
|
||||
if err.message == "alread_exists"
|
||||
if err instanceof Errors.EmailExistsError
|
||||
message = req.i18n.translate("email_already_registered")
|
||||
else
|
||||
message = req.i18n.translate("problem_changing_email_address")
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
User = require("../../models/User").User
|
||||
logger = require("logger-sharelatex")
|
||||
metrics = require('metrics-sharelatex')
|
||||
{ addAffiliation } = require("./UserAffiliationsManager")
|
||||
{ addAffiliation } = require("../Institutions/InstitutionsAPI")
|
||||
|
||||
|
||||
module.exports = UserCreator =
|
||||
|
||||
createNewUser: (opts, callback)->
|
||||
logger.log opts:opts, "creating new user"
|
||||
createNewUser: (attributes, options, callback = (error, user) ->)->
|
||||
if arguments.length == 2
|
||||
callback = options
|
||||
options = {}
|
||||
logger.log user: attributes, "creating new user"
|
||||
user = new User()
|
||||
|
||||
username = opts.email.match(/^[^@]*/)
|
||||
if !opts.first_name? or opts.first_name == ""
|
||||
opts.first_name = username[0]
|
||||
username = attributes.email.match(/^[^@]*/)
|
||||
if !attributes.first_name? or attributes.first_name == ""
|
||||
attributes.first_name = username[0]
|
||||
|
||||
for key, value of opts
|
||||
for key, value of attributes
|
||||
user[key] = value
|
||||
|
||||
user.ace.syntaxValidation = true
|
||||
|
@ -27,6 +30,7 @@ module.exports = UserCreator =
|
|||
user.save (err)->
|
||||
callback(err, user)
|
||||
|
||||
return if options?.skip_affiliation
|
||||
# call addaffiliation after the main callback so it runs in the
|
||||
# background. There is no guaranty this will run so we must no rely on it
|
||||
addAffiliation user._id, user.email, (error) ->
|
||||
|
|
|
@ -4,7 +4,7 @@ ProjectDeleter = require("../Project/ProjectDeleter")
|
|||
logger = require("logger-sharelatex")
|
||||
SubscriptionHandler = require("../Subscription/SubscriptionHandler")
|
||||
async = require("async")
|
||||
{ deleteAffiliations } = require("./UserAffiliationsManager")
|
||||
{ deleteAffiliations } = require("../Institutions/InstitutionsAPI")
|
||||
|
||||
module.exports =
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ UserGetter = require("./UserGetter")
|
|||
UserUpdater = require("./UserUpdater")
|
||||
EmailHelper = require("../Helpers/EmailHelper")
|
||||
UserEmailsConfirmationHandler = require "./UserEmailsConfirmationHandler"
|
||||
{ endorseAffiliation } = require("./UserAffiliationsManager")
|
||||
{ endorseAffiliation } = require("../Institutions/InstitutionsAPI")
|
||||
logger = require("logger-sharelatex")
|
||||
Errors = require "../Errors/Errors"
|
||||
|
||||
|
@ -26,7 +26,8 @@ module.exports = UserEmailsController =
|
|||
role: req.body.role
|
||||
department: req.body.department
|
||||
UserUpdater.addEmailAddress userId, email, affiliationOptions, (error)->
|
||||
return next(error) if error?
|
||||
if error?
|
||||
return UserEmailsController._handleEmailError error, req, res, next
|
||||
UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (err) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
@ -47,9 +48,11 @@ module.exports = UserEmailsController =
|
|||
email = EmailHelper.parseEmail(req.body.email)
|
||||
return res.sendStatus 422 unless email?
|
||||
|
||||
UserUpdater.setDefaultEmailAddress userId, email, (error)->
|
||||
return next(error) if error?
|
||||
res.sendStatus 200
|
||||
UserUpdater.updateV1AndSetDefaultEmailAddress userId, email, (error)->
|
||||
if error?
|
||||
return UserEmailsController._handleEmailError error, req, res, next
|
||||
else
|
||||
return res.sendStatus 200
|
||||
|
||||
|
||||
endorse: (req, res, next) ->
|
||||
|
@ -61,6 +64,19 @@ module.exports = UserEmailsController =
|
|||
return next(error) if error?
|
||||
res.sendStatus 204
|
||||
|
||||
resendConfirmation: (req, res, next) ->
|
||||
userId = AuthenticationController.getLoggedInUserId(req)
|
||||
email = EmailHelper.parseEmail(req.body.email)
|
||||
return res.sendStatus 422 unless email?
|
||||
UserGetter.getUserByAnyEmail email, {_id:1}, (error, user) ->
|
||||
return next(error) if error?
|
||||
if !user? or user?._id?.toString() != userId
|
||||
logger.log {userId, email, foundUserId: user?._id}, "email doesn't match logged in user"
|
||||
return res.sendStatus 422
|
||||
logger.log {userId, email}, 'resending email confirmation token'
|
||||
UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus 200
|
||||
|
||||
showConfirm: (req, res, next) ->
|
||||
res.render 'user/confirm_email', {
|
||||
|
@ -82,3 +98,15 @@ module.exports = UserEmailsController =
|
|||
next(error)
|
||||
else
|
||||
res.sendStatus 200
|
||||
|
||||
_handleEmailError: (error, req, res, next) ->
|
||||
if error instanceof Errors.UnconfirmedEmailError
|
||||
return res.status(409).json {
|
||||
message: 'email must be confirmed'
|
||||
}
|
||||
else if error instanceof Errors.EmailExistsError
|
||||
return res.status(409).json {
|
||||
message: req.i18n.translate("email_already_registered")
|
||||
}
|
||||
else
|
||||
return next(error)
|
|
@ -3,7 +3,8 @@ metrics = require('metrics-sharelatex')
|
|||
logger = require('logger-sharelatex')
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
{ getAffiliations } = require("./UserAffiliationsManager")
|
||||
{ getUserAffiliations } = require("../Institutions/InstitutionsAPI")
|
||||
Errors = require("../Errors/Errors")
|
||||
|
||||
module.exports = UserGetter =
|
||||
getUser: (query, projection, callback = (error, user) ->) ->
|
||||
|
@ -31,7 +32,7 @@ module.exports = UserGetter =
|
|||
return callback error if error?
|
||||
return callback new Error('User not Found') unless user
|
||||
|
||||
getAffiliations userId, (error, affiliationsData) ->
|
||||
getUserAffiliations userId, (error, affiliationsData) ->
|
||||
return callback error if error?
|
||||
callback null, decorateFullEmails(user.email, user.emails, affiliationsData)
|
||||
|
||||
|
@ -77,7 +78,7 @@ module.exports = UserGetter =
|
|||
# check for duplicate email address. This is also enforced at the DB level
|
||||
ensureUniqueEmailAddress: (newEmail, callback) ->
|
||||
@getUserByAnyEmail newEmail, (error, user) ->
|
||||
return callback(message: 'alread_exists') if user?
|
||||
return callback(new Errors.EmailExistsError('alread_exists')) if user?
|
||||
callback(error)
|
||||
|
||||
decorateFullEmails = (defaultEmail, emailsData, affiliationsData) ->
|
||||
|
|
|
@ -68,7 +68,6 @@ module.exports =
|
|||
shouldAllowEditingDetails: shouldAllowEditingDetails
|
||||
languages: Settings.languages,
|
||||
accountSettingsTabActive: true
|
||||
showAffiliationsUI: (req.query?.aff == "true") or false
|
||||
|
||||
sessionsPage: (req, res, next) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
sanitize = require('sanitizer')
|
||||
User = require("../../models/User").User
|
||||
UserCreator = require("./UserCreator")
|
||||
UserGetter = require("./UserGetter")
|
||||
|
@ -54,7 +53,8 @@ module.exports = UserRegistrationHandler =
|
|||
(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
|
||||
(cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb
|
||||
(cb)->
|
||||
NewsLetterManager.subscribe user, ->
|
||||
if userDetails.subscribeToNewsletter == "true"
|
||||
NewsLetterManager.subscribe user, ->
|
||||
cb() #this can be slow, just fire it off
|
||||
], (err)->
|
||||
logger.log user: user, "registered"
|
||||
|
|
|
@ -5,9 +5,13 @@ db = mongojs.db
|
|||
async = require("async")
|
||||
ObjectId = mongojs.ObjectId
|
||||
UserGetter = require("./UserGetter")
|
||||
{ addAffiliation, removeAffiliation } = require("./UserAffiliationsManager")
|
||||
{ addAffiliation, removeAffiliation } = require("../Institutions/InstitutionsAPI")
|
||||
FeaturesUpdater = require("../Subscription/FeaturesUpdater")
|
||||
EmailHelper = require "../Helpers/EmailHelper"
|
||||
Errors = require "../Errors/Errors"
|
||||
Settings = require "settings-sharelatex"
|
||||
request = require 'request'
|
||||
NewsletterManager = require "../Newsletter/NewsletterManager"
|
||||
|
||||
module.exports = UserUpdater =
|
||||
updateUser: (query, update, callback = (error) ->) ->
|
||||
|
@ -96,17 +100,69 @@ module.exports = UserUpdater =
|
|||
setDefaultEmailAddress: (userId, email, callback) ->
|
||||
email = EmailHelper.parseEmail(email)
|
||||
return callback(new Error('invalid email')) if !email?
|
||||
query = _id: userId, 'emails.email': email
|
||||
update = $set: email: email
|
||||
@updateUser query, update, (error, res) ->
|
||||
if error?
|
||||
logger.err error:error, 'problem setting default emails'
|
||||
UserGetter.getUserEmail userId, (error, oldEmail) =>
|
||||
if err?
|
||||
return callback(error)
|
||||
if res.n == 0 # TODO: Check n or nMatched?
|
||||
return callback(new Error('Default email does not belong to user'))
|
||||
callback()
|
||||
query = _id: userId, 'emails.email': email
|
||||
update = $set: email: email
|
||||
@updateUser query, update, (error, res) ->
|
||||
if error?
|
||||
logger.err error:error, 'problem setting default emails'
|
||||
return callback(error)
|
||||
else if res.n == 0 # TODO: Check n or nMatched?
|
||||
return callback(new Error('Default email does not belong to user'))
|
||||
else
|
||||
NewsletterManager.changeEmail oldEmail, email, callback
|
||||
|
||||
confirmEmail: (userId, email, callback) ->
|
||||
|
||||
|
||||
updateV1AndSetDefaultEmailAddress: (userId, email, callback) ->
|
||||
@updateEmailAddressInV1 userId, email, (error) =>
|
||||
return callback(error) if error?
|
||||
@setDefaultEmailAddress userId, email, callback
|
||||
|
||||
updateEmailAddressInV1: (userId, newEmail, callback) ->
|
||||
if !Settings.apis?.v1?.url?
|
||||
return callback()
|
||||
UserGetter.getUser userId, { 'overleaf.id': 1, emails: 1 }, (error, user) ->
|
||||
return callback(error) if error?
|
||||
return callback(new Errors.NotFoundError('no user found')) if !user?
|
||||
if !user.overleaf?.id?
|
||||
return callback()
|
||||
newEmailIsConfirmed = false
|
||||
for email in user.emails
|
||||
if email.email == newEmail and email.confirmedAt?
|
||||
newEmailIsConfirmed = true
|
||||
break
|
||||
if !newEmailIsConfirmed
|
||||
return callback(new Errors.UnconfirmedEmailError("can't update v1 with unconfirmed email"))
|
||||
request {
|
||||
baseUrl: Settings.apis.v1.url
|
||||
url: "/api/v1/sharelatex/users/#{user.overleaf.id}/email"
|
||||
method: 'PUT'
|
||||
auth:
|
||||
user: Settings.apis.v1.user
|
||||
pass: Settings.apis.v1.pass
|
||||
sendImmediately: true
|
||||
json:
|
||||
user:
|
||||
email: newEmail
|
||||
timeout: 5 * 1000
|
||||
}, (error, response, body) ->
|
||||
if error?
|
||||
error = new Errors.V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED'
|
||||
return callback(error)
|
||||
if response.statusCode == 409 # Conflict
|
||||
return callback(new Errors.EmailExistsError('email exists in v1'))
|
||||
else if 200 <= response.statusCode < 300
|
||||
return callback()
|
||||
else
|
||||
return callback new Error("non-success code from v1: #{response.statusCode}")
|
||||
|
||||
confirmEmail: (userId, email, confirmedAt, callback) ->
|
||||
if arguments.length == 3
|
||||
callback = confirmedAt
|
||||
confirmedAt = new Date()
|
||||
email = EmailHelper.parseEmail(email)
|
||||
return callback(new Error('invalid email')) if !email?
|
||||
logger.log {userId, email}, 'confirming user email'
|
||||
|
@ -120,13 +176,13 @@ module.exports = UserUpdater =
|
|||
'emails.email': email
|
||||
update =
|
||||
$set:
|
||||
'emails.$.confirmedAt': new Date()
|
||||
'emails.$.confirmedAt': confirmedAt
|
||||
@updateUser query, update, (error, res) ->
|
||||
return callback(error) if error?
|
||||
logger.log {res, userId, email}, "tried to confirm email"
|
||||
if res.n == 0
|
||||
return callback(new Errors.NotFoundError('user id and email do no match'))
|
||||
callback()
|
||||
FeaturesUpdater.refreshFeatures userId, true, callback
|
||||
|
||||
[
|
||||
'updateUser'
|
||||
|
|
|
@ -109,7 +109,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
|
||||
req.session.cdnBlocked = true
|
||||
|
||||
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
|
||||
isDark = req.headers?.host?.slice(0,7)?.toLowerCase().indexOf("dark") != -1
|
||||
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
|
||||
isLive = !isDark and !isSmoke
|
||||
|
||||
|
@ -167,6 +167,11 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
path = Path.join("/img/", imgFile)
|
||||
return Url.resolve(staticFilesBase, path)
|
||||
|
||||
res.locals.mathJaxPath = res.locals.buildJsPath(
|
||||
'libs/mathjax/MathJax.js',
|
||||
{cdn:false, qs:{config:'TeX-AMS_HTML'}}
|
||||
)
|
||||
|
||||
next()
|
||||
|
||||
|
||||
|
@ -328,3 +333,8 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
defaultLineHeight : if isOl then 'normal' else 'compact'
|
||||
renderAnnouncements : !isOl
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next) ->
|
||||
res.locals.ExposedSettings =
|
||||
isOverleaf: Settings.overleaf?
|
||||
next()
|
||||
|
|
|
@ -20,5 +20,12 @@ module.exports = Features =
|
|||
return Settings.overleaf?
|
||||
when 'templates'
|
||||
return !Settings.overleaf?
|
||||
when 'affiliations'
|
||||
return Settings?.apis?.v1?.url?
|
||||
when 'rich-text'
|
||||
isEnabled = true # Switch to false to disable
|
||||
Settings.overleaf? and isEnabled
|
||||
when 'redirect-sl'
|
||||
return Settings.redirectToV2?
|
||||
else
|
||||
throw new Error("unknown feature: #{feature}")
|
||||
|
|
|
@ -68,7 +68,7 @@ Modules.loadViewIncludes app
|
|||
|
||||
app.use bodyParser.urlencoded({ extended: true, limit: "2mb"})
|
||||
# Make sure we can process the max doc length plus some overhead for JSON encoding
|
||||
app.use bodyParser.json({limit: Settings.max_doc_length + 16 * 1024}) # 16kb overhead
|
||||
app.use bodyParser.json({limit: Settings.max_doc_length + 64 * 1024}) # 64kb overhead
|
||||
app.use multer(dest: Settings.path.uploadFolder)
|
||||
app.use methodOverride()
|
||||
|
||||
|
|
|
@ -7,10 +7,12 @@ ObjectId = Schema.ObjectId
|
|||
|
||||
SubscriptionSchema = new Schema
|
||||
admin_id : {type:ObjectId, ref:'User', index: {unique: true, dropDups: true}}
|
||||
manager_ids : [ type:ObjectId, ref:'User' ]
|
||||
member_ids : [ type:ObjectId, ref:'User' ]
|
||||
invited_emails: [ String ]
|
||||
teamInvites : [ TeamInviteSchema ]
|
||||
recurlySubscription_id : String
|
||||
teamName : {type: String}
|
||||
planCode : {type: String}
|
||||
groupPlan : {type: Boolean, default: false}
|
||||
membersLimit: {type:Number, default:0}
|
||||
|
|
|
@ -22,7 +22,7 @@ UserPagesController = require('./Features/User/UserPagesController')
|
|||
DocumentController = require('./Features/Documents/DocumentController')
|
||||
CompileManager = require("./Features/Compile/CompileManager")
|
||||
CompileController = require("./Features/Compile/CompileController")
|
||||
ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
|
||||
ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
|
||||
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
|
||||
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
|
||||
FileStoreController = require("./Features/FileStore/FileStoreController")
|
||||
|
@ -115,8 +115,11 @@ module.exports = class Router
|
|||
UserEmailsController.showConfirm
|
||||
webRouter.post '/user/emails/confirm',
|
||||
UserEmailsController.confirm
|
||||
webRouter.post '/user/emails/resend_confirmation',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserEmailsController.resendConfirmation
|
||||
|
||||
unless Features.externalAuthenticationSystemUsed()
|
||||
if Features.hasFeature 'affiliations'
|
||||
webRouter.post '/user/emails',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserEmailsController.add
|
||||
|
@ -236,8 +239,13 @@ module.exports = class Router
|
|||
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
|
||||
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
|
||||
|
||||
webRouter.get "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.getLabels
|
||||
webRouter.post "/project/:Project_id/labels", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.createLabel
|
||||
webRouter.delete "/project/:Project_id/labels/:label_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.ensureProjectHistoryEnabled, HistoryController.deleteLabel
|
||||
|
||||
webRouter.post '/project/:project_id/export/:brand_variation_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportProject
|
||||
webRouter.get '/project/:project_id/export/:export_id', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportStatus
|
||||
webRouter.get '/project/:project_id/export/:export_id/zip', AuthorizationMiddlewear.ensureUserCanAdminProject, ExportsController.exportZip
|
||||
|
||||
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
|
||||
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||
|
|
122
services/web/app/views/_mixins_links.pug
Normal file
122
services/web/app/views/_mixins_links.pug
Normal file
|
@ -0,0 +1,122 @@
|
|||
mixin linkAdvisors(linkText, linkClass, track)
|
||||
//- To Do: verify path
|
||||
- var gaCategory = track && track.category ? track.category : 'All'
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
a(href="/advisors"
|
||||
class=linkClass ? linkClass : ''
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
)
|
||||
| #{linkText ? linkText : 'advisor programme'}
|
||||
|
||||
mixin linkBenefits(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/benefits" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'benefits'}
|
||||
|
||||
mixin linkBlog(linkText, linkClass, slug)
|
||||
if slug
|
||||
a(href="/blog/#{slug}" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'blog'}
|
||||
|
||||
mixin linkContact(linkText, linkClass)
|
||||
a(href="/contact" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'contact'}
|
||||
|
||||
mixin linkEducation(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/plans" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'teaching toolkit'}
|
||||
|
||||
mixin linkEmail(linkText, linkClass, email)
|
||||
//- To Do: env var?
|
||||
- var emailDomain = 'overleaf.com'
|
||||
a(href="mailto:#{email ? email : 'contact'}@#{emailDomain}" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'email'}
|
||||
|
||||
mixin linkInvite(linkText, linkClass, track)
|
||||
- var gaCategory = track && track.category ? track.category : 'All'
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
|
||||
a(href="/user/bonus"
|
||||
class=linkClass ? linkClass : ''
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
)
|
||||
| #{linkText ? linkText : 'invite your friends'}
|
||||
|
||||
mixin linkPlansAndPricing(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/plans" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'plans and pricing'}
|
||||
|
||||
mixin linkPrintNewTab(linkText, linkClass, icon, track)
|
||||
- var gaCategory = track && track.category ? track.category : null
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
|
||||
a(href='?media=print'
|
||||
class=linkClass ? linkClass : ''
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
target="_BLANK"
|
||||
)
|
||||
if icon
|
||||
i(class="fa fa-print")
|
||||
|
|
||||
| #{linkText ? linkText : 'print'}
|
||||
|
||||
mixin linkSignIn(linkText, linkClass)
|
||||
a(href="/login" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'sign in'}
|
||||
|
||||
mixin linkSignUp(linkText, linkClass)
|
||||
a(href="/register" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'sign up'}
|
||||
|
||||
mixin linkTweet(linkText, linkClass, tweetText, track)
|
||||
//- twitter-share-button is required by twitter
|
||||
- var gaCategory = track && track.category ? track.category : 'All'
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
a(class="twitter-share-button " + linkClass
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
href="https://twitter.com/intent/tweet?text=" + tweetText
|
||||
target="_BLANK"
|
||||
) #{linkText ? linkText : 'tweet'}
|
||||
|
||||
mixin linkUniversities(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/universities" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'universities'}
|
|
@ -70,6 +70,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
window.systemMessages = !{JSON.stringify(systemMessages).replace(/\//g, '\\/')};
|
||||
window.ab = {};
|
||||
window.user_id = '#{getLoggedInUserId()}';
|
||||
window.ExposedSettings = JSON.parse('!{JSON.stringify(ExposedSettings).replace(/\//g, "\\/")}');
|
||||
|
||||
- if (typeof(settings.algolia) != "undefined")
|
||||
script.
|
||||
|
|
|
@ -65,7 +65,7 @@ block content
|
|||
ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME) }",
|
||||
layout="main",
|
||||
ng-hide="state.loading",
|
||||
resize-on="layout:chat:resize",
|
||||
resize-on="layout:chat:resize,history:toggle",
|
||||
minimum-restore-size-west="130"
|
||||
custom-toggler-pane=hasFeature('custom-togglers') ? "'west'" : "false"
|
||||
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? "'" + translate("tooltip_hide_filetree") + "'" : "false"
|
||||
|
@ -103,6 +103,8 @@ block content
|
|||
h3 {{ title }}
|
||||
.modal-body(ng-bind-html="message")
|
||||
|
||||
script(src=mathJaxPath)
|
||||
|
||||
block requirejs
|
||||
script(type="text/javascript" src='/socket.io/socket.io.js')
|
||||
|
||||
|
@ -131,9 +133,9 @@ block requirejs
|
|||
window.maxDocLength = #{maxDocLength};
|
||||
window.trackChangesState = data.trackChangesState;
|
||||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||
window.richTextEnabled = #{richTextEnabled}
|
||||
window.requirejs = {
|
||||
"paths" : {
|
||||
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, qs:{config:'TeX-AMS_HTML'}})}",
|
||||
"moment": "libs/#{lib('moment')}",
|
||||
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
|
||||
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
|
||||
|
|
|
@ -15,6 +15,7 @@ div.full-size(
|
|||
.ui-layout-center(
|
||||
ng-controller="ReviewPanelController",
|
||||
ng-class="{\
|
||||
'rp-unsupported': editor.showRichText,\
|
||||
'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
|
||||
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
|
||||
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
|
||||
|
@ -25,7 +26,10 @@ div.full-size(
|
|||
'rp-loading-threads': reviewPanel.loadingThreads,\
|
||||
}"
|
||||
)
|
||||
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
|
||||
.loading-panel(
|
||||
ng-show="!editor.sharejs_doc || editor.opening",
|
||||
style=showRichText ? "top: 32px" : "",
|
||||
)
|
||||
span(ng-show="editor.open_doc_id")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
|
@ -39,7 +43,7 @@ div.full-size(
|
|||
ace-editor="editor",
|
||||
ng-if="!editor.showRichText",
|
||||
ng-show="!!editor.sharejs_doc && !editor.opening",
|
||||
style=showRichText ? "top: 32px" : "",
|
||||
style=richTextEnabled ? "top: 32px" : "",
|
||||
theme="settings.theme",
|
||||
keybindings="settings.mode",
|
||||
font-size="settings.fontSize",
|
||||
|
|
|
@ -67,3 +67,53 @@ script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
|
|||
)
|
||||
span(ng-show="!state.inflight") #{translate("restore")}
|
||||
span(ng-show="state.inflight") #{translate("restoring")} ...
|
||||
|
||||
|
||||
script(type="text/ng-template", id="historyLabelTpl")
|
||||
.history-label(
|
||||
ng-class="{ 'history-label-own' : $ctrl.isOwnedByCurrentUser }"
|
||||
)
|
||||
span.history-label-comment(
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-template="'historyLabelTooltipTpl'"
|
||||
tooltip-placement="left"
|
||||
tooltip-enable="$ctrl.showTooltip"
|
||||
)
|
||||
i.fa.fa-tag
|
||||
| {{ $ctrl.labelText }}
|
||||
button.history-label-delete-btn(
|
||||
ng-if="$ctrl.isOwnedByCurrentUser"
|
||||
stop-propagation="click"
|
||||
ng-click="$ctrl.onLabelDelete()"
|
||||
) ×
|
||||
|
||||
script(type="text/ng-template", id="historyLabelTooltipTpl")
|
||||
.history-label-tooltip
|
||||
p.history-label-tooltip-title
|
||||
i.fa.fa-tag
|
||||
| {{ $ctrl.labelText }}
|
||||
p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }}
|
||||
time.history-label-tooltip-datetime {{ $ctrl.labelCreationDateTime | formatDate }}
|
||||
|
||||
|
||||
script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("history_delete_label")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
p.help-block(ng-if="labelDetails")
|
||||
| #{translate("history_are_you_sure_delete_label")}
|
||||
strong "{{ labelDetails.comment }}"
|
||||
| ?
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="$dismiss()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
type="button"
|
||||
ng-click="deleteLabel()"
|
||||
ng-disabled="state.inflight"
|
||||
) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }}
|
||||
|
|
|
@ -3,13 +3,26 @@ aside.change-list(
|
|||
ng-controller="HistoryV2ListController"
|
||||
)
|
||||
history-entries-list(
|
||||
ng-if="!history.showOnlyLabels && !history.error"
|
||||
entries="history.updates"
|
||||
current-user="user"
|
||||
users="projectUsers"
|
||||
load-entries="loadMore()"
|
||||
load-disabled="history.loading || history.atEnd"
|
||||
load-initialize="ui.view == 'history'"
|
||||
is-loading="history.loading"
|
||||
on-entry-select="handleEntrySelect(selectedEntry)"
|
||||
on-label-delete="handleLabelDelete(label)"
|
||||
)
|
||||
history-labels-list(
|
||||
ng-if="history.showOnlyLabels && !history.error"
|
||||
labels="history.labels"
|
||||
current-user="user"
|
||||
users="projectUsers"
|
||||
is-loading="history.loading"
|
||||
selected-label="history.selection.label"
|
||||
on-label-select="handleLabelSelect(label)"
|
||||
on-label-delete="handleLabelDelete(label)"
|
||||
)
|
||||
|
||||
aside.change-list(
|
||||
|
@ -65,6 +78,14 @@ aside.change-list(
|
|||
)
|
||||
|
||||
div.description(ng-click="select()")
|
||||
history-label(
|
||||
ng-repeat="label in update.labels"
|
||||
label-text="label.comment"
|
||||
label-owner-name="getDisplayNameById(label.user_id)"
|
||||
label-creation-date-time="label.created_at"
|
||||
is-owned-by-current-user="label.user_id === user.id"
|
||||
on-label-delete="deleteLabel(label)"
|
||||
)
|
||||
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
|
||||
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
|
||||
| #{translate("file_action_edited")}
|
||||
|
@ -106,8 +127,9 @@ script(type="text/ng-template", id="historyEntriesListTpl")
|
|||
ng-repeat="entry in $ctrl.entries"
|
||||
entry="entry"
|
||||
current-user="$ctrl.currentUser"
|
||||
users="$ctrl.users"
|
||||
on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })"
|
||||
ng-show="!$ctrl.isLoading"
|
||||
on-label-delete="$ctrl.onLabelDelete({ label: label })"
|
||||
)
|
||||
.loading(ng-show="$ctrl.isLoading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
|
@ -129,6 +151,15 @@ script(type="text/ng-template", id="historyEntryTpl")
|
|||
time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }}
|
||||
|
||||
.history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })")
|
||||
history-label(
|
||||
ng-repeat="label in $ctrl.entry.labels | orderBy : '-created_at'"
|
||||
label-text="label.comment"
|
||||
label-owner-name="$ctrl.displayNameById(label.user_id)"
|
||||
label-creation-date-time="label.created_at"
|
||||
is-owned-by-current-user="label.user_id === $ctrl.currentUser.id"
|
||||
on-label-delete="$ctrl.onLabelDelete({ label: label })"
|
||||
)
|
||||
|
||||
ol.history-entry-changes
|
||||
li.history-entry-change(
|
||||
ng-repeat="pathname in ::$ctrl.entry.pathnames"
|
||||
|
@ -148,6 +179,7 @@ script(type="text/ng-template", id="historyEntryTpl")
|
|||
ng-if="::project_op.remove"
|
||||
) #{translate("file_action_deleted")}
|
||||
span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
|
||||
|
||||
.history-entry-metadata
|
||||
time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
|
||||
span
|
||||
|
@ -171,4 +203,37 @@ script(type="text/ng-template", id="historyEntryTpl")
|
|||
li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0")
|
||||
span.name(
|
||||
ng-style="$ctrl.getUserCSSStyle();"
|
||||
) #{translate("anonymous")}
|
||||
) #{translate("anonymous")}
|
||||
|
||||
script(type="text/ng-template", id="historyLabelsListTpl")
|
||||
.history-labels-list
|
||||
.history-entry-label(
|
||||
ng-repeat="label in $ctrl.labels track by label.id"
|
||||
ng-click="$ctrl.onLabelSelect({ label: label })"
|
||||
ng-class="{ 'history-entry-label-selected': label.id === $ctrl.selectedLabel.id }"
|
||||
)
|
||||
history-label(
|
||||
show-tooltip="false"
|
||||
label-text="label.comment"
|
||||
is-owned-by-current-user="label.user_id === $ctrl.currentUser.id"
|
||||
on-label-delete="$ctrl.onLabelDelete({ label: label })"
|
||||
)
|
||||
.history-entry-label-metadata
|
||||
.history-entry-label-metadata-user(ng-init="user = $ctrl.getUserById(label.user_id)")
|
||||
| Saved by
|
||||
span.name(
|
||||
ng-if="user && user._id !== $ctrl.currentUser.id"
|
||||
ng-style="$ctrl.getUserCSSStyle(user, label);"
|
||||
) {{ ::$ctrl.displayName(user) }}
|
||||
span.name(
|
||||
ng-if="user && user._id == $ctrl.currentUser.id"
|
||||
ng-style="$ctrl.getUserCSSStyle(user, label);"
|
||||
) You
|
||||
span.name(
|
||||
ng-if="user == null"
|
||||
ng-style="$ctrl.getUserCSSStyle(user, label);"
|
||||
) #{translate("anonymous")}
|
||||
time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }}
|
||||
.loading(ng-show="$ctrl.isLoading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
|
@ -45,7 +45,7 @@
|
|||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
resize-on="layout:main:resize,history:toggle",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
.alert.alert-info(ng-if="history.diff.binary")
|
||||
|
@ -70,12 +70,24 @@
|
|||
font-size="settings.fontSize",
|
||||
text="history.selectedFile.text",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
resize-on="layout:main:resize,history:toggle",
|
||||
)
|
||||
.alert.alert-info(ng-if="history.selectedFile.binary")
|
||||
| We're still working on showing image and binary changes, sorry. Stay tuned!
|
||||
.loading-panel(ng-show="history.selectedFile.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.error")
|
||||
.alert.alert-danger
|
||||
p
|
||||
| #{translate("generic_history_error")}
|
||||
a(
|
||||
ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}"
|
||||
) #{settings.adminEmail}
|
||||
p.clearfix
|
||||
a.alert-link-as-btn.pull-right(
|
||||
href
|
||||
ng-click="toggleHistory()"
|
||||
) #{translate("back_to_editor")}
|
||||
.error-panel(ng-show="history.selectedFile.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
|
|
|
@ -1,13 +1,75 @@
|
|||
.history-toolbar(
|
||||
ng-controller="HistoryV2ToolbarController"
|
||||
ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
|
||||
)
|
||||
span(ng-show="history.loadingFileTree")
|
||||
span.history-toolbar-selected-version(ng-show="history.loadingFileTree")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}
|
||||
span.history-toolbar-selected-version(
|
||||
ng-show="!history.loadingFileTree && !history.showOnlyLabels && !history.error"
|
||||
) #{translate("browsing_project_as_of")}
|
||||
time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
|
||||
.history-toolbar-btn(
|
||||
ng-click="toggleHistoryViewMode();"
|
||||
)
|
||||
i.fa
|
||||
| #{translate("compare_to_another_version")}
|
||||
span.history-toolbar-selected-version(
|
||||
ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error"
|
||||
) #{translate("browsing_project_labelled")}
|
||||
span.history-toolbar-selected-label "{{ history.selection.label.comment }}"
|
||||
div.history-toolbar-actions(
|
||||
ng-if="!history.error"
|
||||
)
|
||||
button.history-toolbar-btn(
|
||||
ng-click="showAddLabelDialog();"
|
||||
ng-if="!history.showOnlyLabels"
|
||||
ng-disabled="history.loadingFileTree"
|
||||
)
|
||||
i.fa.fa-tag
|
||||
| #{translate("history_label_this_version")}
|
||||
button.history-toolbar-btn(
|
||||
ng-click="toggleHistoryViewMode();"
|
||||
ng-disabled="history.loadingFileTree"
|
||||
)
|
||||
i.fa.fa-exchange
|
||||
| #{translate("compare_to_another_version")}
|
||||
|
||||
.history-toolbar-entries-list(
|
||||
ng-if="!history.error"
|
||||
)
|
||||
toggle-switch(
|
||||
ng-model="history.showOnlyLabels"
|
||||
label-true=translate("history_view_labels")
|
||||
label-false=translate("history_view_all")
|
||||
description=translate("history_view_a11y_description")
|
||||
)
|
||||
|
||||
script(type="text/ng-template", id="historyV2AddLabelModalTemplate")
|
||||
form(
|
||||
name="addLabelModalForm"
|
||||
ng-submit="addLabelModalFormSubmit();"
|
||||
novalidate
|
||||
)
|
||||
.modal-header
|
||||
h3 #{translate("history_add_label")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
.form-group
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder=translate("history_new_label_name")
|
||||
ng-model="inputs.labelName"
|
||||
focus-on="open"
|
||||
required
|
||||
)
|
||||
p.help-block(ng-if="update")
|
||||
| #{translate("history_new_label_added_at")}
|
||||
strong {{ update.meta.end_ts | formatDate:'ddd Do MMM YYYY, h:mm a' }}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="$dismiss()"
|
||||
) #{translate("cancel")}
|
||||
input.btn.btn-primary(
|
||||
ng-disabled="addLabelModalForm.$invalid || state.inflight"
|
||||
ng-value="state.inflight ? '" + translate("history_adding_label") + "' : '" + translate("history_add_label") + "'"
|
||||
type="submit"
|
||||
)
|
|
@ -188,6 +188,15 @@ aside#left-menu.full-size(
|
|||
option(value="pdfjs") #{translate("built_in")}
|
||||
option(value="native") #{translate("native")}
|
||||
|
||||
if (getSessionUser() && getSessionUser().isAdmin && typeof(allowedImageNames) !== 'undefined' && allowedImageNames.length > 0)
|
||||
.form-controls(ng-show="permissions.write")
|
||||
label(for="imageName") #{translate("TeXLive")}
|
||||
select(
|
||||
name="imageName"
|
||||
ng-model="project.imageName"
|
||||
)
|
||||
each image in allowedImageNames
|
||||
option(value=image.imageName) #{image.imageDesc}
|
||||
|
||||
h4 #{translate("hotkeys")}
|
||||
ul.list-unstyled.nav
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#review-panel
|
||||
#review-panel(style=richTextEnabled ? "top: 32px" : "")
|
||||
.rp-in-editor-widgets
|
||||
a.rp-track-changes-indicator(
|
||||
href
|
||||
|
@ -235,6 +235,13 @@
|
|||
i.fa.fa-list
|
||||
span.rp-nav-label #{translate("overview")}
|
||||
|
||||
.rp-unsupported-msg-wrapper
|
||||
.rp-unsupported-msg
|
||||
i.fa.fa-5x.fa-exclamation-triangle
|
||||
p.rp-unsupported-msg-title Sorry, Track Changes is not supported in Rich Text mode (yet).
|
||||
p We didn't want to delay your ability to use Rich Text mode so we've launched without support for Track Changes.
|
||||
p We're working hard to include Track Changes as soon as possible.
|
||||
|
||||
|
||||
script(type='text/ng-template', id='changeEntryTemplate')
|
||||
div
|
||||
|
|
|
@ -192,11 +192,14 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate')
|
|||
ng-click="cancel()"
|
||||
) ×
|
||||
h3(ng-if="action == 'delete'") #{translate("delete_projects")}
|
||||
h3(ng-if="action == 'archive'") #{translate("archive_projects")}
|
||||
h3(ng-if="action == 'leave'") #{translate("leave_projects")}
|
||||
h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")}
|
||||
h3(ng-if="action == 'archive-and-leave'") #{translate("archive_and_leave_projects")}
|
||||
.modal-body
|
||||
div(ng-show="projectsToDelete.length > 0")
|
||||
p #{translate("about_to_delete_projects")}
|
||||
p(ng-if="action == 'delete' || action == 'delete-and-leave'") #{translate("about_to_delete_projects")}
|
||||
p(ng-if="action == 'archive' || action == 'archive-and-leave'") #{translate("about_to_archive_projects")}
|
||||
ul
|
||||
li(ng-repeat="project in projectsToDelete | orderBy:'name'")
|
||||
strong {{project.name}}
|
||||
|
@ -345,13 +348,11 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
|
|||
i.fa.fa-flask
|
||||
.v1-import-col
|
||||
h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental
|
||||
p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2 there is:
|
||||
p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2:
|
||||
ul
|
||||
li <strong>No Journals and Services</strong> menu to submit directly to our partners yet
|
||||
li <strong>No Rich Text (WYSIWYG)</strong> mode yet
|
||||
li <strong>No linked files</strong> (to URLs or to files in other Overleaf projects) yet
|
||||
li <strong>No Zotero and CiteULike</strong> integrations yet
|
||||
li <strong>No labelled versions</strong> yet
|
||||
li You may not be able to access all of your <strong>Labelled versions</strong> yet
|
||||
li There are <strong>no Zotero and CiteULike</strong> integrations yet
|
||||
li Some <strong>Journals and Services in the Submit menu</strong> don't support direct submissions yet
|
||||
p.row-spaced-small
|
||||
| If you currently use the <strong>Overleaf Git bridge</strong> with your v1 project, you can migrate your project to the Overleaf v2 GitHub integration.
|
||||
|
|
||||
|
|
|
@ -14,5 +14,5 @@ block content
|
|||
.content.plans(ng-controller="PlansController")
|
||||
.container(class="more-details" ng-cloak ng-if="plansVariant === 'more-details'")
|
||||
include _plans_page_details_more
|
||||
.container(ng-cloak ng-if="plansVariant === 'default' || !shouldABTestPlans")
|
||||
.container(ng-cloak ng-if="plansVariant === 'default' || !shouldABTestPlans || timeout")
|
||||
include _plans_page_details_less
|
||||
|
|
|
@ -9,7 +9,7 @@ block content
|
|||
.page-header
|
||||
h1 #{translate("account_settings")}
|
||||
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||
if locals.showAffiliationsUI
|
||||
if hasFeature('affiliations')
|
||||
include settings/user-affiliations
|
||||
|
||||
form-messages(for="settingsForm")
|
||||
|
@ -22,25 +22,26 @@ block content
|
|||
h3 #{translate("update_account_info")}
|
||||
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 !hasFeature('affiliations')
|
||||
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
|
||||
|
@ -166,7 +167,12 @@ block content
|
|||
)
|
||||
i.fa.fa-check
|
||||
| #{translate("unsubscribed")}
|
||||
|
||||
|
||||
if !settings.overleaf && user.overleaf
|
||||
p
|
||||
| Please note: If you have linked your account with Overleaf
|
||||
| v2, then deleting your ShareLaTeX account will also delete
|
||||
| account and all of it's associated projects and data.
|
||||
p #{translate("need_to_leave")}
|
||||
a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
|
||||
|
||||
|
|
|
@ -3,55 +3,101 @@ form.row(
|
|||
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.
|
||||
h3 #{translate("emails_and_affiliations_title")}
|
||||
p.small #{translate("emails_and_affiliations_explanation")}
|
||||
table.table.affiliations-table
|
||||
thead
|
||||
tr
|
||||
th.affiliations-table-email Email
|
||||
th.affiliations-table-institution Institution and role
|
||||
th.affiliations-table-email #{translate("email")}
|
||||
th.affiliations-table-institution #{translate("institution_and_role")}
|
||||
th.affiliations-table-inline-actions
|
||||
tbody
|
||||
tr(
|
||||
ng-repeat="userEmail in userEmails"
|
||||
)
|
||||
td {{ userEmail.email + (userEmail.default ? ' (default)' : '') }}
|
||||
td
|
||||
| {{ userEmail.email + (userEmail.default ? ' (default)' : '') }}
|
||||
div(ng-if="!userEmail.confirmedAt").small
|
||||
strong #{translate('unconfirmed')}.
|
||||
|
|
||||
| #{translate('please_check_your_inbox')}.
|
||||
br
|
||||
a(
|
||||
href,
|
||||
ng-click="resendConfirmationEmail(userEmail)"
|
||||
) #{translate('resend_confirmation_email')}
|
||||
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
|
||||
div(ng-if="userEmail.affiliation.institution")
|
||||
div {{ userEmail.affiliation.institution.name }}
|
||||
span.small
|
||||
a(
|
||||
href
|
||||
ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department"
|
||||
ng-click="changeAffiliation(userEmail);"
|
||||
) #{translate("add_role_and_department")}
|
||||
div.small(
|
||||
ng-if="!isChangingAffiliation(userEmail.email) && (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 }}
|
||||
br
|
||||
a(
|
||||
href
|
||||
ng-click="changeAffiliation(userEmail);"
|
||||
) #{translate("change")}
|
||||
.affiliation-change-container(
|
||||
ng-if="isChangingAffiliation(userEmail.email)"
|
||||
)
|
||||
affiliation-form(
|
||||
affiliation-data="affiliationToChange"
|
||||
show-university-and-country="false"
|
||||
show-role-and-department="true"
|
||||
)
|
||||
.affiliation-change-actions.small
|
||||
a(
|
||||
href
|
||||
ng-click="saveAffiliationChange(userEmail);"
|
||||
) #{translate("save_or_cancel-save")}
|
||||
| #{translate("save_or_cancel-or" )}
|
||||
a(
|
||||
href
|
||||
ng-click="cancelAffiliationChange();"
|
||||
) #{translate("save_or_cancel-cancel")}
|
||||
td.affiliations-table-inline-actions
|
||||
// Disabled buttons don't work with tooltips, due to pointer-events: none,
|
||||
// so create a wrapper for the tooltip
|
||||
div.affiliations-table-inline-action-disabled-wrapper(
|
||||
tooltip=translate("please_confirm_your_email_before_making_it_default")
|
||||
tooltip-enable="!ui.isMakingRequest"
|
||||
ng-if="!userEmail.default && (!userEmail.confirmedAt || ui.isMakingRequest)"
|
||||
)
|
||||
button.btn.btn-sm.btn-success.affiliations-table-inline-action(
|
||||
disabled
|
||||
) #{translate("make_default")}
|
||||
button.btn.btn-sm.btn-success.affiliations-table-inline-action(
|
||||
ng-if="!userEmail.default && (userEmail.confirmedAt && !ui.isMakingRequest)"
|
||||
ng-click="setDefaultUserEmail(userEmail)"
|
||||
) #{translate("make_default")}
|
||||
|
|
||||
button.btn.btn-sm.btn-danger.affiliations-table-inline-action(
|
||||
ng-if="!userEmail.default"
|
||||
ng-click="setDefaultUserEmail(userEmail.email)"
|
||||
) Make default
|
||||
br
|
||||
a(
|
||||
href
|
||||
ng-if="!userEmail.default"
|
||||
ng-click="removeUserEmail(userEmail.email)"
|
||||
) Remove
|
||||
ng-click="removeUserEmail(userEmail)"
|
||||
ng-disabled="ui.isMakingRequest"
|
||||
tooltip=translate("remove")
|
||||
)
|
||||
i.fa.fa-fw.fa-trash
|
||||
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"
|
||||
ng-if="!ui.showAddEmailUI && !ui.isMakingRequest"
|
||||
)
|
||||
td(colspan="3")
|
||||
a(
|
||||
href
|
||||
ng-click="showAddEmailForm()"
|
||||
) Add another email
|
||||
) #{translate("add_another_email")}
|
||||
|
||||
tr.affiliations-table-highlighted-row(
|
||||
ng-if="ui.showAddEmailUI"
|
||||
ng-if="ui.showAddEmailUI && !ui.isLoadingEmails"
|
||||
)
|
||||
td
|
||||
.affiliations-form-group
|
||||
|
@ -67,106 +113,129 @@ form.row(
|
|||
input-required="true"
|
||||
)
|
||||
td
|
||||
.affiliations-table-label(
|
||||
p.affiliations-table-label(
|
||||
ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI"
|
||||
)
|
||||
| {{ newAffiliation.university.name }} (
|
||||
a(
|
||||
href
|
||||
ng-click="selectUniversityManually();"
|
||||
) change
|
||||
| )
|
||||
| {{ newAffiliation.university.name }}
|
||||
span.small
|
||||
| (
|
||||
a(
|
||||
href
|
||||
ng-click="selectUniversityManually();"
|
||||
) #{translate("change")}
|
||||
| )
|
||||
.affiliations-table-label(
|
||||
ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI"
|
||||
) Start by adding your email address.
|
||||
) #{translate("start_by_adding_your_email")}
|
||||
.affiliations-table-label(
|
||||
ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI"
|
||||
)
|
||||
| Is your email affiliated with an institution?
|
||||
| #{translate("is_email_affiliated")}
|
||||
br
|
||||
a(
|
||||
href
|
||||
ng-click="selectUniversityManually();"
|
||||
) Let us know
|
||||
.affiliations-form-group(
|
||||
ng-if="ui.showManualUniversitySelectionUI"
|
||||
) #{translate("let_us_know")}
|
||||
affiliation-form(
|
||||
affiliation-data="newAffiliation"
|
||||
show-university-and-country="ui.showManualUniversitySelectionUI"
|
||||
show-role-and-department="ui.isValidEmail && newAffiliation.university"
|
||||
)
|
||||
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"
|
||||
button.btn.btn-sm.btn-primary(
|
||||
ng-disabled="affiliationsForm.$invalid || ui.isMakingRequest"
|
||||
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
|
||||
| #{translate("add_new_email")}
|
||||
tr.affiliations-table-highlighted-row(
|
||||
ng-if="ui.isMakingRequest"
|
||||
)
|
||||
td.text-center(colspan="3", ng-if="ui.isLoadingEmails")
|
||||
i.fa.fa-fw.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
td.text-center(colspan="3", ng-if="ui.isResendingConfirmation")
|
||||
i.fa.fa-fw.fa-spin.fa-refresh
|
||||
| #{translate("sending")}...
|
||||
td.text-center(colspan="3", ng-if="!ui.isLoadingEmails && !ui.isResendingConfirmation")
|
||||
i.fa.fa-fw.fa-spin.fa-refresh
|
||||
| #{translate("saving")}...
|
||||
tr.affiliations-table-error-row(
|
||||
ng-if="ui.hasError"
|
||||
)
|
||||
td.text-center(colspan="3")
|
||||
div
|
||||
i.fa.fa-fw.fa-exclamation-triangle
|
||||
span(ng-if="!ui.errorMessage") #{translate("error_performing_request")}
|
||||
span(ng-if="ui.errorMessage") {{ui.errorMessage}}
|
||||
|
||||
hr
|
||||
|
||||
script(type="text/ng-template", id="affiliationFormTpl")
|
||||
.affiliations-form-group(
|
||||
ng-if="$ctrl.showUniversityAndCountry"
|
||||
)
|
||||
ui-select(
|
||||
ng-model="$ctrl.affiliationData.country"
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder="Country"
|
||||
) {{ $select.selected.name }}
|
||||
ui-select-choices(
|
||||
repeat="country in $ctrl.countries | filter: $select.search"
|
||||
)
|
||||
span(
|
||||
ng-bind="country.name"
|
||||
)
|
||||
.affiliations-form-group(
|
||||
ng-if="$ctrl.showUniversityAndCountry"
|
||||
)
|
||||
ui-select(
|
||||
ng-model="$ctrl.affiliationData.university"
|
||||
ng-disabled="!$ctrl.affiliationData.country"
|
||||
tagging="$ctrladdUniversityToSelection"
|
||||
tagging-label="false"
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder="Institution"
|
||||
) {{ $select.selected.name }}
|
||||
ui-select-choices(
|
||||
repeat="university in $ctrl.universities | filter: $select.search"
|
||||
)
|
||||
span(
|
||||
ng-bind="university.name"
|
||||
)
|
||||
.affiliations-form-group(
|
||||
ng-if="$ctrl.showRoleAndDepartment"
|
||||
)
|
||||
ui-select(
|
||||
ng-model="$ctrl.affiliationData.role"
|
||||
tagging
|
||||
tagging-label="false"
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder="Role"
|
||||
) {{ $select.selected }}
|
||||
ui-select-choices(
|
||||
repeat="role in $ctrl.roles | filter: $select.search"
|
||||
)
|
||||
span(
|
||||
ng-bind="role"
|
||||
)
|
||||
|
||||
.affiliations-form-group(
|
||||
ng-if="$ctrl.showRoleAndDepartment"
|
||||
)
|
||||
ui-select(
|
||||
ng-model="$ctrl.affiliationData.department"
|
||||
tagging
|
||||
tagging-label="false"
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder="Department"
|
||||
) {{ $select.selected }}
|
||||
ui-select-choices(
|
||||
repeat="department in $ctrl.departments | filter: $select.search"
|
||||
)
|
||||
span(
|
||||
ng-bind="department"
|
||||
)
|
||||
|
|
|
@ -135,6 +135,8 @@ module.exports = settings =
|
|||
url: "http://#{process.env['FILESTORE_HOST'] or 'localhost'}:3009"
|
||||
clsi:
|
||||
url: "http://#{process.env['CLSI_HOST'] or 'localhost'}:3013"
|
||||
# url: "http://#{process.env['CLSI_LB_HOST']}:3014"
|
||||
backendGroupName: undefined
|
||||
templates:
|
||||
url: "http://#{process.env['TEMPLATES_HOST'] or 'localhost'}:3007"
|
||||
githubSync:
|
||||
|
@ -277,10 +279,10 @@ module.exports = settings =
|
|||
# Third party services
|
||||
# --------------------
|
||||
#
|
||||
# ShareLaTeX's regular newsletter is managed by Markdown mail. Add your
|
||||
# ShareLaTeX's regular newsletter is managed by mailchimp. Add your
|
||||
# credentials here to integrate with this.
|
||||
# markdownmail:
|
||||
# secret: ""
|
||||
# mailchimp:
|
||||
# api_key: ""
|
||||
# list_id: ""
|
||||
#
|
||||
# Fill in your unique token from various analytics services to enable
|
||||
|
@ -339,7 +341,7 @@ module.exports = settings =
|
|||
# disablePerUserCompiles: true
|
||||
|
||||
# Domain the client (pdfjs) should download the compiled pdf from
|
||||
# pdfDownloadDomain: "http://compiles.sharelatex.test:3014"
|
||||
# pdfDownloadDomain: "http://clsi-lb:3014"
|
||||
|
||||
# Maximum size of text documents in the real-time editing system.
|
||||
max_doc_length: 2 * 1024 * 1024 # 2mb
|
||||
|
@ -425,7 +427,7 @@ module.exports = settings =
|
|||
redirects:
|
||||
"/templates/index": "/templates/"
|
||||
|
||||
reloadModuleViewsOnEachRequest: true
|
||||
reloadModuleViewsOnEachRequest: process.env['NODE_ENV'] != 'production'
|
||||
|
||||
domainLicences: [
|
||||
|
||||
|
@ -472,3 +474,14 @@ module.exports = settings =
|
|||
autoCompile:
|
||||
everyone: 100
|
||||
standard: 25
|
||||
|
||||
# currentImage: "texlive-full:2017.1"
|
||||
# imageRoot: "<DOCKER REPOSITORY ROOT>" # without any trailing slash
|
||||
|
||||
# allowedImageNames: [
|
||||
# {imageName: 'texlive-full:2017.1', imageDesc: 'TeXLive 2017'}
|
||||
# {imageName: 'wl_texlive:2018.1', imageDesc: 'Legacy OL TeXLive 2015'}
|
||||
# {imageName: 'texlive-full:2016.1', imageDesc: 'Legacy SL TeXLive 2016'}
|
||||
# {imageName: 'texlive-full:2015.1', imageDesc: 'Legacy SL TeXLive 2015'}
|
||||
# {imageName: 'texlive-full:2014.2', imageDesc: 'Legacy SL TeXLive 2014.2'}
|
||||
# ]
|
|
@ -19,6 +19,7 @@ services:
|
|||
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file,mendeley'
|
||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
- redis
|
||||
- mongo
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"lodash": "^4.13.1",
|
||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
|
||||
"lynx": "0.1.1",
|
||||
"mailchimp-api-v3": "^1.12.0",
|
||||
"marked": "^0.3.5",
|
||||
"method-override": "^2.3.3",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
|
||||
|
@ -98,7 +99,8 @@
|
|||
"v8-profiler": "^5.2.3",
|
||||
"valid-url": "^1.0.9",
|
||||
"xml2js": "0.2.0",
|
||||
"yauzl": "^2.8.0"
|
||||
"yauzl": "^2.8.0",
|
||||
"minimist": "1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^6.6.1",
|
||||
|
|
|
@ -26,7 +26,29 @@ define [
|
|||
baseUrl: window.sharelatex.sixpackDomain
|
||||
client_id: window.user_id
|
||||
})
|
||||
|
||||
|
||||
MathJax?.Hub?.Config(
|
||||
extensions: ["Safe.js"]
|
||||
messageStyle: "none"
|
||||
imageFont:null
|
||||
"HTML-CSS":
|
||||
availableFonts: ["TeX"]
|
||||
# MathJax's automatic font scaling does not work well when we render math
|
||||
# that isn't yet on the page, so we disable it and set a global font
|
||||
# scale factor
|
||||
scale: 110
|
||||
matchFontHeight: false
|
||||
TeX:
|
||||
equationNumbers: { autoNumber: "AMS" }
|
||||
useLabelIDs: false
|
||||
skipStartupTypeset: true
|
||||
tex2jax:
|
||||
processEscapes: true
|
||||
# Dollar delimiters are added by the mathjax directive
|
||||
inlineMath: [ ["\\(","\\)"] ]
|
||||
displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
|
||||
)
|
||||
|
||||
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
|
||||
|
|
21
services/web/public/coffee/directives/mathjax.coffee
Normal file
21
services/web/public/coffee/directives/mathjax.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.directive "mathjax", () ->
|
||||
return {
|
||||
link: (scope, element, attrs) ->
|
||||
if attrs.delimiter != 'no-single-dollar'
|
||||
inlineMathConfig = MathJax?.Hub?.config?.tex2jax.inlineMath
|
||||
alreadyConfigured = _.find inlineMathConfig, (c) ->
|
||||
c[0] == '$' and c[1] == '$'
|
||||
|
||||
if !alreadyConfigured?
|
||||
MathJax?.Hub?.Config(
|
||||
tex2jax:
|
||||
inlineMath: inlineMathConfig.concat([['$', '$']])
|
||||
)
|
||||
|
||||
setTimeout () ->
|
||||
MathJax?.Hub?.Queue(["Typeset", MathJax?.Hub, element.get(0)])
|
||||
, 0
|
||||
}
|
|
@ -136,7 +136,7 @@ define [
|
|||
ide.referencesSearchManager = new ReferencesManager(ide, $scope)
|
||||
ide.connectionManager = new ConnectionManager(ide, $scope)
|
||||
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
||||
ide.editorManager = new EditorManager(ide, $scope)
|
||||
ide.editorManager = new EditorManager(ide, $scope, localStorage)
|
||||
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
|
||||
if window.data.useV2History
|
||||
ide.historyManager = new HistoryV2Manager(ide, $scope)
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
define [
|
||||
"base"
|
||||
"mathjax"
|
||||
], (App) ->
|
||||
mathjaxConfig =
|
||||
messageStyle: "none"
|
||||
imageFont:null
|
||||
"HTML-CSS": { availableFonts: ["TeX"] },
|
||||
TeX:
|
||||
equationNumbers: { autoNumber: "AMS" },
|
||||
useLabelIDs: false
|
||||
tex2jax:
|
||||
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
|
||||
displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
|
||||
processEscapes: true
|
||||
skipStartupTypeset: true
|
||||
|
||||
MathJax?.Hub?.Config(mathjaxConfig);
|
||||
|
||||
App.directive "mathjax", () ->
|
||||
return {
|
||||
link: (scope, element, attrs) ->
|
||||
setTimeout () ->
|
||||
MathJax?.Hub?.Queue(["Typeset", MathJax?.Hub, element.get(0)])
|
||||
, 0
|
||||
}
|
|
@ -2,6 +2,6 @@ define [
|
|||
"ide/chat/controllers/ChatButtonController"
|
||||
"ide/chat/controllers/ChatController"
|
||||
"ide/chat/controllers/ChatMessageController"
|
||||
"ide/chat/directives/mathjax"
|
||||
"directives/mathjax"
|
||||
"filters/wrapLongWords"
|
||||
], () ->
|
|
@ -112,7 +112,8 @@ define [
|
|||
element.layout().resizeAll()
|
||||
|
||||
if attrs.resizeOn?
|
||||
scope.$on attrs.resizeOn, () -> onExternalResize()
|
||||
for event in attrs.resizeOn.split ","
|
||||
scope.$on event, () -> onExternalResize()
|
||||
|
||||
if hasCustomToggler
|
||||
state = element.layout().readState()
|
||||
|
|
|
@ -6,7 +6,7 @@ define [
|
|||
"ide/editor/controllers/SavingNotificationController"
|
||||
], (Document) ->
|
||||
class EditorManager
|
||||
constructor: (@ide, @$scope) ->
|
||||
constructor: (@ide, @$scope, @localStorage) ->
|
||||
@$scope.editor = {
|
||||
sharejs_doc: null
|
||||
open_doc_id: null
|
||||
|
@ -14,7 +14,7 @@ define [
|
|||
opening: true
|
||||
trackChanges: false
|
||||
wantTrackChanges: false
|
||||
showRichText: false
|
||||
showRichText: @showRichText()
|
||||
}
|
||||
|
||||
@$scope.$on "entity:selected", (event, entity) =>
|
||||
|
@ -41,6 +41,11 @@ define [
|
|||
return if !value?
|
||||
@_syncTrackChangesState(@$scope.editor.sharejs_doc)
|
||||
|
||||
showRichText: () ->
|
||||
if !window.richTextEnabled
|
||||
return false
|
||||
@localStorage("editor.mode.#{@$scope.project_id}") == 'rich-text'
|
||||
|
||||
autoOpenDoc: () ->
|
||||
open_doc_id =
|
||||
@ide.localStorage("doc.open_id.#{@$scope.project_id}") or
|
||||
|
|
|
@ -10,13 +10,14 @@ define [
|
|||
"ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter"
|
||||
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
|
||||
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
|
||||
"ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter"
|
||||
"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
|
||||
"ide/editor/directives/aceEditor/metadata/MetadataManager"
|
||||
"ide/metadata/services/metadata"
|
||||
"ide/graphics/services/graphics"
|
||||
"ide/preamble/services/preamble"
|
||||
"ide/files/services/files"
|
||||
], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
|
||||
"ide/files/services/files"
|
||||
], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, CursorPositionAdapter, TrackChangesManager, MetadataManager) ->
|
||||
EditSession = ace.require('ace/edit_session').EditSession
|
||||
ModeList = ace.require('ace/ext/modelist')
|
||||
Vim = ace.require('ace/keyboard/vim').Vim
|
||||
|
@ -108,7 +109,7 @@ define [
|
|||
|
||||
undoManager = new UndoManager(scope, editor, element)
|
||||
highlightsManager = new HighlightsManager(scope, editor, element)
|
||||
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
|
||||
cursorPositionManager = new CursorPositionManager(scope, new CursorPositionAdapter(editor), localStorage)
|
||||
trackChangesManager = new TrackChangesManager(scope, editor, element)
|
||||
metadataManager = new MetadataManager(scope, editor, element, metadata)
|
||||
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
|
||||
|
@ -308,10 +309,12 @@ define [
|
|||
|
||||
scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
|
||||
if old_sharejs_doc?
|
||||
scope.$broadcast('beforeChangeDocument')
|
||||
detachFromAce(old_sharejs_doc)
|
||||
|
||||
if sharejs_doc?
|
||||
attachToAce(sharejs_doc)
|
||||
if sharejs_doc? and old_sharejs_doc?
|
||||
scope.$broadcast('afterChangeDocument')
|
||||
|
||||
scope.$watch "text", (text) ->
|
||||
if text?
|
||||
|
@ -380,6 +383,30 @@ define [
|
|||
editor.off 'changeSession', onSessionChangeForSpellCheck
|
||||
editor.off 'nativecontextmenu', spellCheckManager.onContextMenu
|
||||
|
||||
onSessionChangeForCursorPosition = (e) ->
|
||||
e.oldSession?.selection.off 'changeCursor', cursorPositionManager.onCursorChange
|
||||
e.session.selection.on 'changeCursor', cursorPositionManager.onCursorChange
|
||||
|
||||
onUnloadForCursorPosition = () ->
|
||||
cursorPositionManager.onUnload(editor.getSession())
|
||||
|
||||
initCursorPosition = () ->
|
||||
editor.on 'changeSession', onSessionChangeForCursorPosition
|
||||
onSessionChangeForCursorPosition({ session: editor.getSession() }) # Force initial setup
|
||||
$(window).on "unload", onUnloadForCursorPosition
|
||||
|
||||
tearDownCursorPosition = () ->
|
||||
editor.off 'changeSession', onSessionChangeForCursorPosition
|
||||
$(window).off "unload", onUnloadForCursorPosition
|
||||
|
||||
initCursorPosition()
|
||||
|
||||
# Trigger the event once *only* - this is called after Ace is connected
|
||||
# to the ShareJs instance but this event should only be triggered the
|
||||
# first time the editor is opened. Not every time the docs opened
|
||||
triggerEditorInitEvent = _.once () ->
|
||||
scope.$broadcast('editorInit')
|
||||
|
||||
attachToAce = (sharejs_doc) ->
|
||||
lines = sharejs_doc.getSnapshot().split("\n")
|
||||
session = editor.getSession()
|
||||
|
@ -425,6 +452,7 @@ define [
|
|||
editor.initing = false
|
||||
# now ready to edit document
|
||||
editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false
|
||||
triggerEditorInitEvent()
|
||||
initSpellCheck()
|
||||
|
||||
resetScrollMargins()
|
||||
|
@ -468,6 +496,7 @@ define [
|
|||
editor.focus()
|
||||
|
||||
detachFromAce = (sharejs_doc) ->
|
||||
tearDownSpellCheck()
|
||||
sharejs_doc.detachFromAce()
|
||||
sharejs_doc.off "remoteop.recordRemote"
|
||||
|
||||
|
@ -488,9 +517,11 @@ define [
|
|||
scope.$on '$destroy', () ->
|
||||
if scope.sharejsDoc?
|
||||
tearDownSpellCheck()
|
||||
tearDownCursorPosition()
|
||||
detachFromAce(scope.sharejsDoc)
|
||||
session = editor.getSession()
|
||||
session?.destroy()
|
||||
scope.eventsBridge.emit "aceScrollbarVisibilityChanged", false, 0
|
||||
|
||||
scope.$emit "#{scope.name}:inited", editor
|
||||
|
||||
|
|
|
@ -162,7 +162,8 @@ define [
|
|||
cursorPosition = @editor.getCursorPosition()
|
||||
end = change.end
|
||||
{lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end)
|
||||
if lineUpToCursor.match(/.*%.*/)
|
||||
if ((i = lineUpToCursor.indexOf('%')) > -1 and lineUpToCursor[i-1] != '\\')
|
||||
console.log lineUpToCursor, i
|
||||
return
|
||||
lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
|
||||
lastTwoChars = lineUpToCursor.slice(-2)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
define [
|
||||
"ide/editor/AceShareJsCodec"
|
||||
], (AceShareJsCodec) ->
|
||||
class CursorPositionAdapter
|
||||
constructor: (@editor) ->
|
||||
|
||||
getCursor: () ->
|
||||
@editor.getCursorPosition()
|
||||
|
||||
getEditorScrollPosition: () ->
|
||||
@editor.getFirstVisibleRow()
|
||||
|
||||
setCursor: (pos) ->
|
||||
pos = pos.cursorPosition or { row: 0, column: 0 }
|
||||
@editor.moveCursorToPosition(pos)
|
||||
|
||||
setEditorScrollPosition: (pos) ->
|
||||
pos = pos.firstVisibleLine or 0
|
||||
@editor.scrollToLine(pos)
|
||||
|
||||
clearSelection: () ->
|
||||
@editor.selection.clearSelection()
|
||||
|
||||
gotoLine: (line, column) ->
|
||||
@editor.gotoLine(line, column)
|
||||
@editor.scrollToLine(line, true, true) # centre and animate
|
||||
@editor.focus()
|
||||
|
||||
gotoOffset: (offset) ->
|
||||
lines = @editor.getSession().getDocument().getAllLines()
|
||||
position = AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
|
||||
@gotoLine(position.row + 1, position.column)
|
|
@ -1,75 +1,63 @@
|
|||
define [
|
||||
"ide/editor/AceShareJsCodec"
|
||||
], (AceShareJsCodec) ->
|
||||
define [], () ->
|
||||
class CursorPositionManager
|
||||
constructor: (@$scope, @editor, @element, @localStorage) ->
|
||||
constructor: (@$scope, @adapter, @localStorage) ->
|
||||
@$scope.$on 'editorInit', @jumpToPositionInNewDoc
|
||||
|
||||
onChangeCursor = (e) =>
|
||||
@emitCursorUpdateEvent(e)
|
||||
@$scope.$on 'beforeChangeDocument', () =>
|
||||
@storeCursorPosition()
|
||||
@storeFirstVisibleLine()
|
||||
|
||||
@editor.on "changeSession", (e) =>
|
||||
if e.oldSession?
|
||||
@storeCursorPosition(e.oldSession)
|
||||
@storeScrollTopPosition(e.oldSession)
|
||||
|
||||
@doc_id = @$scope.sharejsDoc?.doc_id
|
||||
|
||||
e.oldSession?.selection.off 'changeCursor', onChangeCursor
|
||||
e.session.selection.on 'changeCursor', onChangeCursor
|
||||
|
||||
setTimeout () =>
|
||||
@gotoStoredPosition()
|
||||
, 0
|
||||
|
||||
$(window).on "unload", () =>
|
||||
@storeCursorPosition(@editor.getSession())
|
||||
@storeScrollTopPosition(@editor.getSession())
|
||||
@$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc
|
||||
|
||||
@$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) =>
|
||||
if line?
|
||||
setTimeout () =>
|
||||
@gotoLine(line, column)
|
||||
@adapter.gotoLine(line, column)
|
||||
, 10 # Hack: Must happen after @gotoStoredPosition
|
||||
|
||||
@$scope.$on "#{@$scope.name}:gotoOffset", (e, offset) =>
|
||||
if offset?
|
||||
setTimeout () =>
|
||||
@gotoOffset(offset)
|
||||
@adapter.gotoOffset(offset)
|
||||
, 10 # Hack: Must happen after @gotoStoredPosition
|
||||
|
||||
@$scope.$on "#{@$scope.name}:clearSelection", (e) =>
|
||||
@editor.selection.clearSelection()
|
||||
@adapter.clearSelection()
|
||||
|
||||
storeScrollTopPosition: (session) ->
|
||||
jumpToPositionInNewDoc: () =>
|
||||
@doc_id = @$scope.sharejsDoc?.doc_id
|
||||
setTimeout () =>
|
||||
@gotoStoredPosition()
|
||||
, 0
|
||||
|
||||
onUnload: () =>
|
||||
@storeCursorPosition()
|
||||
@storeFirstVisibleLine()
|
||||
|
||||
onCursorChange: () =>
|
||||
@emitCursorUpdateEvent()
|
||||
|
||||
onSyncToPdf: () =>
|
||||
@$scope.$emit "cursor:#{@$scope.name}:syncToPdf"
|
||||
|
||||
storeFirstVisibleLine: () ->
|
||||
if @doc_id?
|
||||
docPosition = @localStorage("doc.position.#{@doc_id}") || {}
|
||||
docPosition.scrollTop = session.getScrollTop()
|
||||
docPosition.firstVisibleLine = @adapter.getEditorScrollPosition()
|
||||
@localStorage("doc.position.#{@doc_id}", docPosition)
|
||||
|
||||
storeCursorPosition: (session) ->
|
||||
storeCursorPosition: () ->
|
||||
if @doc_id?
|
||||
docPosition = @localStorage("doc.position.#{@doc_id}") || {}
|
||||
docPosition.cursorPosition = session.selection.getCursor()
|
||||
docPosition.cursorPosition = @adapter.getCursor()
|
||||
@localStorage("doc.position.#{@doc_id}", docPosition)
|
||||
|
||||
|
||||
emitCursorUpdateEvent: () ->
|
||||
cursor = @editor.getCursorPosition()
|
||||
cursor = @adapter.getCursor()
|
||||
@$scope.$emit "cursor:#{@$scope.name}:update", cursor
|
||||
|
||||
gotoStoredPosition: () ->
|
||||
return if !@doc_id?
|
||||
pos = @localStorage("doc.position.#{@doc_id}") || {}
|
||||
@ignoreCursorPositionChanges = true
|
||||
@editor.moveCursorToPosition(pos.cursorPosition or {row: 0, column: 0})
|
||||
@editor.getSession().setScrollTop(pos.scrollTop or 0)
|
||||
delete @ignoreCursorPositionChanges
|
||||
|
||||
gotoLine: (line, column) ->
|
||||
@editor.gotoLine(line, column)
|
||||
@editor.scrollToLine(line,true,true) # centre and animate
|
||||
@editor.focus()
|
||||
|
||||
gotoOffset: (offset) ->
|
||||
lines = @editor.getSession().getDocument().getAllLines()
|
||||
position = AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
|
||||
@gotoLine(position.row + 1, position.column)
|
||||
@adapter.setCursor(pos)
|
||||
@adapter.setEditorScrollPosition(pos)
|
||||
|
|
|
@ -6,9 +6,14 @@ define [
|
|||
"ide/history/controllers/HistoryV2ListController"
|
||||
"ide/history/controllers/HistoryV2DiffController"
|
||||
"ide/history/controllers/HistoryV2FileTreeController"
|
||||
"ide/history/controllers/HistoryV2ToolbarController"
|
||||
"ide/history/controllers/HistoryV2AddLabelModalController"
|
||||
"ide/history/controllers/HistoryV2DeleteLabelModalController"
|
||||
"ide/history/directives/infiniteScroll"
|
||||
"ide/history/components/historyEntriesList"
|
||||
"ide/history/components/historyEntry"
|
||||
"ide/history/components/historyLabelsList"
|
||||
"ide/history/components/historyLabel"
|
||||
"ide/history/components/historyFileTree"
|
||||
"ide/history/components/historyFileEntity"
|
||||
], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
|
||||
|
@ -22,6 +27,9 @@ define [
|
|||
@hide()
|
||||
else
|
||||
@show()
|
||||
@ide.$timeout () =>
|
||||
@$scope.$broadcast "history:toggle"
|
||||
, 0
|
||||
|
||||
@$scope.toggleHistoryViewMode = () =>
|
||||
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
||||
|
@ -30,6 +38,9 @@ define [
|
|||
else
|
||||
@reset()
|
||||
@$scope.history.viewMode = HistoryViewModes.COMPARE
|
||||
@ide.$timeout () =>
|
||||
@$scope.$broadcast "history:toggle"
|
||||
, 0
|
||||
|
||||
@$scope.$watch "history.selection.updates", (updates) =>
|
||||
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
||||
|
@ -44,6 +55,18 @@ define [
|
|||
else
|
||||
@reloadDiff()
|
||||
|
||||
@$scope.$watch "history.showOnlyLabels", (showOnlyLabels, prevVal) =>
|
||||
if showOnlyLabels? and showOnlyLabels != prevVal
|
||||
if showOnlyLabels
|
||||
@selectedLabelFromUpdatesSelection()
|
||||
else
|
||||
@$scope.history.selection.label = null
|
||||
if @$scope.history.selection.updates.length == 0
|
||||
@autoSelectLastUpdate()
|
||||
|
||||
@$scope.$watch "history.updates.length", () =>
|
||||
@recalculateSelectedUpdates()
|
||||
|
||||
show: () ->
|
||||
@$scope.ui.view = "history"
|
||||
@reset()
|
||||
|
@ -60,6 +83,7 @@ define [
|
|||
nextBeforeTimestamp: null
|
||||
atEnd: false
|
||||
selection: {
|
||||
label: null
|
||||
updates: []
|
||||
docs: {}
|
||||
pathname: null
|
||||
|
@ -68,6 +92,9 @@ define [
|
|||
toV: null
|
||||
}
|
||||
}
|
||||
error: null
|
||||
showOnlyLabels: false
|
||||
labels: null
|
||||
files: []
|
||||
diff: null # When history.viewMode == HistoryViewModes.COMPARE
|
||||
selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
|
||||
|
@ -81,10 +108,9 @@ define [
|
|||
_csrf: window.csrfToken
|
||||
})
|
||||
|
||||
loadFileTreeForUpdate: (update) ->
|
||||
{fromV, toV} = update
|
||||
loadFileTreeForVersion: (version) ->
|
||||
url = "/project/#{@$scope.project_id}/filetree/diff"
|
||||
query = [ "from=#{toV}", "to=#{toV}" ]
|
||||
query = [ "from=#{version}", "to=#{version}" ]
|
||||
url += "?" + query.join("&")
|
||||
@$scope.history.loadingFileTree = true
|
||||
@$scope.history.selectedFile = null
|
||||
|
@ -113,6 +139,10 @@ define [
|
|||
return if @$scope.history.updates.length == 0
|
||||
@selectUpdate @$scope.history.updates[0]
|
||||
|
||||
autoSelectLastLabel: () ->
|
||||
return if @$scope.history.labels.length == 0
|
||||
@selectLabel @$scope.history.labels[0]
|
||||
|
||||
selectUpdate: (update) ->
|
||||
selectedUpdateIndex = @$scope.history.updates.indexOf update
|
||||
if selectedUpdateIndex == -1
|
||||
|
@ -122,28 +152,107 @@ define [
|
|||
update.selectedFrom = false
|
||||
@$scope.history.updates[selectedUpdateIndex].selectedTo = true
|
||||
@$scope.history.updates[selectedUpdateIndex].selectedFrom = true
|
||||
@loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex]
|
||||
@recalculateSelectedUpdates()
|
||||
@loadFileTreeForVersion @$scope.history.updates[selectedUpdateIndex].toV
|
||||
|
||||
selectedLabelFromUpdatesSelection: () ->
|
||||
# Get the number of labels associated with the currently selected update
|
||||
nSelectedLabels = @$scope.history.selection.updates?[0]?.labels?.length
|
||||
# If the currently selected update has no labels, select the last one (version-wise)
|
||||
if nSelectedLabels == 0
|
||||
@autoSelectLastLabel()
|
||||
# If the update has one label, select it
|
||||
else if nSelectedLabels == 1
|
||||
@selectLabel @$scope.history.selection.updates[0].labels[0]
|
||||
# If there are multiple labels for the update, select the latest
|
||||
else if nSelectedLabels > 1
|
||||
sortedLabels = @ide.$filter("orderBy")(@$scope.history.selection.updates[0].labels, '-created_at')
|
||||
lastLabelFromUpdate = sortedLabels[0]
|
||||
@selectLabel lastLabelFromUpdate
|
||||
|
||||
selectLabel: (labelToSelect) ->
|
||||
updateToSelect = null
|
||||
|
||||
if @_isLabelSelected labelToSelect
|
||||
# Label already selected
|
||||
return
|
||||
|
||||
for update in @$scope.history.updates
|
||||
if update.toV == labelToSelect.version
|
||||
updateToSelect = update
|
||||
break
|
||||
|
||||
@$scope.history.selection.label = labelToSelect
|
||||
if updateToSelect?
|
||||
@selectUpdate updateToSelect
|
||||
else
|
||||
@$scope.history.selection.updates = []
|
||||
@loadFileTreeForVersion labelToSelect.version
|
||||
|
||||
recalculateSelectedUpdates: () ->
|
||||
beforeSelection = true
|
||||
afterSelection = false
|
||||
@$scope.history.selection.updates = []
|
||||
for update in @$scope.history.updates
|
||||
if update.selectedTo
|
||||
inSelection = true
|
||||
beforeSelection = false
|
||||
|
||||
update.beforeSelection = beforeSelection
|
||||
update.inSelection = inSelection
|
||||
update.afterSelection = afterSelection
|
||||
|
||||
if inSelection
|
||||
@$scope.history.selection.updates.push update
|
||||
|
||||
if update.selectedFrom
|
||||
inSelection = false
|
||||
afterSelection = true
|
||||
|
||||
BATCH_SIZE: 10
|
||||
fetchNextBatchOfUpdates: () ->
|
||||
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
||||
updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
||||
if @$scope.history.nextBeforeTimestamp?
|
||||
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
||||
updatesURL += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
||||
labelsURL = "/project/#{@ide.project_id}/labels"
|
||||
|
||||
@$scope.history.loading = true
|
||||
@$scope.history.loadingFileTree = true
|
||||
@ide.$http
|
||||
.get(url)
|
||||
|
||||
requests =
|
||||
updates: @ide.$http.get updatesURL
|
||||
|
||||
if !@$scope.history.labels?
|
||||
requests.labels = @ide.$http.get labelsURL
|
||||
|
||||
@ide.$q.all requests
|
||||
.then (response) =>
|
||||
{ data } = response
|
||||
@_loadUpdates(data.updates)
|
||||
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
|
||||
if !data.nextBeforeTimestamp?
|
||||
updatesData = response.updates.data
|
||||
if response.labels?
|
||||
@$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data
|
||||
@_loadUpdates(updatesData.updates)
|
||||
@$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp
|
||||
if !updatesData.nextBeforeTimestamp?
|
||||
@$scope.history.atEnd = true
|
||||
@$scope.history.loading = false
|
||||
.catch (error) =>
|
||||
{ status, statusText } = error
|
||||
@$scope.history.error = { status, statusText }
|
||||
@$scope.history.loading = false
|
||||
@$scope.history.loadingFileTree = false
|
||||
|
||||
_sortLabelsByVersionAndDate: (labels) ->
|
||||
@ide.$filter("orderBy")(labels, [ '-version', '-created_at' ])
|
||||
|
||||
loadFileAtPointInTime: () ->
|
||||
pathname = @$scope.history.selection.pathname
|
||||
toV = @$scope.history.selection.updates[0].toV
|
||||
if @$scope.history.selection.updates?[0]?
|
||||
toV = @$scope.history.selection.updates[0].toV
|
||||
else if @$scope.history.selection.label?
|
||||
toV = @$scope.history.selection.label.version
|
||||
|
||||
if !toV?
|
||||
return
|
||||
url = "/project/#{@$scope.project_id}/diff"
|
||||
query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"]
|
||||
url += "?" + query.join("&")
|
||||
|
@ -199,6 +308,32 @@ define [
|
|||
diff.loading = false
|
||||
diff.error = true
|
||||
|
||||
labelCurrentVersion: (labelComment) =>
|
||||
@_labelVersion labelComment, @$scope.history.selection.updates[0].toV
|
||||
|
||||
deleteLabel: (label) =>
|
||||
url = "/project/#{@$scope.project_id}/labels/#{label.id}"
|
||||
|
||||
@ide.$http({
|
||||
url,
|
||||
method: "DELETE"
|
||||
headers:
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}).then (response) =>
|
||||
@_deleteLabelLocally label
|
||||
|
||||
_isLabelSelected: (label) ->
|
||||
label.id == @$scope.history.selection.label?.id
|
||||
|
||||
_deleteLabelLocally: (labelToDelete) ->
|
||||
for update, i in @$scope.history.updates
|
||||
if update.toV == labelToDelete.version
|
||||
update.labels = _.filter update.labels, (label) ->
|
||||
label.id != labelToDelete.id
|
||||
break
|
||||
@$scope.history.labels = _.filter @$scope.history.labels, (label) ->
|
||||
label.id != labelToDelete.id
|
||||
|
||||
_parseDiff: (diff) ->
|
||||
if diff.binary
|
||||
return { binary: true }
|
||||
|
@ -276,7 +411,27 @@ define [
|
|||
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
||||
@autoSelectRecentUpdates()
|
||||
else
|
||||
@autoSelectLastUpdate()
|
||||
if @$scope.history.showOnlyLabels
|
||||
@autoSelectLastLabel()
|
||||
else
|
||||
@autoSelectLastUpdate()
|
||||
|
||||
_labelVersion: (comment, version) ->
|
||||
url = "/project/#{@$scope.project_id}/labels"
|
||||
@ide.$http
|
||||
.post url, {
|
||||
comment,
|
||||
version,
|
||||
_csrf: window.csrfToken
|
||||
}
|
||||
.then (response) =>
|
||||
@_addLabelToLocalUpdate response.data
|
||||
|
||||
_addLabelToLocalUpdate: (label) =>
|
||||
localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version
|
||||
if localUpdate?
|
||||
localUpdate.labels = @_sortLabelsByVersionAndDate localUpdate.labels.concat label
|
||||
@$scope.history.labels = @_sortLabelsByVersionAndDate @$scope.history.labels.concat label
|
||||
|
||||
_perDocSummaryOfUpdates: (updates) ->
|
||||
# Track current_pathname -> original_pathname
|
||||
|
|
|
@ -3,17 +3,35 @@ define [
|
|||
], (App) ->
|
||||
historyEntriesListController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
ctrl.$entryListViewportEl = null
|
||||
_isEntryElVisible = ($entryEl) ->
|
||||
entryElTop = $entryEl.offset().top
|
||||
entryElBottom = entryElTop + $entryEl.outerHeight()
|
||||
entryListViewportElTop = ctrl.$entryListViewportEl.offset().top
|
||||
entryListViewportElBottom = entryListViewportElTop + ctrl.$entryListViewportEl.height()
|
||||
return entryElTop >= entryListViewportElTop and entryElBottom <= entryListViewportElBottom;
|
||||
_getScrollTopPosForEntry = ($entryEl) ->
|
||||
halfViewportElHeight = ctrl.$entryListViewportEl.height() / 2
|
||||
return $entryEl.offset().top - halfViewportElHeight
|
||||
ctrl.onEntryLinked = (entry, $entryEl) ->
|
||||
if entry.selectedTo and entry.selectedFrom and !_isEntryElVisible $entryEl
|
||||
$scope.$applyAsync () ->
|
||||
ctrl.$entryListViewportEl.scrollTop _getScrollTopPosForEntry $entryEl
|
||||
ctrl.$onInit = () ->
|
||||
ctrl.$entryListViewportEl = $element.find "> .history-entries"
|
||||
return
|
||||
|
||||
App.component "historyEntriesList", {
|
||||
bindings:
|
||||
entries: "<"
|
||||
users: "<"
|
||||
loadEntries: "&"
|
||||
loadDisabled: "<"
|
||||
loadInitialize: "<"
|
||||
isLoading: "<"
|
||||
currentUser: "<"
|
||||
onEntrySelect: "&"
|
||||
onLabelDelete: "&"
|
||||
controller: historyEntriesListController
|
||||
templateUrl: "historyEntriesListTpl"
|
||||
}
|
||||
|
|
|
@ -1,27 +1,44 @@
|
|||
define [
|
||||
"base"
|
||||
"ide/colors/ColorManager"
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
historyEntryController = ($scope, $element, $attrs) ->
|
||||
], (App, ColorManager, displayNameForUser) ->
|
||||
historyEntryController = ($scope, $element, $attrs, _) ->
|
||||
ctrl = @
|
||||
# This method (and maybe the one below) will be removed soon. User details data will be
|
||||
# injected into the history API responses, so we won't need to fetch user data from other
|
||||
# local data structures.
|
||||
_getUserById = (id) ->
|
||||
_.find ctrl.users, (user) ->
|
||||
curUserId = user?._id or user?.id
|
||||
curUserId == id
|
||||
ctrl.displayName = displayNameForUser
|
||||
ctrl.displayNameById = (id) ->
|
||||
displayNameForUser(_getUserById(id))
|
||||
ctrl.getProjectOpDoc = (projectOp) ->
|
||||
if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }"
|
||||
else if projectOp.add? then "#{ projectOp.add.pathname}"
|
||||
else if projectOp.remove? then "#{ projectOp.remove.pathname}"
|
||||
ctrl.getUserCSSStyle = (user) ->
|
||||
hue = user?.hue or 100
|
||||
curUserId = user?._id or user?.id
|
||||
hue = ColorManager.getHueForUserId(curUserId) or 100
|
||||
if ctrl.entry.inSelection
|
||||
color : "#FFF"
|
||||
else
|
||||
color: "hsl(#{ hue }, 70%, 50%)"
|
||||
ctrl.$onInit = () ->
|
||||
ctrl.historyEntriesList.onEntryLinked ctrl.entry, $element.find "> .history-entry"
|
||||
return
|
||||
|
||||
App.component "historyEntry", {
|
||||
bindings:
|
||||
entry: "<"
|
||||
currentUser: "<"
|
||||
users: "<"
|
||||
onSelect: "&"
|
||||
onLabelDelete: "&"
|
||||
require:
|
||||
historyEntriesList: '^historyEntriesList'
|
||||
controller: historyEntryController
|
||||
templateUrl: "historyEntryTpl"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
historyLabelController = ($scope, $element, $attrs, $filter, _) ->
|
||||
ctrl = @
|
||||
ctrl.$onInit = () ->
|
||||
ctrl.showTooltip ?= true
|
||||
return
|
||||
|
||||
App.component "historyLabel", {
|
||||
bindings:
|
||||
labelText: "<"
|
||||
labelOwnerName: "<?"
|
||||
labelCreationDateTime: "<?"
|
||||
isOwnedByCurrentUser: "<"
|
||||
onLabelDelete: "&"
|
||||
showTooltip: "<?"
|
||||
controller: historyLabelController
|
||||
templateUrl: "historyLabelTpl"
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
define [
|
||||
"base"
|
||||
"ide/colors/ColorManager"
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, ColorManager, displayNameForUser) ->
|
||||
historyLabelsListController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
# This method (and maybe the one below) will be removed soon. User details data will be
|
||||
# injected into the history API responses, so we won't need to fetch user data from other
|
||||
# local data structures.
|
||||
ctrl.getUserById = (id) ->
|
||||
_.find ctrl.users, (user) ->
|
||||
curUserId = user?._id or user?.id
|
||||
curUserId == id
|
||||
ctrl.displayName = displayNameForUser
|
||||
ctrl.getUserCSSStyle = (user, label) ->
|
||||
curUserId = user?._id or user?.id
|
||||
hue = ColorManager.getHueForUserId(curUserId) or 100
|
||||
if label.id == ctrl.selectedLabel?.id
|
||||
color : "#FFF"
|
||||
else
|
||||
color: "hsl(#{ hue }, 70%, 50%)"
|
||||
return
|
||||
|
||||
App.component "historyLabelsList", {
|
||||
bindings:
|
||||
labels: "<"
|
||||
users: "<"
|
||||
currentUser: "<"
|
||||
isLoading: "<"
|
||||
selectedLabel: "<"
|
||||
onLabelSelect: "&"
|
||||
onLabelDelete: "&"
|
||||
controller: historyLabelsListController
|
||||
templateUrl: "historyLabelsListTpl"
|
||||
}
|
|
@ -3,9 +3,34 @@ define [
|
|||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
|
||||
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
|
||||
App.controller "HistoryListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
|
||||
|
||||
$scope.projectUsers = []
|
||||
|
||||
$scope.$watch "project.members", (newVal) ->
|
||||
if newVal?
|
||||
$scope.projectUsers = newVal.concat $scope.project.owner
|
||||
|
||||
# This method (and maybe the one below) will be removed soon. User details data will be
|
||||
# injected into the history API responses, so we won't need to fetch user data from other
|
||||
# local data structures.
|
||||
_getUserById = (id) ->
|
||||
_.find $scope.projectUsers, (user) ->
|
||||
curUserId = user?._id or user?.id
|
||||
curUserId == id
|
||||
|
||||
$scope.getDisplayNameById = (id) ->
|
||||
displayNameForUser(_getUserById(id))
|
||||
|
||||
$scope.deleteLabel = (labelDetails) ->
|
||||
$modal.open(
|
||||
templateUrl: "historyV2DeleteLabelModalTemplate"
|
||||
controller: "HistoryV2DeleteLabelModalController"
|
||||
resolve:
|
||||
labelDetails: () -> labelDetails
|
||||
)
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
define [
|
||||
"base",
|
||||
], (App) ->
|
||||
App.controller "HistoryV2AddLabelModalController", ["$scope", "$modalInstance", "ide", "update", ($scope, $modalInstance, ide, update) ->
|
||||
$scope.update = update
|
||||
$scope.inputs =
|
||||
labelName: null
|
||||
$scope.state =
|
||||
inflight: false
|
||||
error: false
|
||||
|
||||
$modalInstance.opened.then () ->
|
||||
$scope.$applyAsync () ->
|
||||
$scope.$broadcast "open"
|
||||
|
||||
$scope.addLabelModalFormSubmit = () ->
|
||||
$scope.state.inflight = true
|
||||
ide.historyManager.labelCurrentVersion $scope.inputs.labelName
|
||||
.then (response) ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
.catch (response) ->
|
||||
{ data, status } = response
|
||||
$scope.state.inflight = false
|
||||
if status == 400
|
||||
$scope.state.error = { message: data }
|
||||
else
|
||||
$scope.state.error = true
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
define [
|
||||
"base",
|
||||
], (App) ->
|
||||
App.controller "HistoryV2DeleteLabelModalController", ["$scope", "$modalInstance", "ide", "labelDetails", ($scope, $modalInstance, ide, labelDetails) ->
|
||||
$scope.labelDetails = labelDetails
|
||||
$scope.state =
|
||||
inflight: false
|
||||
error: false
|
||||
|
||||
$scope.deleteLabel = () ->
|
||||
$scope.state.inflight = true
|
||||
ide.historyManager.deleteLabel labelDetails
|
||||
.then (response) ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
.catch (response) ->
|
||||
{ data, status } = response
|
||||
$scope.state.inflight = false
|
||||
if status == 400
|
||||
$scope.state.error = { message: data }
|
||||
else
|
||||
$scope.state.error = true
|
||||
]
|
|
@ -3,74 +3,31 @@ define [
|
|||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
|
||||
App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) ->
|
||||
App.controller "HistoryV2ListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
$scope.listConfig =
|
||||
showOnlyLabelled: false
|
||||
|
||||
$scope.projectUsers = []
|
||||
|
||||
$scope.$watch "project.members", (newVal) ->
|
||||
if newVal?
|
||||
$scope.projectUsers = newVal.concat $scope.project.owner
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
$scope.handleEntrySelect = (entry) ->
|
||||
# $scope.$applyAsync () ->
|
||||
ide.historyManager.selectUpdate(entry)
|
||||
$scope.recalculateSelectedUpdates()
|
||||
|
||||
$scope.recalculateSelectedUpdates = () ->
|
||||
beforeSelection = true
|
||||
afterSelection = false
|
||||
$scope.history.selection.updates = []
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
inSelection = true
|
||||
beforeSelection = false
|
||||
|
||||
update.beforeSelection = beforeSelection
|
||||
update.inSelection = inSelection
|
||||
update.afterSelection = afterSelection
|
||||
|
||||
if inSelection
|
||||
$scope.history.selection.updates.push update
|
||||
|
||||
if update.selectedFrom
|
||||
inSelection = false
|
||||
afterSelection = true
|
||||
|
||||
$scope.recalculateHoveredUpdates = () ->
|
||||
hoverSelectedFrom = false
|
||||
hoverSelectedTo = false
|
||||
for update in $scope.history.updates
|
||||
# Figure out whether the to or from selector is hovered over
|
||||
if update.hoverSelectedFrom
|
||||
hoverSelectedFrom = true
|
||||
if update.hoverSelectedTo
|
||||
hoverSelectedTo = true
|
||||
|
||||
if hoverSelectedFrom
|
||||
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
|
||||
inHoverSelection = false
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
update.hoverSelectedTo = true
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
if update.hoverSelectedFrom
|
||||
inHoverSelection = false
|
||||
if hoverSelectedTo
|
||||
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
|
||||
inHoverSelection = false
|
||||
for update in $scope.history.updates
|
||||
if update.hoverSelectedTo
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
if update.selectedFrom
|
||||
update.hoverSelectedFrom = true
|
||||
inHoverSelection = false
|
||||
|
||||
$scope.resetHoverState = () ->
|
||||
for update in $scope.history.updates
|
||||
delete update.hoverSelectedFrom
|
||||
delete update.hoverSelectedTo
|
||||
delete update.inHoverSelection
|
||||
|
||||
$scope.$watch "history.updates.length", () ->
|
||||
$scope.recalculateSelectedUpdates()
|
||||
$scope.handleLabelSelect = (label) ->
|
||||
ide.historyManager.selectLabel(label)
|
||||
|
||||
$scope.handleLabelDelete = (labelDetails) ->
|
||||
$modal.open(
|
||||
templateUrl: "historyV2DeleteLabelModalTemplate"
|
||||
controller: "HistoryV2DeleteLabelModalController"
|
||||
resolve:
|
||||
labelDetails: () -> labelDetails
|
||||
)
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
define [
|
||||
"base",
|
||||
], (App) ->
|
||||
App.controller "HistoryV2ToolbarController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
|
||||
$scope.showAddLabelDialog = () ->
|
||||
$modal.open(
|
||||
templateUrl: "historyV2AddLabelModalTemplate"
|
||||
controller: "HistoryV2AddLabelModalController"
|
||||
resolve:
|
||||
update: () -> $scope.history.selection.updates[0]
|
||||
)
|
||||
]
|
|
@ -93,7 +93,7 @@ define [
|
|||
# don't want to compile as soon as it is fixed, so reset the timeout.
|
||||
$scope.startedTryingAutoCompileAt = Date.now()
|
||||
$scope.docLastChangedAt = Date.now()
|
||||
if autoCompileLintingError
|
||||
if autoCompileLintingError and $scope.stop_on_validation_error
|
||||
return
|
||||
|
||||
# If there's a longish compile, don't compile immediately after if user is still typing
|
||||
|
@ -224,6 +224,13 @@ define [
|
|||
_csrf: window.csrfToken
|
||||
}, {params: params}
|
||||
|
||||
buildPdfDownloadUrl = (pdfDownloadDomain, path)->
|
||||
#we only download builds from compiles server for security reasons
|
||||
if pdfDownloadDomain? and path.indexOf("build") != -1
|
||||
return "#{pdfDownloadDomain}#{path}"
|
||||
else
|
||||
return path
|
||||
|
||||
parseCompileResponse = (response) ->
|
||||
|
||||
# keep last url
|
||||
|
@ -244,11 +251,7 @@ define [
|
|||
$scope.pdf.compileInProgress = false
|
||||
$scope.pdf.autoCompileDisabled = false
|
||||
|
||||
buildPdfDownloadUrl = (path)->
|
||||
if pdfDownloadDomain?
|
||||
return "#{pdfDownloadDomain}#{path}"
|
||||
else
|
||||
return path
|
||||
|
||||
# make a cache to look up files by name
|
||||
fileByPath = {}
|
||||
if response?.outputFiles?
|
||||
|
@ -267,24 +270,24 @@ define [
|
|||
if response.status == "timedout"
|
||||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.timedout = true
|
||||
fetchLogs(fileByPath)
|
||||
fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
|
||||
else if response.status == "terminated"
|
||||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.compileTerminated = true
|
||||
fetchLogs(fileByPath)
|
||||
fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
|
||||
else if response.status in ["validation-fail", "validation-pass"]
|
||||
$scope.pdf.view = 'pdf'
|
||||
$scope.pdf.url = buildPdfDownloadUrl last_pdf_url
|
||||
$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, last_pdf_url
|
||||
$scope.shouldShowLogs = true
|
||||
$scope.pdf.failedCheck = true if response.status is "validation-fail"
|
||||
event_tracking.sendMB "syntax-check-#{response.status}"
|
||||
fetchLogs(fileByPath, { validation: true })
|
||||
fetchLogs(fileByPath, { validation: true, pdfDownloadDomain:pdfDownloadDomain})
|
||||
else if response.status == "exited"
|
||||
$scope.pdf.view = 'pdf'
|
||||
$scope.pdf.compileExited = true
|
||||
$scope.pdf.url = buildPdfDownloadUrl last_pdf_url
|
||||
$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, last_pdf_url
|
||||
$scope.shouldShowLogs = true
|
||||
fetchLogs(fileByPath)
|
||||
fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
|
||||
else if response.status == "autocompile-backoff"
|
||||
if $scope.pdf.isAutoCompileOnLoad # initial autocompile
|
||||
$scope.pdf.view = 'uncompiled'
|
||||
|
@ -300,7 +303,7 @@ define [
|
|||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.failure = true
|
||||
$scope.shouldShowLogs = true
|
||||
fetchLogs(fileByPath)
|
||||
fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
|
||||
else if response.status == 'clsi-maintenance'
|
||||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.clsiMaintenance = true
|
||||
|
@ -320,12 +323,12 @@ define [
|
|||
|
||||
# define the base url. if the pdf file has a build number, pass it to the clsi in the url
|
||||
if fileByPath['output.pdf']?.url?
|
||||
$scope.pdf.url = buildPdfDownloadUrl fileByPath['output.pdf'].url
|
||||
$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, fileByPath['output.pdf'].url
|
||||
else if fileByPath['output.pdf']?.build?
|
||||
build = fileByPath['output.pdf'].build
|
||||
$scope.pdf.url = buildPdfDownloadUrl "/project/#{$scope.project_id}/build/#{build}/output/output.pdf"
|
||||
$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, "/project/#{$scope.project_id}/build/#{build}/output/output.pdf"
|
||||
else
|
||||
$scope.pdf.url = buildPdfDownloadUrl "/project/#{$scope.project_id}/output/output.pdf"
|
||||
$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, "/project/#{$scope.project_id}/output/output.pdf"
|
||||
# check if we need to bust cache (build id is unique so don't need it in that case)
|
||||
if not fileByPath['output.pdf']?.build?
|
||||
qs.cache_bust = "#{Date.now()}"
|
||||
|
@ -335,7 +338,7 @@ define [
|
|||
qs.popupDownload = true
|
||||
$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
|
||||
|
||||
fetchLogs(fileByPath)
|
||||
fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
|
||||
|
||||
IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
|
||||
$scope.pdf.outputFiles = []
|
||||
|
@ -384,6 +387,7 @@ define [
|
|||
# check if we need to bust cache (build id is unique so don't need it in that case)
|
||||
if not file?.build?
|
||||
opts.params.cache_bust = "#{Date.now()}"
|
||||
opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, opts.url
|
||||
return $http(opts)
|
||||
|
||||
# accumulate the log entries
|
||||
|
@ -718,6 +722,8 @@ define [
|
|||
.then (highlights) ->
|
||||
$scope.pdf.highlights = highlights
|
||||
|
||||
ide.$scope.$on "cursor:editor:syncToPdf", $scope.syncToPdf
|
||||
|
||||
$scope.syncToCode = () ->
|
||||
synctex
|
||||
.syncToCode($scope.pdf.position, includeVisualOffset: true, fromPdfPosition: true)
|
||||
|
|
|
@ -164,7 +164,7 @@ define [
|
|||
updateScrollbar()
|
||||
|
||||
updateScrollbar = () ->
|
||||
if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE
|
||||
if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE and !$scope.editor.showRichText
|
||||
$reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px"
|
||||
else
|
||||
$reviewPanelEl.css "right", "0"
|
||||
|
|
|
@ -3,11 +3,13 @@ define [
|
|||
], (App) ->
|
||||
# We create and provide this as service so that we can access the global ide
|
||||
# from within other parts of the angular app.
|
||||
App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", ($http, queuedHttp, $modal, $q) ->
|
||||
App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", "$filter", "$timeout", ($http, queuedHttp, $modal, $q, $filter, $timeout) ->
|
||||
ide = {}
|
||||
ide.$http = $http
|
||||
ide.queuedHttp = queuedHttp
|
||||
ide.$q = $q
|
||||
ide.$filter = $filter
|
||||
ide.$timeout = $timeout
|
||||
|
||||
@recentEvents = []
|
||||
ide.pushEvent = (type, meta = {}) =>
|
||||
|
|
|
@ -67,6 +67,11 @@ define [
|
|||
if oldCompiler? and compiler != oldCompiler
|
||||
settings.saveProjectSettings({compiler: compiler})
|
||||
|
||||
$scope.$watch "project.imageName", (imageName, oldImageName) =>
|
||||
return if @ignoreUpdates
|
||||
if oldImageName? and imageName != oldImageName
|
||||
settings.saveProjectSettings({imageName: imageName})
|
||||
|
||||
$scope.$watch "project.rootDoc_id", (rootDoc_id, oldRootDoc_id) =>
|
||||
return if @ignoreUpdates
|
||||
# don't save on initialisation, Angular passes oldRootDoc_id as
|
||||
|
@ -83,6 +88,12 @@ define [
|
|||
$scope.project.compiler = compiler
|
||||
delete @ignoreUpdates
|
||||
|
||||
ide.socket.on "imageNameUpdated", (imageName) =>
|
||||
@ignoreUpdates = true
|
||||
$scope.$apply () =>
|
||||
$scope.project.imageName = imageName
|
||||
delete @ignoreUpdates
|
||||
|
||||
ide.socket.on "spellCheckLanguageUpdated", (languageCode) =>
|
||||
@ignoreUpdates = true
|
||||
$scope.$apply () =>
|
||||
|
|
|
@ -20,9 +20,11 @@ define [
|
|||
"main/subscription/team-invite-controller"
|
||||
"main/contact-us"
|
||||
"main/learn"
|
||||
"main/affiliations/components/affiliationForm"
|
||||
"main/affiliations/controllers/UserAffiliationsController"
|
||||
"main/affiliations/factories/UserAffiliationsDataService"
|
||||
"main/keys"
|
||||
"main/cms/index"
|
||||
"analytics/AbTestingManager"
|
||||
"directives/asyncForm"
|
||||
"directives/stopPropagation"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "AccountSettingsController", ["$scope", "$http", "$modal", "event_tracking", ($scope, $http, $modal, event_tracking) ->
|
||||
App.controller "AccountSettingsController", ["$scope", "$http", "$modal", "event_tracking", "UserAffiliationsDataService", ($scope, $http, $modal, event_tracking, UserAffiliationsDataService) ->
|
||||
$scope.subscribed = true
|
||||
|
||||
$scope.unsubscribe = () ->
|
||||
|
@ -21,8 +21,14 @@ define [
|
|||
$scope.deleteAccount = () ->
|
||||
modalInstance = $modal.open(
|
||||
templateUrl: "deleteAccountModalTemplate"
|
||||
controller: "DeleteAccountModalController",
|
||||
scope: $scope
|
||||
controller: "DeleteAccountModalController"
|
||||
resolve:
|
||||
userDefaultEmail: () ->
|
||||
UserAffiliationsDataService
|
||||
.getUserDefaultEmail()
|
||||
.then (defaultEmailDetails) ->
|
||||
return defaultEmailDetails?.email or null
|
||||
.catch () -> null
|
||||
)
|
||||
|
||||
$scope.upgradeIntegration = (service) ->
|
||||
|
@ -30,8 +36,8 @@ define [
|
|||
]
|
||||
|
||||
App.controller "DeleteAccountModalController", [
|
||||
"$scope", "$modalInstance", "$timeout", "$http",
|
||||
($scope, $modalInstance, $timeout, $http) ->
|
||||
"$scope", "$modalInstance", "$timeout", "$http", "userDefaultEmail",
|
||||
($scope, $modalInstance, $timeout, $http, userDefaultEmail) ->
|
||||
$scope.state =
|
||||
isValid : false
|
||||
deleteText: ""
|
||||
|
@ -46,7 +52,7 @@ define [
|
|||
, 700
|
||||
|
||||
$scope.checkValidation = ->
|
||||
$scope.state.isValid = $scope.state.deleteText == $scope.email and $scope.state.password.length > 0
|
||||
$scope.state.isValid = userDefaultEmail? and $scope.state.deleteText == userDefaultEmail and $scope.state.password.length > 0
|
||||
|
||||
$scope.delete = () ->
|
||||
$scope.state.inflight = true
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
affiliationFormController = ($scope, $element, $attrs, UserAffiliationsDataService) ->
|
||||
ctrl = @
|
||||
ctrl.roles = []
|
||||
ctrl.departments = []
|
||||
ctrl.countries = []
|
||||
ctrl.universities = []
|
||||
_defaultDepartments = []
|
||||
|
||||
ctrl.addUniversityToSelection = (universityName) ->
|
||||
{ name: universityName, isUserSuggested: true }
|
||||
# Populates the countries dropdown
|
||||
UserAffiliationsDataService
|
||||
.getCountries()
|
||||
.then (countries) -> ctrl.countries = countries
|
||||
# Populates the roles dropdown
|
||||
UserAffiliationsDataService
|
||||
.getDefaultRoleHints()
|
||||
.then (roles) -> ctrl.roles = roles
|
||||
# Fetches the default department hints
|
||||
UserAffiliationsDataService
|
||||
.getDefaultDepartmentHints()
|
||||
.then (departments) ->
|
||||
_defaultDepartments = departments
|
||||
# Populates the universities dropdown (after selecting a country)
|
||||
$scope.$watch "$ctrl.affiliationData.country", (newSelectedCountry, prevSelectedCountry) ->
|
||||
if newSelectedCountry? and newSelectedCountry != prevSelectedCountry
|
||||
ctrl.affiliationData.university = null
|
||||
ctrl.affiliationData.role = null
|
||||
ctrl.affiliationData.department = null
|
||||
UserAffiliationsDataService
|
||||
.getUniversitiesFromCountry(newSelectedCountry)
|
||||
.then (universities) -> ctrl.universities = universities
|
||||
# Populates the departments dropdown (after selecting a university)
|
||||
$scope.$watch "$ctrl.affiliationData.university", (newSelectedUniversity, prevSelectedUniversity) ->
|
||||
if newSelectedUniversity? and newSelectedUniversity != prevSelectedUniversity and newSelectedUniversity.departments?.length > 0
|
||||
ctrl.departments = _.uniq newSelectedUniversity.departments
|
||||
else
|
||||
ctrl.departments = _defaultDepartments
|
||||
|
||||
return
|
||||
|
||||
App.component "affiliationForm", {
|
||||
bindings:
|
||||
affiliationData: "="
|
||||
showUniversityAndCountry: "<"
|
||||
showRoleAndDepartment: "<"
|
||||
controller: affiliationFormController
|
||||
templateUrl: "affiliationFormTpl"
|
||||
}
|
|
@ -3,12 +3,6 @@ define [
|
|||
], (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_\-\.]+)([^\.])$/
|
||||
|
@ -20,9 +14,6 @@ define [
|
|||
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
|
||||
|
@ -55,11 +46,40 @@ define [
|
|||
$scope.newAffiliation.department = null
|
||||
$scope.ui.showManualUniversitySelectionUI = true
|
||||
|
||||
$scope.changeAffiliation = (userEmail) ->
|
||||
if userEmail.affiliation?.institution?.id?
|
||||
UserAffiliationsDataService.getUniversityDetails userEmail.affiliation.institution.id
|
||||
.then (universityDetails) -> $scope.affiliationToChange.university = universityDetails
|
||||
|
||||
$scope.affiliationToChange.email = userEmail.email
|
||||
$scope.affiliationToChange.role = userEmail.affiliation.role
|
||||
$scope.affiliationToChange.department = userEmail.affiliation.department
|
||||
|
||||
$scope.saveAffiliationChange = (userEmail) ->
|
||||
userEmail.affiliation.role = $scope.affiliationToChange.role
|
||||
userEmail.affiliation.department = $scope.affiliationToChange.department
|
||||
_resetAffiliationToChange()
|
||||
_monitorRequest(
|
||||
UserAffiliationsDataService
|
||||
.addRoleAndDepartment(
|
||||
userEmail.email,
|
||||
userEmail.affiliation.role,
|
||||
userEmail.affiliation.department
|
||||
)
|
||||
)
|
||||
.then () ->
|
||||
setTimeout () -> _getUserEmails()
|
||||
|
||||
$scope.cancelAffiliationChange = (email) ->
|
||||
_resetAffiliationToChange()
|
||||
|
||||
$scope.isChangingAffiliation = (email) ->
|
||||
$scope.affiliationToChange.email == email
|
||||
|
||||
$scope.showAddEmailForm = () ->
|
||||
$scope.ui.showAddEmailUI = true
|
||||
|
||||
$scope.addNewEmail = () ->
|
||||
$scope.ui.isAddingNewEmail = true
|
||||
if !$scope.newAffiliation.university?
|
||||
addEmailPromise = UserAffiliationsDataService
|
||||
.addUserEmail $scope.newAffiliation.email
|
||||
|
@ -81,86 +101,104 @@ define [
|
|||
$scope.newAffiliation.role,
|
||||
$scope.newAffiliation.department
|
||||
)
|
||||
addEmailPromise.then () ->
|
||||
_reset()
|
||||
_getUserEmails()
|
||||
|
||||
$scope.setDefaultUserEmail = (email) ->
|
||||
$scope.ui.isLoadingEmails = true
|
||||
UserAffiliationsDataService
|
||||
.setDefaultUserEmail email
|
||||
.then () -> _getUserEmails()
|
||||
$scope.ui.isAddingNewEmail = true
|
||||
$scope.ui.showAddEmailUI = false
|
||||
_monitorRequest(addEmailPromise)
|
||||
.then () ->
|
||||
_resetNewAffiliation()
|
||||
_resetAddingEmail()
|
||||
setTimeout () -> _getUserEmails()
|
||||
.finally () ->
|
||||
$scope.ui.isAddingNewEmail = false
|
||||
|
||||
$scope.removeUserEmail = (email) ->
|
||||
$scope.ui.isLoadingEmails = true
|
||||
UserAffiliationsDataService
|
||||
.removeUserEmail email
|
||||
.then () -> _getUserEmails()
|
||||
$scope.setDefaultUserEmail = (userEmail) ->
|
||||
_monitorRequest(
|
||||
UserAffiliationsDataService
|
||||
.setDefaultUserEmail userEmail.email
|
||||
)
|
||||
.then () ->
|
||||
for email in $scope.userEmails or []
|
||||
email.default = false
|
||||
userEmail.default = true
|
||||
|
||||
$scope.getDepartments = () ->
|
||||
if $scope.newAffiliation.university?.departments.length > 0
|
||||
_.uniq $scope.newAffiliation.university.departments
|
||||
else
|
||||
UserAffiliationsDataService.getDefaultDepartmentHints()
|
||||
$scope.removeUserEmail = (userEmail) ->
|
||||
$scope.userEmails = $scope.userEmails.filter (ue) -> ue != userEmail
|
||||
_monitorRequest(
|
||||
UserAffiliationsDataService
|
||||
.removeUserEmail userEmail.email
|
||||
)
|
||||
|
||||
_reset = () ->
|
||||
$scope.resendConfirmationEmail = (userEmail) ->
|
||||
$scope.ui.isResendingConfirmation = true
|
||||
_monitorRequest(
|
||||
UserAffiliationsDataService
|
||||
.resendConfirmationEmail userEmail.email
|
||||
)
|
||||
.finally () ->
|
||||
$scope.ui.isResendingConfirmation = false
|
||||
|
||||
$scope.acknowledgeError = () ->
|
||||
_reset()
|
||||
_getUserEmails()
|
||||
|
||||
_resetAffiliationToChange = () ->
|
||||
$scope.affiliationToChange =
|
||||
email: ""
|
||||
university: null
|
||||
role: null
|
||||
department: null
|
||||
|
||||
_resetNewAffiliation = () ->
|
||||
$scope.newAffiliation =
|
||||
email: ""
|
||||
country: null
|
||||
university: null
|
||||
role: null
|
||||
department: null
|
||||
|
||||
_resetAddingEmail = () ->
|
||||
$scope.ui.showAddEmailUI = false
|
||||
$scope.ui.isValidEmail = false
|
||||
$scope.ui.isBlacklistedEmail = false
|
||||
$scope.ui.showManualUniversitySelectionUI = false
|
||||
|
||||
_reset = () ->
|
||||
$scope.ui =
|
||||
showManualUniversitySelectionUI: false
|
||||
hasError: false
|
||||
errorMessage: ""
|
||||
showChangeAffiliationUI: false
|
||||
isMakingRequest: false
|
||||
isLoadingEmails: false
|
||||
isAddingNewEmail: false
|
||||
showAddEmailUI: false
|
||||
isValidEmail: false
|
||||
isBlacklistedEmail: false
|
||||
isResendingConfirmation: false
|
||||
_resetAffiliationToChange()
|
||||
_resetNewAffiliation()
|
||||
_resetAddingEmail()
|
||||
_reset()
|
||||
|
||||
_monitorRequest = (promise) ->
|
||||
$scope.ui.hasError = false
|
||||
$scope.ui.isMakingRequest = true
|
||||
promise
|
||||
.catch (response) ->
|
||||
$scope.ui.hasError = true
|
||||
$scope.ui.errorMessage = response?.data?.message
|
||||
.finally () ->
|
||||
$scope.ui.isMakingRequest = false
|
||||
return promise
|
||||
|
||||
# Populates the emails table
|
||||
_getUserEmails = () ->
|
||||
$scope.ui.isLoadingEmails = true
|
||||
UserAffiliationsDataService
|
||||
.getUserEmails()
|
||||
_monitorRequest(
|
||||
UserAffiliationsDataService
|
||||
.getUserEmails()
|
||||
)
|
||||
.then (emails) ->
|
||||
$scope.userEmails = emails
|
||||
.finally () ->
|
||||
$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
|
||||
|
||||
]
|
|
@ -27,10 +27,14 @@ define [
|
|||
getDefaultDepartmentHints = () ->
|
||||
$q.resolve defaultDepartmentHints
|
||||
|
||||
getUserEmails = () ->
|
||||
getUserEmails = () ->
|
||||
$http.get "/user/emails"
|
||||
.then (response) -> response.data
|
||||
|
||||
getUserDefaultEmail = () ->
|
||||
getUserEmails().then (userEmails) ->
|
||||
_.find userEmails, (userEmail) -> userEmail.default
|
||||
|
||||
getUniversitiesFromCountry = (country) ->
|
||||
if universities[country.code]?
|
||||
universitiesFromCountry = universities[country.code]
|
||||
|
@ -53,6 +57,10 @@ define [
|
|||
else
|
||||
$q.reject null
|
||||
|
||||
getUniversityDetails = (universityId) ->
|
||||
$http.get "/institutions/list/#{ universityId }"
|
||||
.then (response) -> response.data
|
||||
|
||||
addUserEmail = (email) ->
|
||||
$http.post "/user/emails", {
|
||||
email,
|
||||
|
@ -80,8 +88,17 @@ define [
|
|||
_csrf: window.csrfToken
|
||||
}
|
||||
|
||||
addRoleAndDepartment = (email, role, department) ->
|
||||
$http.post "/user/emails/endorse", {
|
||||
email,
|
||||
role,
|
||||
department,
|
||||
_csrf: window.csrfToken
|
||||
}
|
||||
|
||||
setDefaultUserEmail = (email) ->
|
||||
$http.post "/user/emails/default", {
|
||||
email,
|
||||
_csrf: window.csrfToken
|
||||
}
|
||||
|
||||
|
@ -91,6 +108,12 @@ define [
|
|||
_csrf: window.csrfToken
|
||||
}
|
||||
|
||||
resendConfirmationEmail = (email) ->
|
||||
$http.post "/user/emails/resend_confirmation", {
|
||||
email,
|
||||
_csrf: window.csrfToken
|
||||
}
|
||||
|
||||
isDomainBlacklisted = (domain) ->
|
||||
domain.toLowerCase() of domainsBlackList
|
||||
|
||||
|
@ -99,13 +122,17 @@ define [
|
|||
getDefaultRoleHints
|
||||
getDefaultDepartmentHints
|
||||
getUserEmails
|
||||
getUserDefaultEmail
|
||||
getUniversitiesFromCountry
|
||||
getUniversityDomainFromPartialDomainInput
|
||||
getUniversityDetails
|
||||
addUserEmail
|
||||
addUserAffiliationWithUnknownUniversity
|
||||
addUserAffiliation
|
||||
addRoleAndDepartment
|
||||
setDefaultUserEmail
|
||||
removeUserEmail
|
||||
resendConfirmationEmail
|
||||
isDomainBlacklisted
|
||||
}
|
||||
]
|
|
@ -1,5 +1,6 @@
|
|||
define [
|
||||
"base"
|
||||
"directives/mathjax"
|
||||
"services/algolia-search"
|
||||
], (App) ->
|
||||
|
||||
|
@ -55,4 +56,4 @@ define [
|
|||
hits = _.map response.hits, buildHitViewModel
|
||||
updateHits hits
|
||||
|
||||
|
||||
App.controller "LearnController", () ->
|
||||
|
|
|
@ -4,20 +4,21 @@ define [
|
|||
"libs/recurly-4.8.5"
|
||||
], (App)->
|
||||
|
||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
|
||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils, ipCookie)->
|
||||
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
|
||||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
$scope.plans = MultiCurrencyPricing.plans
|
||||
$scope.planCode = window.plan_code
|
||||
$scope.plansVariant = ipCookie('plansVariant')
|
||||
|
||||
$scope.switchToStudent = ()->
|
||||
currentPlanCode = window.plan_code
|
||||
planCode = currentPlanCode.replace('collaborator', 'student')
|
||||
event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code }
|
||||
event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code, variant: $scope.plansVariant }
|
||||
window.location = "/user/subscription/new?planCode=#{planCode}¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
|
||||
|
||||
event_tracking.sendMB "subscription-form", { plan : window.plan_code }
|
||||
event_tracking.sendMB "subscription-form", { plan : window.plan_code, variant: $scope.plansVariant }
|
||||
|
||||
$scope.paymentMethod =
|
||||
value: "credit_card"
|
||||
|
@ -143,13 +144,14 @@ define [
|
|||
currencyCode : postData.subscriptionDetails.currencyCode,
|
||||
plan_code : postData.subscriptionDetails.plan_code,
|
||||
coupon_code : postData.subscriptionDetails.coupon_code,
|
||||
isPaypal : postData.subscriptionDetails.isPaypal
|
||||
isPaypal : postData.subscriptionDetails.isPaypal,
|
||||
variant : $scope.plansVariant
|
||||
}
|
||||
|
||||
|
||||
$http.post("/user/subscription/create", postData)
|
||||
.then ()->
|
||||
event_tracking.sendMB "subscription-submission-success"
|
||||
event_tracking.sendMB "subscription-submission-success", { variant: $scope.plansVariant }
|
||||
window.location.href = "/user/subscription/thank-you"
|
||||
.catch ()->
|
||||
$scope.processing = false
|
||||
|
@ -234,7 +236,4 @@ define [
|
|||
{code:'VU',name:'Vanuatu'},{code:'VA',name:'Vatican City'},{code:'VE',name:'Venezuela'},{code:'VN',name:'Vietnam'},
|
||||
{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
|
||||
{code:'ZM',name:'Zambia'},{code:'AX',name:'Åland Islandscode:'}
|
||||
]
|
||||
|
||||
sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
||||
]
|
|
@ -145,15 +145,21 @@ define [
|
|||
}
|
||||
|
||||
|
||||
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter) ->
|
||||
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter, ipCookie) ->
|
||||
|
||||
$scope.showPlans = false
|
||||
$scope.shouldABTestPlans = window.shouldABTestPlans
|
||||
|
||||
if $scope.shouldABTestPlans
|
||||
sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
||||
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
|
||||
if rawResponse?.status != 'failed'
|
||||
$scope.plansVariant = chosenVariation
|
||||
expiration = new Date();
|
||||
expiration.setDate(expiration.getDate() + 5);
|
||||
ipCookie('plansVariant', chosenVariation, {expires: expiration})
|
||||
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
|
||||
else
|
||||
$scope.timeout = true
|
||||
|
||||
$scope.showPlans = true
|
||||
|
||||
|
@ -184,9 +190,9 @@ define [
|
|||
if $scope.ui.view == "annual"
|
||||
plan = "#{plan}_annual"
|
||||
plan = eventLabel(plan, location)
|
||||
event_tracking.sendMB 'plans-page-start-trial', {plan}
|
||||
event_tracking.sendMB 'plans-page-start-trial', {plan, variant: $scope.plansVariant}
|
||||
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
|
||||
if $scope.shouldABTestPlans
|
||||
if $scope.plansVariant
|
||||
sixpack.convert 'plans-details'
|
||||
|
||||
$scope.switchToMonthly = (e, location) ->
|
||||
|
|
|
@ -101,11 +101,19 @@ define [
|
|||
App.controller 'DeleteProjectsModalController', ($scope, $modalInstance, $timeout, projects) ->
|
||||
$scope.projectsToDelete = projects.filter (project) -> project.accessLevel == "owner"
|
||||
$scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner"
|
||||
$scope.projectsToArchive = projects.filter (project) ->
|
||||
project.accessLevel == "owner" and !project.archived
|
||||
|
||||
if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0
|
||||
$scope.action = "delete-and-leave"
|
||||
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
|
||||
$scope.action = "archive-and-leave"
|
||||
else
|
||||
$scope.action = "delete-and-leave"
|
||||
else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0
|
||||
$scope.action = "delete"
|
||||
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
|
||||
$scope.action = "archive"
|
||||
else
|
||||
$scope.action = "delete"
|
||||
else
|
||||
$scope.action = "leave"
|
||||
|
||||
|
|
BIN
services/web/public/img/review-icon-sprite-ol.png
Normal file
BIN
services/web/public/img/review-icon-sprite-ol.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 765 B |
BIN
services/web/public/img/review-icon-sprite-ol@2x.png
Normal file
BIN
services/web/public/img/review-icon-sprite-ol@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -43,6 +43,8 @@
|
|||
@import "components/hover.less";
|
||||
@import "components/ui-select.less";
|
||||
@import "components/input-suggestions.less";
|
||||
@import "components/nvd3.less";
|
||||
@import "components/nvd3_override.less";
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import "components/modals.less";
|
||||
|
|
|
@ -24,18 +24,36 @@
|
|||
width: 40%;
|
||||
}
|
||||
.affiliations-table-inline-actions {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
}
|
||||
.affiliations-table-inline-action {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.affiliations-table-inline-action-disabled-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
.affiliations-table-highlighted-row {
|
||||
background-color: tint(@content-alt-bg-color, 6%);
|
||||
}
|
||||
|
||||
.affiliations-table-error-row {
|
||||
background-color: @alert-danger-bg;
|
||||
color: @alert-danger-text;
|
||||
.btn {
|
||||
margin-top: @table-cell-padding;
|
||||
.button-variant(@btn-danger-color; darken(@btn-danger-bg, 8%); @btn-danger-border);
|
||||
}
|
||||
}
|
||||
.affiliations-form-group {
|
||||
margin-top: @table-cell-padding;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.affiliation-change-container,
|
||||
.affiliation-change-actions {
|
||||
margin-top: @table-cell-padding;
|
||||
}
|
||||
|
||||
.affiliations-table-label {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
|
|
@ -93,6 +93,8 @@
|
|||
height: @editor-toolbar-height;
|
||||
background-color: @editor-toolbar-bg;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 10; // Prevent track changes showing over toolbar
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
|
|
|
@ -15,16 +15,30 @@
|
|||
.history-toolbar when (@is-overleaf = false) {
|
||||
border-bottom: @toolbar-border-bottom;
|
||||
}
|
||||
.history-toolbar-time {
|
||||
font-weight: bold;
|
||||
.history-toolbar-selected-version {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.history-toolbar-btn {
|
||||
.btn;
|
||||
.btn-info;
|
||||
.btn-xs;
|
||||
padding-left: @padding-small-horizontal;
|
||||
padding-right: @padding-small-horizontal;
|
||||
margin-left: (@line-height-computed / 2);
|
||||
.history-toolbar-time,
|
||||
.history-toolbar-selected-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.history-toolbar-actions {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.history-toolbar-btn {
|
||||
.btn;
|
||||
.btn-info;
|
||||
.btn-xs;
|
||||
padding-left: @padding-small-horizontal;
|
||||
padding-right: @padding-small-horizontal;
|
||||
margin-left: (@line-height-computed / 2);
|
||||
}
|
||||
.history-toolbar-entries-list {
|
||||
flex: 0 0 @changesListWidth;
|
||||
padding: 0 10px;
|
||||
border-left: 1px solid @editor-border-color;
|
||||
}
|
||||
|
||||
.history-entries {
|
||||
|
@ -48,11 +62,76 @@
|
|||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.history-entry-selected & {
|
||||
.history-entry-selected &,
|
||||
.history-entry-label-selected & {
|
||||
background-color: @history-entry-selected-bg;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
.history-label {
|
||||
display: inline-block;
|
||||
color: @history-entry-label-color;
|
||||
font-size: @font-size-small;
|
||||
margin-bottom: 3px;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
.history-entry-selected &,
|
||||
.history-entry-label-selected & {
|
||||
color: @history-entry-selected-label-color;
|
||||
}
|
||||
}
|
||||
.history-label-comment,
|
||||
.history-label-delete-btn {
|
||||
padding: 0 @padding-xs-horizontal 1px @padding-xs-horizontal;
|
||||
border: 0;
|
||||
background-color: @history-entry-label-bg-color;
|
||||
.history-entry-selected &,
|
||||
.history-entry-label-selected & {
|
||||
background-color: @history-entry-selected-label-bg-color;
|
||||
}
|
||||
}
|
||||
.history-label-comment {
|
||||
display: block;
|
||||
float: left;
|
||||
border-radius: 9999px;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.history-label-own & {
|
||||
padding-right: (@padding-xs-horizontal / 2);
|
||||
border-radius: 9999px 0 0 9999px;
|
||||
}
|
||||
}
|
||||
.history-label-delete-btn {
|
||||
padding-left: (@padding-xs-horizontal / 2);
|
||||
padding-right: @padding-xs-horizontal;
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
&:hover {
|
||||
background-color: darken(@history-entry-label-bg-color, 8%);
|
||||
.history-entry-selected &,
|
||||
.history-entry-label-selected & {
|
||||
background-color: darken(@history-entry-selected-label-bg-color, 8%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-label-tooltip {
|
||||
white-space: normal;
|
||||
padding: (@line-height-computed / 4);
|
||||
text-align: left;
|
||||
}
|
||||
.history-label-tooltip-title,
|
||||
.history-label-tooltip-owner,
|
||||
.history-label-tooltip-datetime {
|
||||
margin: 0 0 (@line-height-computed / 4) 0;
|
||||
}
|
||||
.history-label-tooltip-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.history-label-tooltip-datetime {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.history-entry-changes {
|
||||
.list-unstyled;
|
||||
margin-bottom: 3px;
|
||||
|
@ -68,7 +147,8 @@
|
|||
color: @history-highlight-color;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
.history-entry-selected & {
|
||||
.history-entry-selected &,
|
||||
.history-entry-label-selected & {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +173,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.history-labels-list {
|
||||
.history-entries;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.history-entry-label {
|
||||
.history-entry-details;
|
||||
padding: 7px 10px;
|
||||
&.history-entry-label-selected {
|
||||
background-color: @history-entry-selected-bg;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
.history-file-tree-inner {
|
||||
.full-size;
|
||||
overflow-y: auto;
|
||||
|
@ -169,330 +262,3 @@
|
|||
color: @brand-primary;
|
||||
}
|
||||
}
|
||||
// @changesListWidth: 250px;
|
||||
// @changesListPadding: @line-height-computed / 2;
|
||||
|
||||
// @selector-padding-vertical: 10px;
|
||||
// @selector-padding-horizontal: @line-height-computed / 2;
|
||||
// @day-header-height: 24px;
|
||||
|
||||
// @range-bar-color: @link-color;
|
||||
// @range-bar-selected-offset: 14px;
|
||||
|
||||
// #history {
|
||||
// .upgrade-prompt {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// z-index: 100;
|
||||
// background-color: rgba(128,128,128,0.4);
|
||||
// .message {
|
||||
// margin: auto;
|
||||
// margin-top: 100px;
|
||||
// padding: (@line-height-computed / 2) @line-height-computed;
|
||||
// width: 400px;
|
||||
// background-color: white;
|
||||
// border-radius: 8px;
|
||||
// }
|
||||
// .message-wider {
|
||||
// width: 650px;
|
||||
// margin-top: 60px;
|
||||
// padding: 0;
|
||||
// }
|
||||
|
||||
// .message-header {
|
||||
// .modal-header;
|
||||
// }
|
||||
|
||||
// .message-body {
|
||||
// .modal-body;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .diff-panel {
|
||||
// .full-size;
|
||||
// margin-right: @changesListWidth;
|
||||
// }
|
||||
|
||||
// .diff {
|
||||
// .full-size;
|
||||
// .toolbar {
|
||||
// padding: 3px;
|
||||
// .name {
|
||||
// float: left;
|
||||
// padding: 3px @line-height-computed / 4;
|
||||
// display: inline-block;
|
||||
// }
|
||||
// }
|
||||
// .diff-editor {
|
||||
// .full-size;
|
||||
// top: 40px;
|
||||
// }
|
||||
// .hide-ace-cursor {
|
||||
// .ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// .diff-deleted {
|
||||
// padding: @line-height-computed;
|
||||
// }
|
||||
// .deleted-warning {
|
||||
// background-color: @brand-danger;
|
||||
// color: white;
|
||||
// padding: @line-height-computed / 2;
|
||||
// margin-right: @line-height-computed / 4;
|
||||
// }
|
||||
// &-binary {
|
||||
// .alert {
|
||||
// margin: @line-height-computed / 2;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// aside.change-list {
|
||||
// border-left: 1px solid @editor-border-color;
|
||||
// height: 100%;
|
||||
// width: @changesListWidth;
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
|
||||
// .loading {
|
||||
// text-align: center;
|
||||
// font-family: @font-family-serif;
|
||||
// }
|
||||
|
||||
// ul {
|
||||
// li.change {
|
||||
// position: relative;
|
||||
// user-select: none;
|
||||
// -ms-user-select: none;
|
||||
// -moz-user-select: none;
|
||||
// -webkit-user-select: none;
|
||||
|
||||
// .day {
|
||||
// background-color: #fafafa;
|
||||
// border-bottom: 1px solid @editor-border-color;
|
||||
// padding: 4px;
|
||||
// font-weight: bold;
|
||||
// text-align: center;
|
||||
// height: @day-header-height;
|
||||
// font-size: 14px;
|
||||
// line-height: 1;
|
||||
// }
|
||||
// .selectors {
|
||||
// input {
|
||||
// margin: 0;
|
||||
// }
|
||||
// position: absolute;
|
||||
// left: @selector-padding-horizontal;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// width: 24px;
|
||||
// .selector-from {
|
||||
// position: absolute;
|
||||
// bottom: @selector-padding-vertical;
|
||||
// left: 0;
|
||||
// opacity: 0.8;
|
||||
// }
|
||||
// .selector-to {
|
||||
// position: absolute;
|
||||
// top: @selector-padding-vertical;
|
||||
// left: 0;
|
||||
// opacity: 0.8;
|
||||
// }
|
||||
// .range {
|
||||
// position: absolute;
|
||||
// left: 5px;
|
||||
// width: 4px;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// }
|
||||
// }
|
||||
// .description {
|
||||
// padding: (@line-height-computed / 4);
|
||||
// padding-left: 38px;
|
||||
// min-height: 38px;
|
||||
// border-bottom: 1px solid @editor-border-color;
|
||||
// cursor: pointer;
|
||||
// &:hover {
|
||||
// background-color: @gray-lightest;
|
||||
// }
|
||||
// }
|
||||
// .users {
|
||||
// .user {
|
||||
// font-size: 0.8rem;
|
||||
// color: @gray;
|
||||
// text-transform: capitalize;
|
||||
// position: relative;
|
||||
// padding-left: 16px;
|
||||
// .color-square {
|
||||
// height: 12px;
|
||||
// width: 12px;
|
||||
// border-radius: 3px;
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// bottom: 3px;
|
||||
// }
|
||||
// .name {
|
||||
// width: 94%;
|
||||
// white-space: nowrap;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .time {
|
||||
// float: right;
|
||||
// color: @gray;
|
||||
// display: inline-block;
|
||||
// padding-right: (@line-height-computed / 2);
|
||||
// font-size: 0.8rem;
|
||||
// line-height: @line-height-computed;
|
||||
// }
|
||||
// .doc {
|
||||
// font-size: 0.9rem;
|
||||
// font-weight: bold;
|
||||
// }
|
||||
// .action {
|
||||
// color: @gray;
|
||||
// text-transform: uppercase;
|
||||
// font-size: 0.7em;
|
||||
// margin-bottom: -2px;
|
||||
// margin-top: 2px;
|
||||
// &-edited {
|
||||
// margin-top: 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.loading-changes, li.empty-message {
|
||||
// padding: 6px;
|
||||
// cursor: default;
|
||||
// &:hover {
|
||||
// background-color: inherit;
|
||||
// }
|
||||
// }
|
||||
// li.selected {
|
||||
// border-left: 4px solid @range-bar-color;
|
||||
// .day {
|
||||
// padding-left: 0;
|
||||
// }
|
||||
// .description {
|
||||
// padding-left: 34px;
|
||||
// }
|
||||
// .selectors {
|
||||
// left: @selector-padding-horizontal - 4px;
|
||||
// .range {
|
||||
// background-color: @range-bar-color;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-to {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.selected-from {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// bottom: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-from {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.first-in-day {
|
||||
// .selectors {
|
||||
// .selector-to {
|
||||
// top: @day-header-height + @selector-padding-vertical;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.first-in-day.selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @day-header-height + @range-bar-selected-offset;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ul.hover-state {
|
||||
// li {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// background-color: transparent;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.hover-selected {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: 0;
|
||||
// background-color: @gray-light;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.hover-selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-to {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.hover-selected-from {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// bottom: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-from {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.first-in-day.hover-selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @day-header-height + @range-bar-selected-offset;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .diff-deleted {
|
||||
// padding-top: 15px;
|
||||
// }
|
||||
|
||||
// .editor-dark {
|
||||
// #history {
|
||||
// aside.change-list {
|
||||
// border-color: @editor-dark-toolbar-border-color;
|
||||
|
||||
// ul li.change {
|
||||
// .day {
|
||||
// background-color: darken(@editor-dark-background-color, 10%);
|
||||
// border-bottom: 1px solid @editor-dark-toolbar-border-color;
|
||||
// }
|
||||
// .description {
|
||||
// border-bottom: 1px solid @editor-dark-toolbar-border-color;
|
||||
// &:hover {
|
||||
// background-color: black;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
.wl-icon:before{
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn-wrapping{
|
||||
white-space: normal;
|
||||
}
|
||||
.button-as-link{
|
||||
color: green;
|
||||
text-transform: none;
|
||||
|
|
|
@ -139,6 +139,10 @@
|
|||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.rp-unsupported & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
border-bottom: 1px solid @rp-border-grey;
|
||||
background-color: @rp-bg-dim-blue;
|
||||
|
@ -217,6 +221,10 @@
|
|||
.rp-size-mini & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rp-unsupported & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rp-state-current-file & {
|
||||
position: absolute;
|
||||
|
@ -714,6 +722,10 @@
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.rp-unsupported & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rp-state-current-file & {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
@ -834,6 +846,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
.rp-unsupported-msg-wrapper {
|
||||
display: none;
|
||||
.rp-size-expanded.rp-unsupported & {
|
||||
display: block;
|
||||
}
|
||||
|
||||
height: 100%;
|
||||
|
||||
.rp-unsupported-msg {
|
||||
display: flex;
|
||||
width: @review-panel-width - 40px;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
||||
.rp-unsupported-msg-title {
|
||||
font-size: 1.3em;
|
||||
margin-top: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ace-editor-wrapper {
|
||||
.track-changes-marker-callout {
|
||||
border-radius: 0;
|
||||
|
@ -885,7 +922,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.review-icon {
|
||||
.review-icon when (@is-overleaf = false) {
|
||||
display: inline-block;
|
||||
background: url('/img/review-icon-sprite.png') top/30px no-repeat;
|
||||
width: 30px;
|
||||
|
@ -908,10 +945,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.review-icon when (@is-overleaf) {
|
||||
background-position-y: -60px;
|
||||
.toolbar .btn-full-height:hover & {
|
||||
background-position-y: -60px;
|
||||
.review-icon when (@is-overleaf = true) {
|
||||
display: inline-block;
|
||||
background: url('/img/review-icon-sprite-ol.png') top/30px no-repeat;
|
||||
width: 30px;
|
||||
|
||||
&::before {
|
||||
content: '\00a0'; // Non-breakable space. A non-breakable character here makes this icon work like font-awesome.
|
||||
}
|
||||
|
||||
@media (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
|
||||
background-image: url('/img/review-icon-sprite-ol@2x.png');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1003,7 +1047,8 @@
|
|||
.rp-size-mini & {
|
||||
right: @review-off-width;
|
||||
}
|
||||
.rp-size-expanded & {
|
||||
.rp-size-expanded &,
|
||||
.rp-unsupported & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -1053,6 +1098,10 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.rp-unsupported & {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rp-size-expanded & {
|
||||
&::after {
|
||||
content: "\f105";
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
@rt-font-family: 'Source Sans Pro', 'Helvetica', 'Arial', sans-serif;
|
||||
// @rt-font-family-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif;
|
||||
@rt-font-family-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif;
|
||||
@rt-line-padding: 8%;
|
||||
|
||||
.rich-text .CodeMirror {
|
||||
font-family: @rt-font-family;
|
||||
font-family: @rt-font-family-serif;
|
||||
font-size: 1.15em;
|
||||
|
||||
pre {
|
||||
font-family: @rt-font-family;
|
||||
font-family: @rt-font-family-serif;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
// Add horizontal padding, to emulate a manuscript more closely
|
||||
padding: 0 @rt-line-padding;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
// TODO: Change prefix away from wl- ?
|
||||
|
@ -32,44 +41,48 @@
|
|||
|
||||
/****************************************************************************/
|
||||
|
||||
// wl-indent-X is used to add extra left padding to nested itemize/enumerate
|
||||
// environments, so that the inner list appears more indented than the outer
|
||||
.wl-indent-0 {
|
||||
padding-left: 2.5em !important;
|
||||
padding-left: calc(~"2.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-1 {
|
||||
padding-left: 3.5em !important;
|
||||
padding-left: calc(~"3.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-2 {
|
||||
padding-left: 4.5em !important;
|
||||
padding-left: calc(~"4.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-3 {
|
||||
padding-left: 5.5em !important;
|
||||
padding-left: calc(~"5.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-4 {
|
||||
padding-left: 6.5em !important;
|
||||
padding-left: calc(~"6.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
// wl-indent-env-X is used to add extra left padding to empty nested itemize/
|
||||
// enumerate environments
|
||||
.wl-indent-env-0 {
|
||||
padding-left: 4px !important;
|
||||
padding-left: calc(~"4px + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-env-1 {
|
||||
padding-left: 1.5em !important;
|
||||
padding-left: calc(~"1.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-env-2 {
|
||||
padding-left: 2.5em !important;
|
||||
padding-left: calc(~"2.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-env-3 {
|
||||
padding-left: 3.5em !important;
|
||||
padding-left: calc(~"3.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-indent-env-4 {
|
||||
padding-left: 4.5em !important;
|
||||
padding-left: calc(~"4.5em + @{rt-line-padding}") !important;
|
||||
}
|
||||
|
||||
.wl-enumerate-item-open {
|
||||
|
@ -108,7 +121,7 @@
|
|||
.wl-figure-wrap {
|
||||
padding: 10px 0;
|
||||
background-color: #f5f5f5;
|
||||
box-shadow: 2px 2px 2px #DFDFDF;
|
||||
box-shadow: 1.3px 2px 2px #DFDFDF;
|
||||
width: 96%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue