Merge branch 'master' into ns-use-regex-test

This commit is contained in:
Nate Stemen
2018-08-27 14:26:51 -04:00
92 changed files with 2713 additions and 704 deletions

View File

@@ -1,6 +1,7 @@
AnalyticsManager = require "./AnalyticsManager"
Errors = require "../Errors/Errors"
AuthenticationController = require("../Authentication/AuthenticationController")
InstitutionsAPI = require("../Institutions/InstitutionsAPI")
GeoIpLookup = require '../../infrastructure/GeoIpLookup'
module.exports = AnalyticsController =
@@ -23,6 +24,14 @@ module.exports = AnalyticsController =
AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) ->
respondWith(error, res, next)
licences: (req, res, next) ->
{resource_id, start_date, end_date, lag} = req.query
InstitutionsAPI.getInstitutionLicences resource_id, start_date, end_date, lag, (error, licences) ->
if error?
next(error)
else
res.send licences
respondWith = (error, res, next) ->
if error instanceof Errors.ServiceNotConfiguredError
# ignore, no-op

View File

@@ -23,4 +23,4 @@ module.exports =
publicApiRouter.use '/analytics/uniExternalCollaboration',
AuthenticationController.httpAuth,
AnalyticsProxy.call('/uniExternalCollaboration')
AnalyticsProxy.call('/uniExternalCollaboration')

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")
@@ -64,7 +63,10 @@ module.exports = AuthenticationController =
if user # `user` is either a user object or false
AuthenticationController.finishLogin(user, req, res, next)
else
res.json message: info
if info.redir?
res.json {redir: info.redir}
else
res.json message: info
)(req, res, next)
finishLogin: (user, req, res, next) ->
@@ -81,20 +83,30 @@ module.exports = AuthenticationController =
doPassportLogin: (req, username, password, done) ->
email = username.toLowerCase()
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
return done(err) if err?
if !isAllowed
logger.log email:email, "too many login requests"
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
AuthenticationManager.authenticate email: email, password, (error, user) ->
return done(error) if error?
if user?
# async actions
return done(null, user)
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
Modules = require "../../infrastructure/Modules"
Modules.hooks.fire 'preDoPassportLogin', req, email, (err, infoList) ->
return next(err) if err?
info = infoList.find((i) => i?)
if info?
return done(null, false, info)
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
return done(err) if err?
if !isAllowed
logger.log email:email, "too many login requests"
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
AuthenticationManager.authenticate email: email, password, (error, user) ->
return done(error) if error?
if user?
# async actions
return done(null, user)
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
return done(
null,
false,
{text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}
)
_loginAsyncHandlers: (req, user) ->
UserHandler.setupLoginData(user, ()->)

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,6 +112,7 @@ 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

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

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

@@ -9,15 +9,22 @@ module.exports = InstitutionsAPI =
method: 'GET'
path: "/api/v2/institutions/#{institutionId.toString()}/affiliations"
defaultErrorMessage: "Couldn't get institution affiliations"
}, callback
}, (error, body) -> callback(error, body or [])
getInstitutionLicences: (institutionId, startDate, endDate, lag, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/institutions/#{institutionId.toString()}/institution_licences"
body: {start_date: startDate, end_date: endDate, lag}
defaultErrorMessage: "Couldn't get institution licences"
}, callback
getUserAffiliations: (userId, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/users/#{userId.toString()}/affiliations"
defaultErrorMessage: "Couldn't get user affiliations"
}, callback
}, (error, body) -> callback(error, body or [])
addAffiliation: (userId, email, affiliationOptions, callback) ->
@@ -25,11 +32,11 @@ module.exports = InstitutionsAPI =
callback = affiliationOptions
affiliationOptions = {}
{ university, department, role } = affiliationOptions
{ university, department, role, confirmedAt } = affiliationOptions
makeAffiliationRequest {
method: 'POST'
path: "/api/v2/users/#{userId.toString()}/affiliations"
body: { email, university, department, role }
body: { email, university, department, role, confirmedAt }
defaultErrorMessage: "Couldn't create affiliation"
}, callback
@@ -80,6 +87,8 @@ 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)

View File

@@ -1,4 +1,4 @@
UserGetter = require '../User/UserGetter'
InstitutionsGetter = require './InstitutionsGetter'
PlansLocator = require '../Subscription/PlansLocator'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
@@ -13,11 +13,10 @@ module.exports = InstitutionsFeatures =
hasLicence: (userId, callback = (error, hasLicence) ->) ->
UserGetter.getUserFullEmails userId, (error, emailsData) ->
InstitutionsGetter.getConfirmedInstitutions userId, (error, institutions) ->
return callback error if error?
affiliation = emailsData.find (emailData) ->
licence = emailData.affiliation?.institution?.licence
emailData.confirmedAt? and licence? and licence != 'free'
hasLicence = institutions.some (institution) ->
institution.licence and institution.licence != 'free'
callback(null, !!affiliation)
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

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

@@ -49,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
@@ -347,7 +351,7 @@ module.exports = ProjectController =
useV2History: !!project.overleaf?.history?.display
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

@@ -6,15 +6,18 @@ metrics = require('metrics-sharelatex')
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

@@ -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,7 +53,8 @@ module.exports = UserRegistrationHandler =
(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
(cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb
(cb)->
NewsLetterManager.subscribe user, ->
if userDetails.subscribeToNewsletter == "true"
NewsLetterManager.subscribe user, ->
cb() #this can be slow, just fire it off
], (err)->
logger.log user: user, "registered"

View File

@@ -11,6 +11,7 @@ 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) ->) ->
@@ -99,15 +100,21 @@ module.exports = UserUpdater =
setDefaultEmailAddress: (userId, email, callback) ->
email = EmailHelper.parseEmail(email)
return callback(new Error('invalid email')) if !email?
query = _id: userId, 'emails.email': email
update = $set: email: email
@updateUser query, update, (error, res) ->
if error?
logger.err error:error, 'problem setting default emails'
UserGetter.getUserEmail userId, (error, oldEmail) =>
if err?
return callback(error)
if res.n == 0 # TODO: Check n or nMatched?
return callback(new Error('Default email does not belong to user'))
callback()
query = _id: userId, 'emails.email': email
update = $set: email: email
@updateUser query, update, (error, res) ->
if error?
logger.err error:error, 'problem setting default emails'
return callback(error)
else if res.n == 0 # TODO: Check n or nMatched?
return callback(new Error('Default email does not belong to user'))
else
NewsletterManager.changeEmail oldEmail, email, callback
updateV1AndSetDefaultEmailAddress: (userId, email, callback) ->
@updateEmailAddressInV1 userId, email, (error) =>
@@ -152,11 +159,14 @@ module.exports = UserUpdater =
else
return callback new Error("non-success code from v1: #{response.statusCode}")
confirmEmail: (userId, email, callback) ->
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'
addAffiliation userId, email, (error) =>
addAffiliation userId, email, {confirmedAt: confirmedAt}, (error) =>
if error?
logger.err error: error, 'problem adding affiliation while confirming email'
return callback(error)
@@ -166,7 +176,7 @@ 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"

View File

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

@@ -25,5 +25,7 @@ module.exports = Features =
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

