Merge branch 'master' into pr-v2-light-theme

This commit is contained in:
Paulo Reis 2018-08-22 11:19:01 +01:00
commit a15706ce24
148 changed files with 4354 additions and 1220 deletions

View file

@ -6,6 +6,10 @@ pipeline {
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 {
@ -14,6 +18,20 @@ pipeline {
}
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']) {
@ -67,6 +85,8 @@ pipeline {
}
}
stage('Test and Minify') {
parallel {
stage('Unit Test') {
agent {
docker {
@ -79,13 +99,6 @@ pipeline {
}
}
stage('Frontend Unit Test') {
steps {
// Spawns its own docker containers
sh 'make --no-print-directory test_frontend'
}
}
stage('Acceptance Test') {
steps {
// Spawns its own docker containers
@ -104,6 +117,15 @@ pipeline {
sh 'WEBPACK_ENV=production make minify'
}
}
}
}
stage('Frontend Unit Test') {
steps {
// Spawns its own docker containers
sh 'make --no-print-directory test_frontend'
}
}
stage('Package') {
steps {
@ -142,11 +164,32 @@ pipeline {
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}",
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"
}
}
}

View file

@ -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,18 +61,34 @@ 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
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()
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
@ -83,20 +98,26 @@ module.exports = AuthenticationController =
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'})
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

View file

@ -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) ->

View file

@ -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"

View file

@ -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 =

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
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)

View file

@ -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
)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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 = [])->

View file

@ -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

View file

@ -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 = []

View file

@ -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"

View file

@ -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'

View file

@ -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)

View file

@ -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

View file

@ -9,11 +9,16 @@ async = require("async")
module.exports =
addUserToGroup: (req, res)->
addUserToGroup: (req, res, next)->
adminUserId = AuthenticationController.getLoggedInUserId(req)
newEmail = req.body?.email?.toLowerCase()?.trim()
getManagedSubscription adminUserId, (error, subscription) ->
return next(error) if error?
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
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
@ -23,32 +28,37 @@ module.exports =
result.limitReached = true
res.json(result)
removeUserFromGroup: (req, res)->
removeUserFromGroup: (req, res, next)->
adminUserId = AuthenticationController.getLoggedInUserId(req)
userToRemove_id = req.params.user_id
getManagedSubscription adminUserId, (error, subscription) ->
return next(error) if error?
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
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)
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 adminUserId, userToRemove_id, (err)->
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)

View file

@ -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

View file

@ -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

View file

@ -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
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

View file

@ -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)

View file

@ -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")

View file

@ -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) ->

View file

@ -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 =

View file

@ -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)

View file

@ -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) ->

View file

@ -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)

View file

@ -1,4 +1,3 @@
sanitize = require('sanitizer')
User = require("../../models/User").User
UserCreator = require("./UserCreator")
UserGetter = require("./UserGetter")
@ -54,6 +53,7 @@ module.exports = UserRegistrationHandler =
(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
(cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb
(cb)->
if userDetails.subscribeToNewsletter == "true"
NewsLetterManager.subscribe user, ->
cb() #this can be slow, just fire it off
], (err)->

View file

@ -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?
UserGetter.getUserEmail userId, (error, oldEmail) =>
if err?
return callback(error)
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)
if res.n == 0 # TODO: Check n or nMatched?
else if res.n == 0 # TODO: Check n or nMatched?
return callback(new Error('Default email does not belong to user'))
callback()
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'

View file

@ -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()

View file

@ -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}")

View file

@ -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()

View file

@ -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}

View file

@ -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

View 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")
| &nbsp;
| #{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'}

View file

@ -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.

View file

@ -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}",

View file

@ -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
| &nbsp;&nbsp;#{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",

View file

@ -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
| &nbsp;{{ $ctrl.labelText }}
button.history-label-delete-btn(
ng-if="$ctrl.isOwnedByCurrentUser"
stop-propagation="click"
ng-click="$ctrl.onLabelDelete()"
) &times;
script(type="text/ng-template", id="historyLabelTooltipTpl")
.history-label-tooltip
p.history-label-tooltip-title
i.fa.fa-tag
| &nbsp;{{ $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")}' }}

View file

@ -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
@ -172,3 +204,36 @@ script(type="text/ng-template", id="historyEntryTpl")
span.name(
ng-style="$ctrl.getUserCSSStyle();"
) #{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
| &nbsp;&nbsp; #{translate("loading")}...

View file

@ -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
| &nbsp;&nbsp;#{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")}

View file

@ -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
| &nbsp;&nbsp; #{translate("loading")}...
span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}&nbsp;
span.history-toolbar-selected-version(
ng-show="!history.loadingFileTree && !history.showOnlyLabels && !history.error"
) #{translate("browsing_project_as_of")}&nbsp;
time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
.history-toolbar-btn(
ng-click="toggleHistoryViewMode();"
span.history-toolbar-selected-version(
ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error"
) #{translate("browsing_project_labelled")}&nbsp;
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
| &nbsp;#{translate("history_label_this_version")}
button.history-toolbar-btn(
ng-click="toggleHistoryViewMode();"
ng-disabled="history.loadingFileTree"
)
i.fa.fa-exchange
| &nbsp;#{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"
)
i.fa
| #{translate("compare_to_another_version")}

View file

@ -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

View file

@ -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

View file

@ -192,11 +192,14 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate')
ng-click="cancel()"
) &times;
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.
|

View file

@ -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

View file

@ -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,6 +22,7 @@ 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 !hasFeature('affiliations')
if !externalAuthenticationSystemUsed()
.form-group
label(for='email') #{translate("email")}
@ -167,6 +168,11 @@ 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")}

View file

@ -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
div(ng-if="userEmail.affiliation.institution") {{ userEmail.affiliation.institution.name }}
div(ng-if="userEmail.affiliation.role || userEmail.affiliation.department")
| {{ 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")
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 }}
td
a(
href
ng-if="!userEmail.default"
ng-click="setDefaultUserEmail(userEmail.email)"
) Make default
br
a(
href
ng-if="!userEmail.default"
ng-click="removeUserEmail(userEmail.email)"
) Remove
tr.affiliations-table-highlighted-row(
ng-if="ui.isLoadingEmails"
ng-click="changeAffiliation(userEmail);"
) #{translate("change")}
.affiliation-change-container(
ng-if="isChangingAffiliation(userEmail.email)"
)
td.text-center(colspan="3")
i.fa.fa-fw.fa-spin.fa-refresh
| &nbsp;Loading...
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")}
| &nbsp;#{translate("save_or_cancel-or" )}&nbsp;
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")}
| &nbsp;
button.btn.btn-sm.btn-danger.affiliations-table-inline-action(
ng-if="!userEmail.default"
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.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,65 +113,102 @@ form.row(
input-required="true"
)
td
.affiliations-table-label(
p.affiliations-table-label(
ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI"
)
| {{ newAffiliation.university.name }} (
| {{ newAffiliation.university.name }}
span.small
| (
a(
href
ng-click="selectUniversityManually();"
) change
) #{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
) #{translate("let_us_know")}
affiliation-form(
affiliation-data="newAffiliation"
show-university-and-country="ui.showManualUniversitySelectionUI"
show-role-and-department="ui.isValidEmail && newAffiliation.university"
)
td
button.btn.btn-sm.btn-primary(
ng-disabled="affiliationsForm.$invalid || ui.isMakingRequest"
ng-click="addNewEmail()"
)
| #{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
| &nbsp;#{translate("loading")}...
td.text-center(colspan="3", ng-if="ui.isResendingConfirmation")
i.fa.fa-fw.fa-spin.fa-refresh
| &nbsp;#{translate("sending")}...
td.text-center(colspan="3", ng-if="!ui.isLoadingEmails && !ui.isResendingConfirmation")
i.fa.fa-fw.fa-spin.fa-refresh
| &nbsp;#{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") &nbsp;#{translate("error_performing_request")}
span(ng-if="ui.errorMessage") &nbsp;{{ui.errorMessage}}
hr
script(type="text/ng-template", id="affiliationFormTpl")
.affiliations-form-group(
ng-if="ui.showManualUniversitySelectionUI"
ng-if="$ctrl.showUniversityAndCountry"
)
ui-select(
ng-model="newAffiliation.country"
ng-model="$ctrl.affiliationData.country"
)
ui-select-match(
placeholder="Country"
) {{ $select.selected.name }}
ui-select-choices(
repeat="country in countries | filter: $select.search"
repeat="country in $ctrl.countries | filter: $select.search"
)
span(
ng-bind="country.name"
s)
)
.affiliations-form-group(
ng-if="ui.showManualUniversitySelectionUI"
ng-if="$ctrl.showUniversityAndCountry"
)
ui-select(
ng-model="newAffiliation.university"
ng-disabled="!newAffiliation.country"
tagging="addUniversityToSelection"
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 universities | filter: $select.search"
repeat="university in $ctrl.universities | filter: $select.search"
)
span(
ng-bind="university.name"
)
.affiliations-form-group(
ng-if="ui.isValidEmail && newAffiliation.university"
ng-if="$ctrl.showRoleAndDepartment"
)
ui-select(
ng-model="newAffiliation.role"
ng-model="$ctrl.affiliationData.role"
tagging
tagging-label="false"
)
@ -133,17 +216,17 @@ form.row(
placeholder="Role"
) {{ $select.selected }}
ui-select-choices(
repeat="role in roles | filter: $select.search"
repeat="role in $ctrl.roles | filter: $select.search"
)
span(
ng-bind="role"
)
.affiliations-form-group(
ng-if="ui.isValidEmail && newAffiliation.university"
ng-if="$ctrl.showRoleAndDepartment"
)
ui-select(
ng-model="newAffiliation.department"
ng-model="$ctrl.affiliationData.department"
tagging
tagging-label="false"
)
@ -151,22 +234,8 @@ form.row(
placeholder="Department"
) {{ $select.selected }}
ui-select-choices(
repeat="department in departments | filter: $select.search"
repeat="department in $ctrl.departments | filter: $select.search"
)
span(
ng-bind="department"
)
td
button.btn.btn-primary(
ng-disabled="affiliationsForm.$invalid || ui.isAddingNewEmail"
ng-click="addNewEmail()"
)
span(
ng-if="!ui.isAddingNewEmail"
) Add new email
span(
ng-if="ui.isAddingNewEmail"
)
i.fa.fa-fw.fa-spin.fa-refresh
| &nbsp;Adding...
hr

View file

@ -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'}
# ]

View file

@ -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

View file

@ -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",

View file

@ -27,6 +27,28 @@ define [
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

View 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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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"
], () ->

View file

@ -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()

View file

@ -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

View file

@ -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) ->
], (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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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
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 }
@ -275,9 +410,29 @@ define [
if firstLoad
if @$scope.history.viewMode == HistoryViewModes.COMPARE
@autoSelectRecentUpdates()
else
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
# create bare object for use as Map

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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()

View file

@ -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
]