@@ -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")
@@ -245,6 +245,7 @@ module.exports = class Router
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,21 @@
mixin faq_search(headerText, headerClass)
- if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined")
if headerText
div(class=headerClass) #{headerText}
.wiki(ng-controller="SearchWikiController")
form.project-search.form-horizontal(role="form")
.form-group.has-feedback.has-feedback-left
.col-sm-12
input.form-control(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....")
i.fa.fa-search.form-control-feedback-left
i.fa.fa-times.form-control-feedback(
ng-click="clearSearchText()",
style="cursor: pointer;",
ng-show="searchQueryText.length > 0"
)
.row
.col-md-12(ng-cloak)
a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin
span(ng-bind-html='hit.name')
div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content')

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

@@ -0,0 +1,74 @@
mixin paginate(pages, page_path, max_btns)
//- @param pages.current_page the current page viewed
//- @param pages.total_pages previously calculated,
//- based on total entries and entries per page
//- @param page_path the relative path, minus a trailing slash and page param
//- @param max_btns max number of buttons on either side of the current page
//- button and excludes first, prev, next, last
if pages && pages.current_page && pages.total_pages
- var max_btns = max_btns || 4
- var prev_page = Math.max(parseInt(pages.current_page, 10) - max_btns, 1)
- var next_page = parseInt(pages.current_page, 10) + 1
- var next_index = 0;
- var full_page_path = page_path + "/page/"
nav(role="navigation" aria-label="Pagination Navigation")
ul.pagination
if pages.current_page > 1
li
a(
aria-label="Go to first page"
href=page_path
) « First
li
a(
aria-label="Go to previous page"
href=full_page_path + (parseInt(pages.current_page, 10) - 1)
rel="prev"
) Prev
if pages.current_page - max_btns > 1
li
span …
while prev_page < pages.current_page
li
a(
aria-label="Go to page " + prev_page
href=full_page_path + prev_page
) #{prev_page}
- prev_page++
li(class="active")
span(
aria-label="Current Page, Page " + pages.current_page
aria-current="true"
) #{pages.current_page}
if pages.current_page < pages.total_pages
while next_page <= pages.total_pages && next_index < max_btns
li
a(
aria-label="Go to page " + next_page
href=full_page_path + next_page
) #{next_page}
- next_page++
- next_index++
if next_page <= pages.total_pages
li
span …
li
a(
aria-label="Go to next page"
href=full_page_path + (parseInt(pages.current_page, 10) + 1)
rel="next"
) Next
li
a(
aria-label="Go to last page"
href=full_page_path + pages.total_pages
) Last »

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"

View File

@@ -1,45 +1,4 @@
div#history(ng-show="ui.view == 'history'")
span
.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
.message(ng-if="project.owner._id == user.id")
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
) #{translate("start_free_trial")}
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
p
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
include ./history/entriesListV1
include ./history/entriesListV2
@@ -67,3 +26,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,28 @@ aside.change-list(
ng-controller="HistoryV2ListController"
)
history-entries-list(
ng-if="!history.showOnlyLabels && !history.error"
entries="history.updates"
current-user="user"
current-user-is-owner="project.owner._id === user.id"
users="projectUsers"
load-entries="loadMore()"
load-disabled="history.loading || history.atEnd"
load-initialize="ui.view == 'history'"
is-loading="history.loading"
free-history-limit-hit="history.freeHistoryLimitHit"
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 +80,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,12 +129,55 @@ 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
| &nbsp;&nbsp; #{translate("loading")}...
.history-entries-list-upgrade-prompt(
ng-if="$ctrl.freeHistoryLimitHit && $ctrl.currentUserIsOwner"
ng-controller="FreeTrialModalController"
)
p #{translate("currently_seeing_only_24_hrs_history")}
p: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
) #{translate("start_free_trial")}
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
.history-entries-list-upgrade-prompt(
ng-if="$ctrl.freeHistoryLimitHit && !$ctrl.currentUserIsOwner"
)
p #{translate("currently_seeing_only_24_hrs_history")}
strong #{translate("ask_proj_owner_to_upgrade_for_full_history")}
script(type="text/ng-template", id="historyEntryTpl")
.history-entry(
@@ -129,6 +195,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 +223,7 @@ script(type="text/ng-template", id="historyEntryTpl")
ng-if="::project_op.remove"
) #{translate("file_action_deleted")}
span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
.history-entry-metadata
time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
span
@@ -171,4 +247,37 @@ script(type="text/ng-template", id="historyEntryTpl")
li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0")
span.name(
ng-style="$ctrl.getUserCSSStyle();"
) #{translate("anonymous")}
) #{translate("anonymous")}
script(type="text/ng-template", id="historyLabelsListTpl")
.history-labels-list
.history-entry-label(
ng-repeat="label in $ctrl.labels track by label.id"
ng-click="$ctrl.onLabelSelect({ label: label })"
ng-class="{ 'history-entry-label-selected': label.id === $ctrl.selectedLabel.id }"
)
history-label(
show-tooltip="false"
label-text="label.comment"
is-owned-by-current-user="label.user_id === $ctrl.currentUser.id"
on-label-delete="$ctrl.onLabelDelete({ label: label })"
)
.history-entry-label-metadata
.history-entry-label-metadata-user(ng-init="user = $ctrl.getUserById(label.user_id)")
| Saved by
span.name(
ng-if="user && user._id !== $ctrl.currentUser.id"
ng-style="$ctrl.getUserCSSStyle(user, label);"
) {{ ::$ctrl.displayName(user) }}
span.name(
ng-if="user && user._id == $ctrl.currentUser.id"
ng-style="$ctrl.getUserCSSStyle(user, label);"
) You
span.name(
ng-if="user == null"
ng-style="$ctrl.getUserCSSStyle(user, label);"
) #{translate("anonymous")}
time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }}
.loading(ng-show="$ctrl.isLoading")
i.fa.fa-spin.fa-refresh
| &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.selection.updates.length && !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();"
)
i.fa
| #{translate("compare_to_another_version")}
span.history-toolbar-selected-version(
ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error"
) #{translate("browsing_project_labelled")}&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 || history.selection.updates.length == 0"
)
i.fa.fa-tag
| &nbsp;#{translate("history_label_this_version")}
button.history-toolbar-btn(
ng-click="toggleHistoryViewMode();"
ng-disabled="history.loadingFileTree || history.selection.updates.length == 0"
)
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"
)

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

@@ -305,7 +305,7 @@ div.full-size.pdf(ng-controller="PdfController")
dbl-click-callback="syncToCode"
)
iframe(
ng-src="{{ pdf.url }}"
ng-src="{{ pdf.url | trusted }}"
ng-if="settings.pdfViewer == 'native'"
)

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 && hasFeature('affiliations')
if hasFeature('affiliations')
include settings/user-affiliations
form-messages(for="settingsForm")
@@ -22,7 +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 !(locals.showAffiliationsUI && hasFeature('affiliations'))
if !hasFeature('affiliations')
if !externalAuthenticationSystemUsed()
.form-group
label(for='email') #{translate("email")}
@@ -167,7 +167,12 @@ block content
)
i.fa.fa-check
| #{translate("unsubscribed")}
if !settings.overleaf && user.overleaf
p
| Please note: If you have linked your account with Overleaf
| v2, then deleting your ShareLaTeX account will also delete
| account and all of it's associated projects and data.
p #{translate("need_to_leave")}
a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}

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

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

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