View file

@ -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
]

View file

@ -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
$scope.handleLabelSelect = (label) ->
ide.historyManager.selectLabel(label)
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.handleLabelDelete = (labelDetails) ->
$modal.open(
templateUrl: "historyV2DeleteLabelModalTemplate"
controller: "HistoryV2DeleteLabelModalController"
resolve:
labelDetails: () -> labelDetails
)
]

View file

@ -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]
)
]

View file

@ -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)

View file

@ -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"

View file

@ -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 = {}) =>

View file

@ -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 () =>

View file

@ -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"

View file

@ -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

View file

@ -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"
}

View file

@ -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 () ->
$scope.ui.isAddingNewEmail = true
$scope.ui.showAddEmailUI = false
_monitorRequest(addEmailPromise)
.then () ->
_resetNewAffiliation()
_resetAddingEmail()
setTimeout () -> _getUserEmails()
.finally () ->
$scope.ui.isAddingNewEmail = false
$scope.setDefaultUserEmail = (userEmail) ->
_monitorRequest(
UserAffiliationsDataService
.setDefaultUserEmail userEmail.email
)
.then () ->
for email in $scope.userEmails or []
email.default = false
userEmail.default = true
$scope.removeUserEmail = (userEmail) ->
$scope.userEmails = $scope.userEmails.filter (ue) -> ue != userEmail
_monitorRequest(
UserAffiliationsDataService
.removeUserEmail userEmail.email
)
$scope.resendConfirmationEmail = (userEmail) ->
$scope.ui.isResendingConfirmation = true
_monitorRequest(
UserAffiliationsDataService
.resendConfirmationEmail userEmail.email
)
.finally () ->
$scope.ui.isResendingConfirmation = false
$scope.acknowledgeError = () ->
_reset()
_getUserEmails()
$scope.setDefaultUserEmail = (email) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.setDefaultUserEmail email
.then () -> _getUserEmails()
_resetAffiliationToChange = () ->
$scope.affiliationToChange =
email: ""
university: null
role: null
department: null
$scope.removeUserEmail = (email) ->
$scope.ui.isLoadingEmails = true
UserAffiliationsDataService
.removeUserEmail email
.then () -> _getUserEmails()
$scope.getDepartments = () ->
if $scope.newAffiliation.university?.departments.length > 0
_.uniq $scope.newAffiliation.university.departments
else
UserAffiliationsDataService.getDefaultDepartmentHints()
_reset = () ->
_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
_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
]

View file

@ -31,6 +31,10 @@ define [
$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
}
]

View file

@ -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", () ->

View file

@ -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}&currency=#{$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
@ -235,6 +237,3 @@ define [
{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:'&angst;land Islandscode:'}
]
sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
$scope.plansVariant = chosenVariation

View file

@ -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)->
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) ->

View file

@ -101,10 +101,18 @@ 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
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
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
$scope.action = "archive"
else
$scope.action = "delete"
else
$scope.action = "leave"

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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";

View file

@ -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;
}

View file

@ -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 {

View file

@ -15,9 +15,18 @@
.history-toolbar when (@is-overleaf = false) {
border-bottom: @toolbar-border-bottom;
}
.history-toolbar-time {
.history-toolbar-selected-version {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-toolbar-time,
.history-toolbar-selected-label {
font-weight: bold;
}
.history-toolbar-actions {
flex-grow: 1;
}
.history-toolbar-btn {
.btn;
.btn-info;
@ -26,6 +35,11 @@
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 {
font-size: @history-base-font-size;
@ -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;
// }
// }
// }
// }
// }
// }

View file

@ -23,6 +23,9 @@
.wl-icon:before{
font-size: 14px;
}
.btn-wrapping{
white-space: normal;
}
.button-as-link{
color: green;
text-transform: none;

View file

@ -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;
@ -218,6 +222,10 @@
display: block;
}
.rp-unsupported & {
display: none;
}
.rp-state-current-file & {
position: absolute;
top: 0;
@ -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";

View file

@ -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