@@ -162,7 +162,7 @@ define [
cursorPosition = @editor.getCursorPosition()
end = change.end
{lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end)
if /.*((?![\\]).|^)%.*/.test(lineUpToCursor)
if ((i = lineUpToCursor.indexOf('%')) > -1 and lineUpToCursor[i-1] != '\\')
return
lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
lastTwoChars = lineUpToCursor.slice(-2)

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()
@@ -59,7 +82,10 @@ define [
viewMode: null
nextBeforeTimestamp: null
atEnd: false
userHasFullFeature: @$scope.project?.features?.versioning or false
freeHistoryLimitHit: false
selection: {
label: null
updates: []
docs: {}
pathname: null
@@ -68,6 +94,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 +110,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 +141,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 +154,109 @@ 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? or @$scope.history.freeHistoryLimitHit
@$scope.history.atEnd = true
@$scope.history.loading = false
if @$scope.history.updates.length == 0
@$scope.history.loadingFileTree = false
.catch (error) =>
{ status, statusText } = error
@$scope.history.error = { status, statusText }
@$scope.history.loading = false
@$scope.history.loadingFileTree = false
_sortLabelsByVersionAndDate: (labels) ->
@ide.$filter("orderBy")(labels, [ '-version', '-created_at' ])
loadFileAtPointInTime: () ->
pathname = @$scope.history.selection.pathname
toV = @$scope.history.selection.updates[0].toV
if @$scope.history.selection.updates?[0]?
toV = @$scope.history.selection.updates[0].toV
else if @$scope.history.selection.label?
toV = @$scope.history.selection.label.version
if !toV?
return
url = "/project/#{@$scope.project_id}/diff"
query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"]
url += "?" + query.join("&")
@@ -199,6 +312,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 }
@@ -252,23 +391,34 @@ define [
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates or []
dateTimeNow = new Date()
timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
cutOffIndex = null
for update, i in updates or []
for user in update.meta.users or []
if user?
user.hue = ColorManager.getHueForUserId(user.id)
if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
update.meta.first_in_day = true
update.selectedFrom = false
update.selectedTo = false
update.inSelection = false
previousUpdate = update
if !@$scope.history.userHasFullFeature and update.meta.end_ts < timestamp24hoursAgo
cutOffIndex = i or 1 # Make sure that we show at least one entry (to allow labelling).
@$scope.history.freeHistoryLimitHit = true
break
firstLoad = @$scope.history.updates.length == 0
if !@$scope.history.userHasFullFeature and cutOffIndex?
updates = updates.slice 0, cutOffIndex
@$scope.history.updates =
@$scope.history.updates.concat(updates)
@@ -276,7 +426,27 @@ define [
if @$scope.history.viewMode == HistoryViewModes.COMPARE
@autoSelectRecentUpdates()
else
@autoSelectLastUpdate()
if @$scope.history.showOnlyLabels
@autoSelectLastLabel()
else
@autoSelectLastUpdate()
_labelVersion: (comment, version) ->
url = "/project/#{@$scope.project_id}/labels"
@ide.$http
.post url, {
comment,
version,
_csrf: window.csrfToken
}
.then (response) =>
@_addLabelToLocalUpdate response.data
_addLabelToLocalUpdate: (label) =>
localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version
if localUpdate?
localUpdate.labels = @_sortLabelsByVersionAndDate localUpdate.labels.concat label
@$scope.history.labels = @_sortLabelsByVersionAndDate @$scope.history.labels.concat label
_perDocSummaryOfUpdates: (updates) ->
# Track current_pathname -> original_pathname

View File

@@ -3,17 +3,37 @@ 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: "<"
freeHistoryLimitHit: "<"
currentUserIsOwner: "<"
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
update.beforeSelection = beforeSelection
update.inSelection = inSelection
update.afterSelection = afterSelection
if inSelection
$scope.history.selection.updates.push update
if update.selectedFrom
inSelection = false
afterSelection = true
$scope.recalculateHoveredUpdates = () ->
hoverSelectedFrom = false
hoverSelectedTo = false
for update in $scope.history.updates
# Figure out whether the to or from selector is hovered over
if update.hoverSelectedFrom
hoverSelectedFrom = true
if update.hoverSelectedTo
hoverSelectedTo = true
if hoverSelectedFrom
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
inHoverSelection = false
for update in $scope.history.updates
if update.selectedTo
update.hoverSelectedTo = true
inHoverSelection = true
update.inHoverSelection = inHoverSelection
if update.hoverSelectedFrom
inHoverSelection = false
if hoverSelectedTo
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
inHoverSelection = false
for update in $scope.history.updates
if update.hoverSelectedTo
inHoverSelection = true
update.inHoverSelection = inHoverSelection
if update.selectedFrom
update.hoverSelectedFrom = true
inHoverSelection = false
$scope.resetHoverState = () ->
for update in $scope.history.updates
delete update.hoverSelectedFrom
delete update.hoverSelectedTo
delete update.inHoverSelection
$scope.$watch "history.updates.length", () ->
$scope.recalculateSelectedUpdates()
$scope.handleLabelSelect = (label) ->
ide.historyManager.selectLabel(label)
$scope.handleLabelDelete = (labelDetails) ->
$modal.open(
templateUrl: "historyV2DeleteLabelModalTemplate"
controller: "HistoryV2DeleteLabelModalController"
resolve:
labelDetails: () -> labelDetails
)
]

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

@@ -12,6 +12,10 @@ define [
# and then again on ack.
AUTO_COMPILE_DEBOUNCE = 2000
App.filter('trusted', ['$sce', ($sce)->
return (url)-> return $sce.trustAsResourceUrl(url);
])
App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) ->
# enable per-user containers by default
perUserCompile = true
@@ -224,6 +228,13 @@ define [
_csrf: window.csrfToken
}, {params: params}
buildPdfDownloadUrl = (pdfDownloadDomain, path)->
#we only download builds from compiles server for security reasons
if pdfDownloadDomain? and path? and path.indexOf("build") != -1
return "#{pdfDownloadDomain}#{path}"
else
return path
parseCompileResponse = (response) ->
# keep last url
@@ -244,11 +255,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 +274,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 +307,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 +327,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 +342,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 +391,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

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

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

@@ -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]
@@ -118,6 +122,7 @@ define [
getDefaultRoleHints
getDefaultDepartmentHints
getUserEmails
getUserDefaultEmail
getUniversitiesFromCountry
getUniversityDomainFromPartialDomainInput
getUniversityDetails

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
@@ -234,7 +236,4 @@ define [
{code:'VU',name:'Vanuatu'},{code:'VA',name:'Vatican City'},{code:'VE',name:'Venezuela'},{code:'VN',name:'Vietnam'},
{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
{code:'ZM',name:'Zambia'},{code:'AX',name:'&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)->
$scope.plansVariant = chosenVariation
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
if rawResponse?.status != 'failed'
$scope.plansVariant = chosenVariation
expiration = new Date();
expiration.setDate(expiration.getDate() + 5);
ipCookie('plansVariant', chosenVariation, {expires: expiration})
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
else
$scope.timeout = true
$scope.showPlans = true
@@ -184,9 +190,9 @@ define [
if $scope.ui.view == "annual"
plan = "#{plan}_annual"
plan = eventLabel(plan, location)
event_tracking.sendMB 'plans-page-start-trial', {plan}
event_tracking.sendMB 'plans-page-start-trial', {plan, variant: $scope.plansVariant}
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
if $scope.shouldABTestPlans
if $scope.plansVariant
sixpack.convert 'plans-details'
$scope.switchToMonthly = (e, location) ->

View File

@@ -101,11 +101,19 @@ define [
App.controller 'DeleteProjectsModalController', ($scope, $modalInstance, $timeout, projects) ->
$scope.projectsToDelete = projects.filter (project) -> project.accessLevel == "owner"
$scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner"
$scope.projectsToArchive = projects.filter (project) ->
project.accessLevel == "owner" and !project.archived
if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0
$scope.action = "delete-and-leave"
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
$scope.action = "archive-and-leave"
else
$scope.action = "delete-and-leave"
else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0
$scope.action = "delete"
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
$scope.action = "archive"
else
$scope.action = "delete"
else
$scope.action = "leave"

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

@@ -15,16 +15,31 @@
.history-toolbar when (@is-overleaf = false) {
border-bottom: @toolbar-border-bottom;
}
.history-toolbar-time {
font-weight: bold;
.history-toolbar-selected-version {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: (@line-height-computed / 2);
}
.history-toolbar-btn {
.btn;
.btn-info;
.btn-xs;
padding-left: @padding-small-horizontal;
padding-right: @padding-small-horizontal;
margin-left: (@line-height-computed / 2);
.history-toolbar-time,
.history-toolbar-selected-label {
font-weight: bold;
}
.history-toolbar-actions {
flex-grow: 1;
}
.history-toolbar-btn {
.btn;
.btn-info;
.btn-xs;
padding-left: @padding-small-horizontal;
padding-right: @padding-small-horizontal;
margin-right: (@line-height-computed / 2);
}
.history-toolbar-entries-list {
flex: 0 0 @changesListWidth;
padding: 0 10px;
border-left: 1px solid @editor-border-color;
}
.history-entries {
@@ -48,11 +63,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 +148,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 +174,25 @@
}
}
.history-entries-list-upgrade-prompt {
background-color: #FFF;
margin-bottom: 2px;
padding: 5px 10px;
}
.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 +269,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

@@ -922,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;
@@ -945,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');
}
}

View File

@@ -194,56 +194,57 @@
}
.toggle-switch {
position: relative;
position: relative;
height: 100%;
width: 100%;
background-color: @toggle-switch-bg;
border-radius: @btn-border-radius-base;
background-color: @toggle-switch-bg;
border-radius: @btn-border-radius-base;
}
.toggle-switch-label {
position: relative;
display: block;
font-weight: normal;
z-index: 2;
float: left;
width: 50%;
height: 100%;
line-height: 24px;
z-index: 2;
float: left;
width: 50%;
height: 100%;
line-height: 24px;
text-align: center;
margin-bottom: 0;
cursor: pointer;
user-select: none;
transition: color 0.12s ease-out;
cursor: pointer;
user-select: none;
color: @text-color;
transition: color 0.12s ease-out;
}
.toggle-switch-input {
position: absolute;
opacity: 0;
position: absolute;
opacity: 0;
}
.toggle-switch-input:checked + .toggle-switch-label {
color: #fff;
font-weight: bold;
color: #fff;
font-weight: bold;
}
.toggle-switch-selection {
display: block;
position: absolute;
z-index: 1;
top: 2px;
left: 2px;
right: 2px;
width: calc(~"50% - 2px");
height: calc(~"100% - 4px");
background: @toggle-switch-highlight-color;
border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base;
transition: transform 0.12s ease-out, border-radius 0.12s ease-out;
display: block;
position: absolute;
z-index: 1;
top: 2px;
left: 2px;
right: 2px;
width: calc(~"50% - 2px");
height: calc(~"100% - 4px");
background: @toggle-switch-highlight-color;
border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base;
transition: transform 0.12s ease-out, border-radius 0.12s ease-out;
}
.toggle-switch-input:checked:nth-child(4) ~ .toggle-switch-selection {
transform: translate(100%);
border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
transform: translate(100%);
border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
}
/**************************************

View File

@@ -87,6 +87,11 @@
margin-top: 0;
margin-bottom: 0;
}
.metric-tooltip {
top: -1em;
font-size: .5em;
}
}
// END: Metrics header

View File

@@ -0,0 +1,161 @@
.content-portal {
padding-top: @navbar-height!important;
/*
Begin Header
*/
.banner-image {
background-size: cover;
background-position: 50% 50%;
background-repeat: no-repeat;
height: 375px;
}
.image-fill {
display: inline-block;
height: 100%;
vertical-align: middle;
}
.institution-logo {
left: 50%;
margin-left: -100px;
padding: 0;
position: absolute;
div {
background-color: @white;
box-shadow: 1px 11px 22px -9px @black-alpha-strong;
display: inline-block;
height: 125px;
overflow: hidden;
position: absolute;
text-align: center;
top: -110px;
white-space: nowrap;
width: @btn-portal-width;
}
img {
max-height: 75px;
max-width: 150px;
vertical-align: middle;
}
}
.portal-name {
background-color: @ol-blue-gray-0;
padding-bottom: @line-height-computed; //- center header when no tabs
padding-top: @padding-md;
text-align: center;
width: 100%;
}
// End Header
/*
Begin Layout
*/
.button-pull,
.content-pull {
float: left;
}
.button-pull {
text-align: right;
> a.btn {
white-space: normal;
width: @btn-portal-width;
text-align: center;
}
}
.content-pull {
padding-right: @padding-sm;
width: calc(~"100% - "@btn-portal-width);
}
// End Layout
/*
Begin Card
*/
.card {
margin-bottom: @margin-md;
}
// End Card
/*
Begin Actions
*/
.portal-actions {
i {
margin-bottom: @margin-sm;
}
}
// End Actions
/*
Begin Print
*/
.print {
.hidden-print {
display: none;
}
}
// End Print
/*
Begin Tabs
*/
.nav-tabs {
// Overrides for nav.less
background-color: @ol-blue-gray-0;
border: 0!important;
margin-bottom: @margin-md;
margin-top: -@line-height-computed; //- adjusted for portal-name
padding: @padding-lg 0 @padding-md;
text-align: center;
a {
color: @link-color;
&:hover {
background-color: transparent!important;
border: 0!important;
color: @link-hover-color!important;
}
}
li {
display: inline-block;
float: none;
a {
border: 0;
}
}
li.active > a {
background-color: transparent!important;
border: 0;
border-bottom: 1px solid @accent-color-secondary!important;
color: @accent-color-secondary;
&:hover {
color: @accent-color-secondary!important;
}
}
}
.tab-content:extend(.container) {
background-color: transparent!important;
border: none!important;
}
// End Tabs
@media (max-width: @screen-size-sm-max) {
.content-pull {
padding: 0;
width: auto;
}
.button-pull {
> a.btn {
width: auto;
}
}
}
}

View File

@@ -34,7 +34,7 @@
@daterangepicker-in-range-bg-color: #ebf4f8;
@daterangepicker-active-color: #fff;
@daterangepicker-active-bg-color: #a93529;
@daterangepicker-active-bg-color: #4F9C45;
@daterangepicker-active-border-color: transparent;
@daterangepicker-unselected-color: #999;
@@ -393,13 +393,14 @@
.daterangepicker_input {
position: relative;
padding-left: 0;
i {
position: absolute;
// NOTE: These appear to be eyeballed to me...
left: 8px;
top: 8px;
top: 10px;
}
}
&.rtl {
@@ -476,6 +477,19 @@
/* Larger Screen Styling */
@media (min-width: 564px) {
.daterangepicker {
.glyphicon {
font-family: FontAwesome;
}
.glyphicon-chevron-left:before{
content: "\f053";
}
.glyphicon-chevron-right:before{
content: "\f054";
}
.glyphicon-calendar:before{
content: "\f073";
}
width: auto;
.ranges {

View File

@@ -0,0 +1,26 @@
.embed-responsive {
display: block;
height: 0;
overflow: hidden;
padding: 0;
position: relative;
}
.embed-responsive .embed-responsive-item,
.embed-responsive iframe,
.embed-responsive embed,
.embed-responsive object,
.embed-responsive video {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
.embed-responsive-16by9 {
padding-bottom: 56.25% !important;
}
.embed-responsive-4by3 {
padding-bottom: 75% !important;
}

View File

@@ -0,0 +1,9 @@
// Colors
.icon-accent {
color: @accent-color-secondary;
}
// Sizes
.icon-lg {
font-size: @font-size-h1;
}

View File

@@ -0,0 +1,674 @@
/* nvd3 version 1.8.4 (https://github.com/novus/nvd3) 2016-07-03 */
.nvd3 .nv-axis {
pointer-events:none;
opacity: 1;
}
.nvd3 .nv-axis path {
fill: none;
stroke: #000;
stroke-opacity: .75;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis path.domain {
stroke-opacity: .75;
}
.nvd3 .nv-axis.nv-x path.domain {
stroke-opacity: 0;
}
.nvd3 .nv-axis line {
fill: none;
stroke: #e5e5e5;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis .zero line,
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
stroke-opacity: .75;
}
.nvd3 .nv-axis .nv-axisMaxMin text {
font-weight: bold;
}
.nvd3 .x .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
text-anchor: middle
}
.nvd3 .nv-axis.nv-disabled {
opacity: 0;
}
.nvd3 .nv-bars rect {
fill-opacity: .75;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-bars rect.hover {
fill-opacity: 1;
}
.nvd3 .nv-bars .hover rect {
fill: lightblue;
}
.nvd3 .nv-bars text {
fill: rgba(0,0,0,0);
}
.nvd3 .nv-bars .hover text {
fill: rgba(0,0,0,1);
}
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
stroke-opacity: 0;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-candlestickBar .nv-ticks rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
font-weight: bold;
fill: rgba(0,0,0,1);
stroke: rgba(0,0,0,0);
}
/* boxplot CSS */
.nvd3 .nv-boxplot circle {
fill-opacity: 0.5;
}
.nvd3 .nv-boxplot circle:hover {
fill-opacity: 1;
}
.nvd3 .nv-boxplot rect:hover {
fill-opacity: 1;
}
.nvd3 line.nv-boxplot-median {
stroke: black;
}
.nv-boxplot-tick:hover {
stroke-width: 2.5px;
}
/* bullet */
.nvd3.nv-bullet { font: 10px sans-serif; }
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-markerLine { stroke: #000; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
.nvd3.nv-bullet .nv-range {
fill: #bababa;
fill-opacity: .4;
}
.nvd3.nv-bullet .nv-range:hover {
fill-opacity: .7;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick {
stroke-width: 1px;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover {
stroke-width: 2px;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect {
stroke: #d62728;
fill: #d62728;
}
.with-transitions .nv-candlestickBar .nv-ticks .nv-tick {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-candlestickBar .nv-ticks line {
stroke: #333;
}
.nv-force-node {
stroke: #fff;
stroke-width: 1.5px;
}
.nv-force-link {
stroke: #999;
stroke-opacity: .6;
}
.nv-force-node text {
stroke-width: 0px
}
.nvd3 .nv-legend .nv-disabled rect {
/*fill-opacity: 0;*/
}
.nvd3 .nv-check-box .nv-box {
fill-opacity:0;
stroke-width:2;
}
.nvd3 .nv-check-box .nv-check {
fill-opacity:0;
stroke-width:4;
}
.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check {
fill-opacity:0;
stroke-opacity:0;
}
.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check {
opacity: 0;
}
/* line plus bar */
.nvd3.nv-linePlusBar .nv-bar rect {
fill-opacity: .75;
}
.nvd3.nv-linePlusBar .nv-bar rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-groups path.nv-line {
fill: none;
}
.nvd3 .nv-groups path.nv-area {
stroke: none;
}
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.with-transitions .nvd3 .nv-groups .nv-point {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-scatter .nv-groups .nv-point.hover,
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 7px;
fill-opacity: .95 !important;
stroke-opacity: .95 !important;
}
.nvd3 .nv-point-paths path {
stroke: #aaa;
stroke-opacity: 0;
fill: #eee;
fill-opacity: 0;
}
.nvd3 .nv-indexLine {
cursor: ew-resize;
}
/********************
* SVG CSS
*/
/********************
Default CSS for an svg element nvd3 used
*/
svg.nvd3-svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-ms-user-select: none;
-moz-user-select: none;
user-select: none;
display: block;
width:100%;
height:100%;
}
/********************
Box shadow and border radius styling
*/
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.nvd3 text {
font: normal 12px Arial;
}
.nvd3 .title {
font: bold 14px Arial;
}
.nvd3 .nv-background {
fill: white;
fill-opacity: 0;
}
.nvd3.nv-noData {
font-size: 18px;
font-weight: bold;
}
/**********
* Brush
*/
.nv-brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
.nv-brush .resize path {
fill: #eee;
stroke: #666;
}
/**********
* Legend
*/
.nvd3 .nv-legend .nv-series {
cursor: pointer;
}
.nvd3 .nv-legend .nv-disabled circle {
fill-opacity: 0;
}
/* focus */
.nvd3 .nv-brush .extent {
fill-opacity: 0 !important;
}
.nvd3 .nv-brushBackground rect {
stroke: #000;
stroke-width: .4;
fill: #fff;
fill-opacity: .7;
}
/**********
* Print
*/
@media print {
.nvd3 text {
stroke-width: 0;
fill-opacity: 1;
}
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
stroke-width: 1px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
stroke-width: 2px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
stroke: #2ca02c;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
stroke: #d62728;
}
.nvd3 .background path {
fill: none;
stroke: #EEE;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.nvd3 .foreground path {
fill: none;
stroke-opacity: .7;
}
.nvd3 .nv-parallelCoordinates-brush .extent
{
fill: #fff;
fill-opacity: .6;
stroke: gray;
shape-rendering: crispEdges;
}
.nvd3 .nv-parallelCoordinates .hover {
fill-opacity: 1;
stroke-width: 3px;
}
.nvd3 .missingValuesline line {
fill: none;
stroke: black;
stroke-width: 1;
stroke-opacity: 1;
stroke-dasharray: 5, 5;
}
.nvd3.nv-pie path {
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-pie .nv-pie-title {
font-size: 24px;
fill: rgba(19, 196, 249, 0.59);
}
.nvd3.nv-pie .nv-slice text {
stroke: #000;
stroke-width: 0;
}
.nvd3.nv-pie path {
stroke: #fff;
stroke-width: 1px;
stroke-opacity: 1;
}
.nvd3.nv-pie path {
fill-opacity: .7;
}
.nvd3.nv-pie .hover path {
fill-opacity: 1;
}
.nvd3.nv-pie .nv-label {
pointer-events: none;
}
.nvd3.nv-pie .nv-label rect {
fill-opacity: 0;
stroke-opacity: 0;
}
/* scatter */
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .5;
}
.nvd3 .nv-scatter .nv-point.hover {
fill-opacity: 1;
}
.nv-noninteractive {
pointer-events: none;
}
.nv-distx, .nv-disty {
pointer-events: none;
}
/* sparkline */
.nvd3.nv-sparkline path {
fill: none;
}
.nvd3.nv-sparklineplus g.nv-hoverValue {
pointer-events: none;
}
.nvd3.nv-sparklineplus .nv-hoverValue line {
stroke: #333;
stroke-width: 1.5px;
}
.nvd3.nv-sparklineplus,
.nvd3.nv-sparklineplus g {
pointer-events: all;
}
.nvd3 .nv-hoverArea {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-sparklineplus .nv-xValue,
.nvd3.nv-sparklineplus .nv-yValue {
stroke-width: 0;
font-size: .9em;
font-weight: normal;
}
.nvd3.nv-sparklineplus .nv-yValue {
stroke: #f66;
}
.nvd3.nv-sparklineplus .nv-maxValue {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-sparklineplus .nv-minValue {
stroke: #d62728;
fill: #d62728;
}
.nvd3.nv-sparklineplus .nv-currentValue {
font-weight: bold;
font-size: 1.1em;
}
/* stacked area */
.nvd3.nv-stackedarea path.nv-area {
fill-opacity: .7;
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-stackedarea path.nv-area.hover {
fill-opacity: .9;
}
.nvd3.nv-stackedarea .nv-groups .nv-point {
stroke-opacity: 0;
fill-opacity: 0;
}
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1.0);
color: rgba(0,0,0,1.0);
padding: 1px;
border: 1px solid rgba(0,0,0,.2);
z-index: 10000;
display: block;
font-family: Arial;
font-size: 13px;
text-align: left;
pointer-events: none;
white-space: nowrap;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.nvtooltip {
background: rgba(255,255,255, 0.8);
border: 1px solid rgba(0,0,0,0.5);
border-radius: 4px;
}
/*Give tooltips that old fade in transition by
putting a "with-transitions" class on the container div.
*/
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
transition: opacity 50ms linear;
-moz-transition: opacity 50ms linear;
-webkit-transition: opacity 50ms linear;
transition-delay: 200ms;
-moz-transition-delay: 200ms;
-webkit-transition-delay: 200ms;
}
.nvtooltip.x-nvtooltip,
.nvtooltip.y-nvtooltip {
padding: 8px;
}
.nvtooltip h3 {
margin: 0;
padding: 4px 14px;
line-height: 18px;
font-weight: normal;
background-color: rgba(247,247,247,0.75);
color: rgba(0,0,0,1.0);
text-align: center;
border-bottom: 1px solid #ebebeb;
-webkit-border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
border-radius: 5px 5px 0 0;
}
.nvtooltip p {
margin: 0;
padding: 5px 14px;
text-align: center;
}
.nvtooltip span {
display: inline-block;
margin: 2px 0;
}
.nvtooltip table {
margin: 6px;
border-spacing:0;
}
.nvtooltip table td {
padding: 2px 9px 2px 0;
vertical-align: middle;
}
.nvtooltip table td.key {
font-weight: normal;
}
.nvtooltip table td.key.total {
font-weight: bold;
}
.nvtooltip table td.value {
text-align: right;
font-weight: bold;
}
.nvtooltip table td.percent {
color: darkgray;
}
.nvtooltip table tr.highlight td {
padding: 1px 9px 1px 0;
border-bottom-style: solid;
border-bottom-width: 1px;
border-top-style: solid;
border-top-width: 1px;
}
.nvtooltip table td.legend-color-guide div {
width: 8px;
height: 8px;
vertical-align: middle;
}
.nvtooltip table td.legend-color-guide div {
width: 12px;
height: 12px;
border: 1px solid #999;
}
.nvtooltip .footer {
padding: 3px;
text-align: center;
}
.nvtooltip-pending-removal {
pointer-events: none;
display: none;
}
/****
Interactive Layer
*/
.nvd3 .nv-interactiveGuideLine {
pointer-events:none;
}
.nvd3 line.nv-guideline {
stroke: #ccc;
}

View File

@@ -0,0 +1,16 @@
.nvd3 {
.nv-axis {
.tick {
line {
opacity: 0;
}
}
path.domain {
opacity: 0;
}
}
}
svg.nvd3-iddle {
@extend svg.nvd3-svg;
}

View File

@@ -62,9 +62,12 @@
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
@margin-sm: 10px;
@margin-md: 20px;
@margin-lg: 30px;
@margin-xs: 5px;
@margin-sm: 10px;
@margin-md: 20px;
@margin-lg: 30px;
@margin-xl: 40px;
@margin-xxl: 50px;
@padding-base-vertical: 5px;
@padding-base-horizontal: 16px;
@@ -986,14 +989,18 @@
@sys-msg-border : 1px solid @common-border-color;
// v2 History
@history-base-font-size : @font-size-small;
@history-base-bg : @gray-lightest;
@history-entry-day-bg : @gray;
@history-entry-selected-bg : @red;
@history-base-color : @gray-light;
@history-highlight-color : @gray;
@history-toolbar-bg-color : @toolbar-alt-bg-color;
@history-toolbar-color : @text-color;
@history-base-font-size : @font-size-small;
@history-base-bg : @gray-lightest;
@history-entry-label-bg-color : @red;
@history-entry-label-color : #FFF;
@history-entry-selected-label-bg-color : #FFF;
@history-entry-selected-label-color : @red;
@history-entry-day-bg : @gray;
@history-entry-selected-bg : @red;
@history-base-color : @gray-light;
@history-highlight-color : @gray;
@history-toolbar-bg-color : @toolbar-alt-bg-color;
@history-toolbar-color : @text-color;
// Input suggestions
@input-suggestion-v-offset : 6px;

View File

@@ -6,6 +6,7 @@
@footer-height: 50px;
// Styleguide colors
@ol-blue-gray-0 : #f4f5f8;
@ol-blue-gray-1 : #E4E8EE;
@ol-blue-gray-2 : #9DA7B7;
@ol-blue-gray-3 : #5D6879;
@@ -21,6 +22,8 @@
@ol-dark-red : #A6312B;
@ol-type-color : @ol-blue-gray-3;
@accent-color-primary: @ol-green;
@accent-color-secondary: @ol-dark-green;
// Navbar customization
@navbar-title-color : @ol-blue-gray-1;
@@ -65,8 +68,14 @@
@btn-info-bg : @ol-blue;
@btn-info-border : transparent;
// Padding
@padding-xs-horizontal : 8px;
@padding-sm: 10px;
@padding-md: 20px;
@padding-lg: 30px;
@padding-xl: 40px;
// Alerts
@alert-padding : 15px;
@alert-border-radius : @border-radius-base;
@@ -167,6 +176,9 @@
@folders-tag-menu-hover : rgba(0, 0, 0, .1);
@folders-tag-menu-active-hover : rgba(0, 0, 0, .1);
// Portal
@btn-portal-width : 200px;
// Project table
@structured-list-line-height : 2.5;
@structured-list-link-color : @ol-blue;
@@ -265,6 +277,21 @@
@chat-new-message-textarea-bg : @ol-blue-gray-1;
@chat-new-message-textarea-color : @ol-blue-gray-6;
// Pagination
@pagination-active-bg : @ol-dark-green;
@pagination-active-border : @gray-lighter;
@pagination-active-color : #FFF;
@pagination-bg : #FFF;
@pagination-border : @gray-lighter;
@pagination-color : @ol-dark-green;
@pagination-disabled-color : @gray-dark;
@pagination-disabled-bg : @gray-lightest;
@pagination-disabled-border : @gray-lighter;
@pagination-hover-color : @ol-dark-green;
@pagination-hover-bg : @gray-lightest;
@pagination-hover-border : @gray-lighter;
// PDF
@pdf-top-offset : @toolbar-small-height;
@pdf-bg : @ol-blue-gray-1;
@@ -273,16 +300,29 @@
@log-line-no-color : #FFF;
@log-hints-color : @ol-blue-gray-4;
// Portals
@black-alpha-strong : rgba(0,0,0,0.8);
// v2 History
@history-base-font-size : @font-size-small;
@history-base-bg : @ol-blue-gray-1;
@history-entry-day-bg : @ol-blue-gray-2;
@history-entry-selected-bg : @ol-green;
@history-base-color : @ol-blue-gray-2;
@history-highlight-color : @ol-type-color;
@history-toolbar-bg-color : @editor-toolbar-bg;
@history-toolbar-color : #FFF;
@history-base-font-size : @font-size-small;
@history-base-bg : @ol-blue-gray-1;
@history-entry-label-bg-color : @ol-blue;
@history-entry-label-color : #FFF;
@history-entry-selected-label-bg-color: #FFF;
@history-entry-selected-label-color : @ol-blue;
@history-entry-day-bg : @ol-blue-gray-2;
@history-entry-selected-bg : @ol-green;
@history-base-color : @ol-blue-gray-2;
@history-highlight-color : @ol-type-color;
@history-toolbar-bg-color : @editor-toolbar-bg;
@history-toolbar-color : #FFF;
// Screens
// added -size to not conflict with common_variables
@screen-size-sm-max : 767px;
@screen-size-md-min : 768px;
@screen-size-md-max : 991px;
// System messages
@sys-msg-background : @ol-blue;
@@ -300,6 +340,7 @@
@gray-light: #a4a4a4;
@gray-lighter: #cfcfcf;
@gray-lightest: #f0f0f0;
@white: #ffffff;
@blue: #405ebf;
@blueDark: #040D2D;

View File

@@ -6,8 +6,12 @@
@import "app/ol-style-guide.less";
@import "_style_includes.less";
@import "_ol_style_includes.less";
@import "components/embed-responsive.less";
@import "components/icons.less";
@import "components/pagination.less";
// Pages
@import "app/about.less";
@import "app/blog-posts.less";
@import "app/cms-page.less";
@import "app/cms-page.less";
@import "app/portals.less";

View File

@@ -0,0 +1,85 @@
const mongojs = require('../app/js/infrastructure/mongojs')
const { db } = mongojs
const async = require('async')
const minilist = require('minimist')
const newTimeout = 240
const oldTimeoutLimits = {$gt: 60, $lt: 240}
const updateUser = function (user, callback) {
console.log(`Updating user ${user._id}`)
const update = {
$set: {
'features.compileTimeout': newTimeout
}
}
db.users.update({
_id: user._id,
'features.compileTimeout': oldTimeoutLimits
}, update, callback)
}
const updateUsers = (users, callback) =>
async.eachLimit(users, ASYNC_LIMIT, updateUser, function (error) {
if (error) {
callback(error)
return
}
counter += users.length
console.log(`${counter} users updated`)
if (DO_ALL) {
return loopForUsers(callback)
} else {
console.log('*** run again to continue updating ***')
return callback()
}
})
var loopForUsers = callback =>
db.users.find(
{ 'features.compileTimeout': oldTimeoutLimits },
{ 'features.compileTimeout': 1 }
).limit(FETCH_LIMIT, function (error, users) {
if (error) {
callback(error)
return
}
if (users.length === 0) {
console.log(`DONE (${counter} users updated)`)
return callback()
}
updateUsers(users, callback)
})
var counter = 0
var run = () =>
loopForUsers(function (error) {
if (error) { throw error }
process.exit()
})
let FETCH_LIMIT, ASYNC_LIMIT, DO_ALL
var setup = function () {
let args = minilist(process.argv.slice(2))
// --fetch N get N users each time
FETCH_LIMIT = (args.fetch) ? args.fetch : 100
// --async M run M updates in parallel
ASYNC_LIMIT = (args.async) ? args.async : 10
// --all means run to completion
if (args.all) {
if (args.fetch) {
console.error('error: do not use --fetch with --all')
process.exit(1)
} else {
DO_ALL = true
// if we are updating for all users then ignore the fetch limit.
FETCH_LIMIT = 0
// A limit() value of 0 (i.e. .limit(0)) is equivalent to setting
// no limit.
// https://docs.mongodb.com/manual/reference/method/cursor.limit
}
}
}
setup()
run()

View File

@@ -17,9 +17,13 @@ describe 'AnalyticsController', ->
updateEditingSession: sinon.stub().callsArgWith(3)
recordEvent: sinon.stub().callsArgWith(3)
@InstitutionsAPI =
getInstitutionLicences: sinon.stub().callsArgWith(4)
@controller = SandboxedModule.require modulePath, requires:
"./AnalyticsManager":@AnalyticsManager
"../Authentication/AuthenticationController":@AuthenticationController
"../Institutions/InstitutionsAPI":@InstitutionsAPI
"logger-sharelatex":
log:->
'../../infrastructure/GeoIpLookup': @GeoIpLookup =
@@ -66,3 +70,19 @@ describe 'AnalyticsController', ->
@controller.recordEvent @req, @res
@AnalyticsManager.recordEvent.calledWith(@req.sessionID, @req.params["event"], @req.body).should.equal true
done()
describe "licences", ->
beforeEach ->
@req =
query:
resource_id:1
start_date:'1514764800'
end_date:'1530662400'
resource_type:'institution'
sessionID: "sessionIDHere"
session: {}
it "should trigger institutions api to fetch licences graph data", (done)->
@controller.licences @req, @res
@InstitutionsAPI.getInstitutionLicences.calledWith(@req.query["resource_id"], @req.query["start_date"], @req.query["end_date"], @req.query["lag"]).should.equal true
done()

View File

@@ -15,7 +15,6 @@ describe "AuthenticationController", ->
tk.freeze(Date.now())
@AuthenticationController = SandboxedModule.require modulePath, requires:
"./AuthenticationManager": @AuthenticationManager = {}
"../User/UserGetter" : @UserGetter = {}
"../User/UserUpdater" : @UserUpdater = {}
"metrics-sharelatex": @Metrics = { inc: sinon.stub() }
"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() }
@@ -29,6 +28,7 @@ describe "AuthenticationController", ->
trackSession: sinon.stub()
untrackSession: sinon.stub()
revokeAllUserSessions: sinon.stub().callsArgWith(1, null)
"../../infrastructure/Modules": @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, [])}}
@user =
_id: ObjectId()
email: @email = "USER@example.com"
@@ -214,6 +214,7 @@ describe "AuthenticationController", ->
beforeEach ->
@AuthenticationController._recordFailedLogin = sinon.stub()
@AuthenticationController._recordSuccessfulLogin = sinon.stub()
@Modules.hooks.fire = sinon.stub().callsArgWith(3, null, [])
# @AuthenticationController.establishUserSession = sinon.stub().callsArg(2)
@req.body =
email: @email
@@ -222,6 +223,17 @@ describe "AuthenticationController", ->
postLoginRedirect: "/path/to/redir/to"
@cb = sinon.stub()
describe "when the preDoPassportLogin hooks produce an info object", ->
beforeEach ->
@Modules.hooks.fire = sinon.stub().callsArgWith(3, null, [null, {redir: '/somewhere'}, null])
it "should stop early and call done with this info object", (done) ->
@AuthenticationController.doPassportLogin(@req, @req.body.email, @req.body.password, @cb)
@cb.callCount.should.equal 1
@cb.calledWith(null, false, {redir: '/somewhere'}).should.equal true
@LoginRateLimiter.processLoginRequest.callCount.should.equal 0
done()
describe "when the users rate limit", ->
beforeEach ->

View File

@@ -32,6 +32,7 @@ describe "EditorController", ->
'../Project/ProjectEntityUpdateHandler' : @ProjectEntityUpdateHandler = {}
'../Project/ProjectOptionsHandler' : @ProjectOptionsHandler =
setCompiler: sinon.stub().yields()
setImageName: sinon.stub().yields()
setSpellCheckLanguage: sinon.stub().yields()
'../Project/ProjectDetailsHandler': @ProjectDetailsHandler =
setProjectDescription: sinon.stub().yields()
@@ -377,6 +378,19 @@ describe "EditorController", ->
.calledWith(@project_id, "compilerUpdated", @compiler)
.should.equal true
describe "setImageName", ->
beforeEach ->
@imageName = "texlive-1234.5"
@EditorController.setImageName @project_id, @imageName, @callback
it "should send the new imageName and project id to the project options handler", ->
@ProjectOptionsHandler.setImageName
.calledWith(@project_id, @imageName)
.should.equal true
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "imageNameUpdated", @imageName)
.should.equal true
describe "setSpellCheckLanguage", ->
beforeEach ->
@languageCode = "fr"

View File

@@ -63,6 +63,8 @@ describe 'ExportsHandler', ->
beforeEach (done) ->
@project =
id: @project_id
compiler: 'pdflatex'
imageName: 'mock-image-name'
overleaf:
id: @project_history_id # for projects imported from v1
history:
@@ -100,6 +102,9 @@ describe 'ExportsHandler', ->
historyId: @project_history_id
historyVersion: @historyVersion
v1ProjectId: @project_history_id
metadata:
compiler: 'pdflatex'
imageName: 'mock-image-name'
user:
id: @user_id
firstName: @user.first_name
@@ -132,6 +137,9 @@ describe 'ExportsHandler', ->
historyId: @project_history_id
historyVersion: @historyVersion
v1ProjectId: @project_history_id
metadata:
compiler: 'pdflatex'
imageName: 'mock-image-name'
user:
id: @user_id
firstName: @custom_first_name
@@ -273,3 +281,32 @@ describe 'ExportsHandler', ->
@callback.calledWith(null, { body: @body })
.should.equal true
describe 'fetchZip', ->
beforeEach (done) ->
@settings.apis =
v1:
url: 'http://localhost:5000'
user: 'overleaf'
pass: 'pass'
@export_id = 897
@body = "https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee"
@stubGet = sinon.stub().yields(null, {statusCode: 200}, { body: @body })
done()
describe "when all goes well", ->
beforeEach (done) ->
@stubRequest.get = @stubGet
@ExportsHandler.fetchZip @export_id, (error, body) =>
@callback(error, body)
done()
it 'should issue the request', ->
expect(@stubGet.getCall(0).args[0]).to.deep.equal
url: @settings.apis.v1.url + '/api/v1/sharelatex/exports/' + @export_id + '/zip_url'
auth:
user: @settings.apis.v1.user
pass: @settings.apis.v1.pass
it 'should return the v1 export id', ->
@callback.calledWith(null, { body: @body })
.should.equal true

View File

@@ -11,12 +11,12 @@ describe "InstitutionsAPI", ->
beforeEach ->
@logger = err: sinon.stub(), log: ->
settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
@settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
@request = sinon.stub()
@InstitutionsAPI = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
'settings-sharelatex': settings
'settings-sharelatex': @settings
'request': @request
@stubbedUser =
@@ -40,6 +40,34 @@ describe "InstitutionsAPI", ->
should.not.exist(requestOptions.body)
body.should.equal responseBody
done()
it 'handle empty response', (done)->
@settings.apis = null
@InstitutionsAPI.getInstitutionAffiliations @institutionId, (err, body) =>
should.not.exist(err)
expect(body).to.be.a 'Array'
body.length.should.equal 0
done()
describe 'getInstitutionLicences', ->
it 'get licences', (done)->
@institutionId = 123
responseBody = {"lag":"monthly","data":[{"key":"users","values":[{"x":"2018-01-01","y":1}]}]}
@request.yields(null, { statusCode: 200 }, responseBody)
startDate = '1417392000'
endDate = '1420848000'
@InstitutionsAPI.getInstitutionLicences @institutionId, startDate, endDate, 'monthly', (err, body) =>
should.not.exist(err)
@request.calledOnce.should.equal true
requestOptions = @request.lastCall.args[0]
expectedUrl = "v1.url/api/v2/institutions/#{@institutionId}/institution_licences"
requestOptions.url.should.equal expectedUrl
requestOptions.method.should.equal 'GET'
requestOptions.body['start_date'].should.equal startDate
requestOptions.body['end_date'].should.equal endDate
requestOptions.body.lag.should.equal 'monthly'
body.should.equal responseBody
done()
describe 'getUserAffiliations', ->
it 'get affiliations', (done)->
@@ -65,6 +93,14 @@ describe "InstitutionsAPI", ->
err.message.should.have.string body.errors
done()
it 'handle empty response', (done)->
@settings.apis = null
@InstitutionsAPI.getUserAffiliations @stubbedUser._id, (err, body) =>
should.not.exist(err)
expect(body).to.be.a 'Array'
body.length.should.equal 0
done()
describe 'addAffiliation', ->
beforeEach ->
@request.callsArgWith(1, null, { statusCode: 201 })
@@ -74,6 +110,7 @@ describe "InstitutionsAPI", ->
university: { id: 1 }
role: 'Prof'
department: 'Math'
confirmedAt: new Date()
@InstitutionsAPI.addAffiliation @stubbedUser._id, @newEmail, affiliationOptions, (err)=>
should.not.exist(err)
@request.calledOnce.should.equal true
@@ -83,11 +120,12 @@ describe "InstitutionsAPI", ->
requestOptions.method.should.equal 'POST'
body = requestOptions.body
Object.keys(body).length.should.equal 4
Object.keys(body).length.should.equal 5
body.email.should.equal @newEmail
body.university.should.equal affiliationOptions.university
body.department.should.equal affiliationOptions.department
body.role.should.equal affiliationOptions.role
body.confirmedAt.should.equal affiliationOptions.confirmedAt
done()
it 'handle error', (done)->

View File

@@ -8,11 +8,11 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/Instit
describe 'InstitutionsFeatures', ->
beforeEach ->
@UserGetter = getUserFullEmails: sinon.stub()
@InstitutionsGetter = getConfirmedInstitutions: sinon.stub()
@PlansLocator = findLocalPlanInSettings: sinon.stub()
@institutionPlanCode = 'institution_plan_code'
@InstitutionsFeatures = SandboxedModule.require modulePath, requires:
'../User/UserGetter': @UserGetter
'./InstitutionsGetter': @InstitutionsGetter
'../Subscription/PlansLocator': @PlansLocator
'settings-sharelatex': institutionPlanCode: @institutionPlanCode
'logger-sharelatex':
@@ -23,47 +23,37 @@ describe 'InstitutionsFeatures', ->
describe "hasLicence", ->
it 'should handle error', (done)->
@UserGetter.getUserFullEmails.yields(new Error('Nope'))
@InstitutionsGetter.getConfirmedInstitutions.yields(new Error('Nope'))
@InstitutionsFeatures.hasLicence @userId, (error, hasLicence) ->
expect(error).to.exist
done()
it 'should return false if user has no affiliations', (done) ->
@UserGetter.getUserFullEmails.yields(null, [])
@InstitutionsFeatures.hasLicence @userId, (error, hasLicence) ->
expect(error).to.not.exist
expect(hasLicence).to.be.false
done()
it 'should return false if user has no confirmed affiliations', (done) ->
affiliations = [
{ confirmedAt: null, affiliation: institution: { licence: 'pro_plus' } }
]
@UserGetter.getUserFullEmails.yields(null, affiliations)
institutions = []
@InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions)
@InstitutionsFeatures.hasLicence @userId, (error, hasLicence) ->
expect(error).to.not.exist
expect(hasLicence).to.be.false
done()
it 'should return false if user has no paid affiliations', (done) ->
affiliations = [
{ confirmedAt: new Date(), affiliation: institution: { licence: 'free' } }
institutions = [
{ licence: 'free' }
]
@UserGetter.getUserFullEmails.yields(null, affiliations)
@InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions)
@InstitutionsFeatures.hasLicence @userId, (error, hasLicence) ->
expect(error).to.not.exist
expect(hasLicence).to.be.false
done()
it 'should return true if user has confirmed paid affiliation', (done)->
affiliations = [
{ confirmedAt: new Date(), affiliation: institution: { licence: 'pro_plus' } }
{ confirmedAt: new Date(), affiliation: institution: { licence: 'free' } }
{ confirmedAt: null, affiliation: institution: { licence: 'pro' } }
{ confirmedAt: null, affiliation: institution: { licence: null } }
{ confirmedAt: new Date(), affiliation: institution: {} }
institutions = [
{ licence: 'pro_plus' }
{ licence: 'free' }
{ licence: 'pro' }
{ licence: null }
]
@UserGetter.getUserFullEmails.yields(null, affiliations)
@InstitutionsGetter.getConfirmedInstitutions.yields(null, institutions)
@InstitutionsFeatures.hasLicence @userId, (error, hasLicence) ->
expect(error).to.not.exist
expect(hasLicence).to.be.true

View File

@@ -0,0 +1,44 @@
SandboxedModule = require('sandboxed-module')
require('chai').should()
expect = require('chai').expect
sinon = require('sinon')
modulePath = require('path').join __dirname, '../../../../app/js/Features/Institutions/InstitutionsGetter.js'
describe 'InstitutionsGetter', ->
beforeEach ->
@UserGetter = getUserFullEmails: sinon.stub()
@InstitutionsGetter = SandboxedModule.require modulePath, requires:
'../User/UserGetter': @UserGetter
'logger-sharelatex':
log:-> console.log(arguments)
err:->
@userId = '12345abcde'
describe "getConfirmedInstitutions", ->
it 'filters unconfirmed emails', (done) ->
@userEmails = [
{ confirmedAt: null, affiliation: institution: { id: 123 } }
{ confirmedAt: new Date(), affiliation: institution: { id: 456 } }
{ confirmedAt: new Date(), affiliation: null }
{ confirmedAt: new Date(), affiliation: institution: null }
]
@UserGetter.getUserFullEmails.yields(null, @userEmails)
@InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) ->
expect(error).to.not.exist
institutions.length.should.equal 1
institutions[0].id.should.equal 456
done()
it 'should handle empty response', (done) ->
@UserGetter.getUserFullEmails.yields(null, [])
@InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) ->
expect(error).to.not.exist
institutions.length.should.equal 0
done()
it 'should handle error', (done) ->
@UserGetter.getUserFullEmails.yields(new Error('Nope'))
@InstitutionsGetter.getConfirmedInstitutions @userId, (error, institutions) ->
expect(error).to.exist
done()

View File

@@ -145,6 +145,18 @@ describe "ProjectController", ->
done()
@ProjectController.updateProjectSettings @req, @res
it "should update the imageName", (done) ->
@EditorController.setImageName = sinon.stub().callsArg(2)
@req.body =
imageName: @imageName = "texlive-1234.5"
@res.sendStatus = (code) =>
@EditorController.setImageName
.calledWith(@project_id, @imageName)
.should.equal true
code.should.equal 204
done()
@ProjectController.updateProjectSettings @req, @res
it "should update the spell check language", (done) ->
@EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2)
@req.body =

View File

@@ -111,7 +111,7 @@ describe 'ProjectCreationHandler', ->
project.spellCheckLanguage.should.equal "de"
done()
it "should set the imageName to currentImageName if set", (done) ->
it "should set the imageName to currentImageName if set and no imageName attribute", (done) ->
@Settings.currentImageName = "mock-image-name"
@handler.createBlankProject ownerId, projectName, (err, project)=>
project.imageName.should.equal @Settings.currentImageName
@@ -123,6 +123,14 @@ describe 'ProjectCreationHandler', ->
expect(project.imageName).to.not.exist
done()
it "should set the imageName to the attribute value if set and not overwrite it with the currentImageName", (done) ->
@Settings.currentImageName = "mock-image-name"
attributes =
imageName: "attribute-image-name"
@handler.createBlankProject ownerId, projectName, attributes, (err, project)=>
project.imageName.should.equal attributes.imageName
done()
it "should not set the overleaf.history.display if not configured in settings", (done) ->
@Settings.apis.project_history.displayHistoryForNewProjects = false
@handler.createBlankProject ownerId, projectName, (err, project)=>

View File

@@ -19,6 +19,11 @@ describe 'creating a project', ->
{name: "English", code: "en"}
{name: "French", code: "fr"}
]
imageRoot: "docker-repo/subdir"
allowedImageNames: [
{imageName: "texlive-0000.0", imageDesc: "test image 0"}
{imageName: "texlive-1234.5", imageDesc: "test image 1"}
]
'logger-sharelatex':
log:->
err:->
@@ -37,6 +42,19 @@ describe 'creating a project', ->
@projectModel.update.called.should.equal false
done()
describe 'Setting the imageName', ->
it 'should perform and update on mongo', (done)->
@handler.setImageName project_id, "texlive-1234.5", (err)=>
args = @projectModel.update.args[0]
args[0]._id.should.equal project_id
args[1].imageName.should.equal "docker-repo/subdir/texlive-1234.5"
done()
@projectModel.update.args[0][3]()
it 'should not perform and update on mongo if it is not a reconised compiler', (done)->
@handler.setImageName project_id, "something", (err)=>
@projectModel.update.called.should.equal false
done()
describe "setting the spellCheckLanguage", ->

View File

@@ -20,7 +20,7 @@ describe "UserCreator", ->
@addAffiliation = sinon.stub().yields()
@UserCreator = SandboxedModule.require modulePath, requires:
"../../models/User": User:@UserModel
"logger-sharelatex":{log:->}
"logger-sharelatex":{ log: sinon.stub(), err: sinon.stub() }
'metrics-sharelatex': {timeAsyncMethod: ()->}
"../Institutions/InstitutionsAPI": addAffiliation: @addAffiliation
@@ -88,3 +88,11 @@ describe "UserCreator", ->
process.nextTick () =>
sinon.assert.calledWith(@addAffiliation, user._id, user.email)
done()
it "should not add affiliation if skipping", (done)->
attributes = email: @email
options = skip_affiliation: true
@UserCreator.createNewUser attributes, options, (err, user) =>
process.nextTick () =>
sinon.assert.notCalled(@addAffiliation)
done()

View File

@@ -132,11 +132,17 @@ describe "UserRegistrationHandler", ->
@AuthenticationManager.setUserPassword.calledWith(@user._id, @passingRequest.password).should.equal true
done()
it "should add the user to the news letter manager", (done)->
it "should add the user to the newsletter if accepted terms", (done)->
@passingRequest.subscribeToNewsletter = "true"
@handler.registerNewUser @passingRequest, (err)=>
@NewsLetterManager.subscribe.calledWith(@user).should.equal true
done()
it "should not add the user to the newsletter if not accepted terms", (done)->
@handler.registerNewUser @passingRequest, (err)=>
@NewsLetterManager.subscribe.calledWith(@user).should.equal false
done()
it "should track the registration event", (done)->
@handler.registerNewUser @passingRequest, (err)=>
@AnalyticsManager.recordEvent

View File

@@ -18,21 +18,27 @@ describe "UserUpdater", ->
getUserEmail: sinon.stub()
getUserByAnyEmail: sinon.stub()
ensureUniqueEmailAddress: sinon.stub()
@logger = err: sinon.stub(), log: ->
@logger =
err: sinon.stub()
log: ->
warn: ->
@addAffiliation = sinon.stub().yields()
@removeAffiliation = sinon.stub().callsArgWith(2, null)
@refreshFeatures = sinon.stub().yields()
@NewsletterManager =
changeEmail:sinon.stub()
@UserUpdater = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger
"../../infrastructure/mongojs":@mongojs
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
"./UserGetter": @UserGetter
'../Institutions/InstitutionsAPI':
addAffiliation: @addAffiliation
removeAffiliation: @removeAffiliation
'../Subscription/FeaturesUpdater': refreshFeatures: @refreshFeatures
"../../infrastructure/mongojs":@mongojs
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
"settings-sharelatex": @settings = {}
"request": @request = {}
"../Newsletter/NewsletterManager": @NewsletterManager
@stubbedUser =
_id: "3131231"
@@ -174,6 +180,10 @@ describe "UserUpdater", ->
done()
describe 'setDefaultEmailAddress', ->
beforeEach ->
@UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email)
@NewsletterManager.changeEmail.callsArgWith(2, null)
it 'set default', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1)
@@ -185,6 +195,16 @@ describe "UserUpdater", ->
).should.equal true
done()
it 'set changed the email in newsletter', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1)
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@NewsletterManager.changeEmail.calledWith(
@stubbedUser.email, @newEmail
).should.equal true
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@@ -221,7 +241,7 @@ describe "UserUpdater", ->
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@addAffiliation.calledOnce.should.equal true
sinon.assert.calledWith(@addAffiliation, @stubbedUser._id, @newEmail)
sinon.assert.calledWith(@addAffiliation, @stubbedUser._id, @newEmail, { confirmedAt: new Date() } )
done()
it 'handle error', (done)->
@@ -244,7 +264,7 @@ describe "UserUpdater", ->
done()
it 'handle affiliation error', (done)->
@addAffiliation.callsArgWith(2, new Error('nope'))
@addAffiliation.callsArgWith(3, new Error('nope'))
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
@UserUpdater.updateUser.called.should.equal false

View File

@@ -4,30 +4,45 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
@scope =
$watch: sinon.stub()
$on: sinon.stub()
project:
features:
versioning: true
@ide = {}
@historyManager = new HistoryV2Manager(@ide, @scope)
it "should setup the history scope on intialization", ->
it "should setup the history scope on initialization", ->
expect(@scope.history).to.deep.equal({
isV2: true
updates: []
viewMode: null
nextBeforeTimestamp: null
atEnd: false
userHasFullFeature: true
freeHistoryLimitHit: false
selection: {
label: null
updates: []
pathname: null
docs: {}
pathname: null
range: {
fromV: null
toV: null
}
}
error: null
showOnlyLabels: false
labels: null
diff: null
files: []
selectedFile: null
})
it "should setup history without full access to the feature if the project does not have versioning", ->
@scope.project.features.versioning = false
@historyManager = new HistoryV2Manager(@ide, @scope)
expect(@scope.history.userHasFullFeature).to.equal false
describe "_perDocSummaryOfUpdates", ->
it "should return the range of updates for the docs", ->
result = @historyManager._perDocSummaryOfUpdates([{