mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-01 21:31:36 +02:00
Merge branch 'master' into ns-use-regex-test
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -23,4 +23,4 @@ module.exports =
|
||||
|
||||
publicApiRouter.use '/analytics/uniExternalCollaboration',
|
||||
AuthenticationController.httpAuth,
|
||||
AnalyticsProxy.call('/uniExternalCollaboration')
|
||||
AnalyticsProxy.call('/uniExternalCollaboration')
|
||||
|
||||
@@ -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, ()->)
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = (backendGroup)->
|
||||
return callback()
|
||||
serverId = @_parseServerIdFromResponse(response)
|
||||
if !serverId? # We don't get a cookie back if it hasn't changed
|
||||
return callback()
|
||||
return rclient.expire(@buildKey(project_id), Settings.clsiCookie.ttl, callback)
|
||||
if rclient_secondary?
|
||||
@_setServerIdInRedis rclient_secondary, project_id, serverId
|
||||
@_setServerIdInRedis rclient, project_id, serverId, (err) ->
|
||||
|
||||
@@ -7,8 +7,8 @@ ProjectGetter = require("../Project/ProjectGetter")
|
||||
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
|
||||
logger = require "logger-sharelatex"
|
||||
Url = require("url")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")()
|
||||
NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")("newBackendcloud")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
|
||||
NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi_new?.backendGroupName)
|
||||
ClsiStateManager = require("./ClsiStateManager")
|
||||
_ = require("underscore")
|
||||
async = require("async")
|
||||
@@ -100,6 +100,7 @@ module.exports = ClsiManager =
|
||||
timer = new Metrics.Timer("compile.currentBackend")
|
||||
request opts, (err, response, body)->
|
||||
timer.done()
|
||||
Metrics.inc "compile.currentBackend.response.#{response?.statusCode}"
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi"
|
||||
return callback(err)
|
||||
@@ -111,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
|
||||
|
||||
@@ -9,7 +9,7 @@ Settings = require "settings-sharelatex"
|
||||
AuthenticationController = require "../Authentication/AuthenticationController"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
RateLimiter = require("../../infrastructure/RateLimiter")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")()
|
||||
ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
|
||||
Path = require("path")
|
||||
|
||||
module.exports = CompileController =
|
||||
|
||||
@@ -163,6 +163,13 @@ module.exports = EditorController =
|
||||
EditorRealTimeController.emitToRoom project_id, 'compilerUpdated', compiler
|
||||
callback()
|
||||
|
||||
setImageName : (project_id, imageName, callback = (err) ->) ->
|
||||
ProjectOptionsHandler.setImageName project_id, imageName, (err) ->
|
||||
return callback(err) if err?
|
||||
logger.log imageName:imageName, project_id:project_id, "setting imageName"
|
||||
EditorRealTimeController.emitToRoom project_id, 'imageNameUpdated', imageName
|
||||
callback()
|
||||
|
||||
setSpellCheckLanguage : (project_id, languageCode, callback = (err) ->) ->
|
||||
ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err) ->
|
||||
return callback(err) if err?
|
||||
|
||||
@@ -37,3 +37,11 @@ module.exports =
|
||||
status_detail: parsed_export.status_detail
|
||||
}
|
||||
res.send export_json: json
|
||||
|
||||
exportZip: (req, res) ->
|
||||
{export_id} = req.params
|
||||
AuthenticationController.getLoggedInUserId(req)
|
||||
ExportsHandler.fetchZip export_id, (err, export_zip_url) ->
|
||||
return err if err?
|
||||
|
||||
res.redirect export_zip_url
|
||||
|
||||
@@ -57,6 +57,9 @@ module.exports = ExportsHandler = self =
|
||||
historyId: project.overleaf?.history?.id
|
||||
historyVersion: historyVersion
|
||||
v1ProjectId: project.overleaf?.id
|
||||
metadata:
|
||||
compiler: project.compiler
|
||||
imageName: project.imageName
|
||||
user:
|
||||
id: user_id
|
||||
firstName: user.first_name
|
||||
@@ -115,3 +118,19 @@ module.exports = ExportsHandler = self =
|
||||
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
|
||||
logger.err err:err, export:export_id, "v1 export returned failure status code: #{res.statusCode}"
|
||||
callback err
|
||||
|
||||
fetchZip: (export_id, callback=(err, zip_url) ->) ->
|
||||
console.log("#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url")
|
||||
request.get {
|
||||
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/zip_url"
|
||||
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
|
||||
}, (err, res, body) ->
|
||||
if err?
|
||||
logger.err err:err, export:export_id, "error making request to v1 export"
|
||||
callback err
|
||||
else if 200 <= res.statusCode < 300
|
||||
callback null, body
|
||||
else
|
||||
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
|
||||
logger.err err:err, export:export_id, "v1 export zip fetch returned failure status code: #{res.statusCode}"
|
||||
callback err
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = [])->
|
||||
|
||||
@@ -55,7 +55,8 @@ module.exports = ProjectCreationHandler =
|
||||
if Settings.apis?.project_history?.displayHistoryForNewProjects
|
||||
project.overleaf.history.display = true
|
||||
if Settings.currentImageName?
|
||||
project.imageName = Settings.currentImageName
|
||||
# avoid clobbering any imageName already set in attributes (e.g. importedImageName)
|
||||
project.imageName ?= Settings.currentImageName
|
||||
project.rootFolder[0] = rootFolder
|
||||
User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
|
||||
project.spellCheckLanguage = user.ace.spellCheckLanguage
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
_ = require("underscore")
|
||||
|
||||
Path = require 'path'
|
||||
|
||||
module.exports = ProjectEditorHandler =
|
||||
trackChangesAvailable: false
|
||||
@@ -20,6 +20,7 @@ module.exports = ProjectEditorHandler =
|
||||
members: []
|
||||
invites: invites
|
||||
tokens: project.tokens
|
||||
imageName: if project.imageName? then Path.basename(project.imageName) else undefined
|
||||
|
||||
if !result.invites?
|
||||
result.invites = []
|
||||
|
||||
@@ -17,6 +17,16 @@ module.exports =
|
||||
if callback?
|
||||
callback()
|
||||
|
||||
setImageName : (project_id, imageName, callback = ()->)->
|
||||
logger.log project_id:project_id, imageName:imageName, "setting the imageName"
|
||||
imageName = imageName.toLowerCase()
|
||||
if ! _.some(settings.allowedImageNames, (allowed) -> imageName is allowed.imageName)
|
||||
return callback()
|
||||
conditions = {_id:project_id}
|
||||
update = {imageName: settings.imageRoot + '/' + imageName}
|
||||
Project.update conditions, update, {}, (err)->
|
||||
if callback?
|
||||
callback()
|
||||
|
||||
setSpellCheckLanguage: (project_id, languageCode, callback = ()->)->
|
||||
logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language"
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -68,7 +68,6 @@ module.exports =
|
||||
shouldAllowEditingDetails: shouldAllowEditingDetails
|
||||
languages: Settings.languages,
|
||||
accountSettingsTabActive: true
|
||||
showAffiliationsUI: (req.query?.aff == "true") or false
|
||||
|
||||
sessionsPage: (req, res, next) ->
|
||||
user = AuthenticationController.getSessionUser(req)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
sanitize = require('sanitizer')
|
||||
User = require("../../models/User").User
|
||||
UserCreator = require("./UserCreator")
|
||||
UserGetter = require("./UserGetter")
|
||||
@@ -54,7 +53,8 @@ module.exports = UserRegistrationHandler =
|
||||
(cb)-> User.update {_id: user._id}, {"$set":{holdingAccount:false}}, cb
|
||||
(cb)-> AuthenticationManager.setUserPassword user._id, userDetails.password, cb
|
||||
(cb)->
|
||||
NewsLetterManager.subscribe user, ->
|
||||
if userDetails.subscribeToNewsletter == "true"
|
||||
NewsLetterManager.subscribe user, ->
|
||||
cb() #this can be slow, just fire it off
|
||||
], (err)->
|
||||
logger.log user: user, "registered"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
21
services/web/app/views/_mixins/faq_search.pug
Normal file
21
services/web/app/views/_mixins/faq_search.pug
Normal 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')
|
||||
122
services/web/app/views/_mixins/links.pug
Normal file
122
services/web/app/views/_mixins/links.pug
Normal file
@@ -0,0 +1,122 @@
|
||||
mixin linkAdvisors(linkText, linkClass, track)
|
||||
//- To Do: verify path
|
||||
- var gaCategory = track && track.category ? track.category : 'All'
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
a(href="/advisors"
|
||||
class=linkClass ? linkClass : ''
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
)
|
||||
| #{linkText ? linkText : 'advisor programme'}
|
||||
|
||||
mixin linkBenefits(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/benefits" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'benefits'}
|
||||
|
||||
mixin linkBlog(linkText, linkClass, slug)
|
||||
if slug
|
||||
a(href="/blog/#{slug}" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'blog'}
|
||||
|
||||
mixin linkContact(linkText, linkClass)
|
||||
a(href="/contact" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'contact'}
|
||||
|
||||
mixin linkEducation(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/plans" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'teaching toolkit'}
|
||||
|
||||
mixin linkEmail(linkText, linkClass, email)
|
||||
//- To Do: env var?
|
||||
- var emailDomain = 'overleaf.com'
|
||||
a(href="mailto:#{email ? email : 'contact'}@#{emailDomain}" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'email'}
|
||||
|
||||
mixin linkInvite(linkText, linkClass, track)
|
||||
- var gaCategory = track && track.category ? track.category : 'All'
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
|
||||
a(href="/user/bonus"
|
||||
class=linkClass ? linkClass : ''
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
)
|
||||
| #{linkText ? linkText : 'invite your friends'}
|
||||
|
||||
mixin linkPlansAndPricing(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/plans" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'plans and pricing'}
|
||||
|
||||
mixin linkPrintNewTab(linkText, linkClass, icon, track)
|
||||
- var gaCategory = track && track.category ? track.category : null
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
|
||||
a(href='?media=print'
|
||||
class=linkClass ? linkClass : ''
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
target="_BLANK"
|
||||
)
|
||||
if icon
|
||||
i(class="fa fa-print")
|
||||
|
|
||||
| #{linkText ? linkText : 'print'}
|
||||
|
||||
mixin linkSignIn(linkText, linkClass)
|
||||
a(href="/login" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'sign in'}
|
||||
|
||||
mixin linkSignUp(linkText, linkClass)
|
||||
a(href="/register" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'sign up'}
|
||||
|
||||
mixin linkTweet(linkText, linkClass, tweetText, track)
|
||||
//- twitter-share-button is required by twitter
|
||||
- var gaCategory = track && track.category ? track.category : 'All'
|
||||
- var gaAction = track && track.action ? track.action : null
|
||||
- var gaLabel = track && track.label ? track.label : null
|
||||
- var mb = track && track.mb ? 'true' : null
|
||||
- var mbSegmentation = track && track.segmentation ? track.segmentation : null
|
||||
- var trigger = track && track.trigger ? track.trigger : null
|
||||
a(class="twitter-share-button " + linkClass
|
||||
event-tracking-ga=gaCategory
|
||||
event-tracking=gaAction
|
||||
event-tracking-label=gaLabel
|
||||
event-tracking-trigger=trigger
|
||||
event-tracking-mb=mb
|
||||
event-segmentation=mbSegmentation
|
||||
href="https://twitter.com/intent/tweet?text=" + tweetText
|
||||
target="_BLANK"
|
||||
) #{linkText ? linkText : 'tweet'}
|
||||
|
||||
mixin linkUniversities(linkText, linkClass)
|
||||
//- To Do: verify path
|
||||
a(href="/universities" class=linkClass ? linkClass : '')
|
||||
| #{linkText ? linkText : 'universities'}
|
||||
@@ -70,6 +70,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||
window.systemMessages = !{JSON.stringify(systemMessages).replace(/\//g, '\\/')};
|
||||
window.ab = {};
|
||||
window.user_id = '#{getLoggedInUserId()}';
|
||||
window.ExposedSettings = JSON.parse('!{JSON.stringify(ExposedSettings).replace(/\//g, "\\/")}');
|
||||
|
||||
- if (typeof(settings.algolia) != "undefined")
|
||||
script.
|
||||
|
||||
74
services/web/app/views/mixins/_pagination.pug
Normal file
74
services/web/app/views/mixins/_pagination.pug
Normal 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 »
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{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
|
||||
| {{ $ctrl.labelText }}
|
||||
button.history-label-delete-btn(
|
||||
ng-if="$ctrl.isOwnedByCurrentUser"
|
||||
stop-propagation="click"
|
||||
ng-click="$ctrl.onLabelDelete()"
|
||||
) ×
|
||||
|
||||
script(type="text/ng-template", id="historyLabelTooltipTpl")
|
||||
.history-label-tooltip
|
||||
p.history-label-tooltip-title
|
||||
i.fa.fa-tag
|
||||
| {{ $ctrl.labelText }}
|
||||
p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }}
|
||||
time.history-label-tooltip-datetime {{ $ctrl.labelCreationDateTime | formatDate }}
|
||||
|
||||
|
||||
script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("history_delete_label")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
p.help-block(ng-if="labelDetails")
|
||||
| #{translate("history_are_you_sure_delete_label")}
|
||||
strong "{{ labelDetails.comment }}"
|
||||
| ?
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="$dismiss()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
type="button"
|
||||
ng-click="deleteLabel()"
|
||||
ng-disabled="state.inflight"
|
||||
) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }}
|
||||
|
||||
@@ -3,13 +3,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
|
||||
| #{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
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{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
|
||||
| #{translate("loading")}...
|
||||
@@ -45,7 +45,7 @@
|
||||
text="history.diff.text",
|
||||
highlights="history.diff.highlights",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
resize-on="layout:main:resize,history:toggle",
|
||||
navigate-highlights="true"
|
||||
)
|
||||
.alert.alert-info(ng-if="history.diff.binary")
|
||||
@@ -70,12 +70,24 @@
|
||||
font-size="settings.fontSize",
|
||||
text="history.selectedFile.text",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
resize-on="layout:main:resize,history:toggle",
|
||||
)
|
||||
.alert.alert-info(ng-if="history.selectedFile.binary")
|
||||
| We're still working on showing image and binary changes, sorry. Stay tuned!
|
||||
.loading-panel(ng-show="history.selectedFile.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.error")
|
||||
.alert.alert-danger
|
||||
p
|
||||
| #{translate("generic_history_error")}
|
||||
a(
|
||||
ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}"
|
||||
) #{settings.adminEmail}
|
||||
p.clearfix
|
||||
a.alert-link-as-btn.pull-right(
|
||||
href
|
||||
ng-click="toggleHistory()"
|
||||
) #{translate("back_to_editor")}
|
||||
.error-panel(ng-show="history.selectedFile.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
|
||||
@@ -1,13 +1,75 @@
|
||||
.history-toolbar(
|
||||
ng-controller="HistoryV2ToolbarController"
|
||||
ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
|
||||
)
|
||||
span(ng-show="history.loadingFileTree")
|
||||
span.history-toolbar-selected-version(ng-show="history.loadingFileTree")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}
|
||||
span.history-toolbar-selected-version(
|
||||
ng-show="!history.loadingFileTree && !history.showOnlyLabels && history.selection.updates.length && !history.error"
|
||||
) #{translate("browsing_project_as_of")}
|
||||
time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
|
||||
.history-toolbar-btn(
|
||||
ng-click="toggleHistoryViewMode();"
|
||||
)
|
||||
i.fa
|
||||
| #{translate("compare_to_another_version")}
|
||||
span.history-toolbar-selected-version(
|
||||
ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error"
|
||||
) #{translate("browsing_project_labelled")}
|
||||
span.history-toolbar-selected-label "{{ history.selection.label.comment }}"
|
||||
div.history-toolbar-actions(
|
||||
ng-if="!history.error"
|
||||
)
|
||||
button.history-toolbar-btn(
|
||||
ng-click="showAddLabelDialog();"
|
||||
ng-if="!history.showOnlyLabels"
|
||||
ng-disabled="history.loadingFileTree || history.selection.updates.length == 0"
|
||||
)
|
||||
i.fa.fa-tag
|
||||
| #{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
|
||||
| #{translate("compare_to_another_version")}
|
||||
|
||||
.history-toolbar-entries-list(
|
||||
ng-if="!history.error"
|
||||
)
|
||||
toggle-switch(
|
||||
ng-model="history.showOnlyLabels"
|
||||
label-true=translate("history_view_labels")
|
||||
label-false=translate("history_view_all")
|
||||
description=translate("history_view_a11y_description")
|
||||
)
|
||||
|
||||
script(type="text/ng-template", id="historyV2AddLabelModalTemplate")
|
||||
form(
|
||||
name="addLabelModalForm"
|
||||
ng-submit="addLabelModalFormSubmit();"
|
||||
novalidate
|
||||
)
|
||||
.modal-header
|
||||
h3 #{translate("history_add_label")}
|
||||
.modal-body
|
||||
.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}}
|
||||
.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
|
||||
.form-group
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder=translate("history_new_label_name")
|
||||
ng-model="inputs.labelName"
|
||||
focus-on="open"
|
||||
required
|
||||
)
|
||||
p.help-block(ng-if="update")
|
||||
| #{translate("history_new_label_added_at")}
|
||||
strong {{ update.meta.end_ts | formatDate:'ddd Do MMM YYYY, h:mm a' }}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="$dismiss()"
|
||||
) #{translate("cancel")}
|
||||
input.btn.btn-primary(
|
||||
ng-disabled="addLabelModalForm.$invalid || state.inflight"
|
||||
ng-value="state.inflight ? '" + translate("history_adding_label") + "' : '" + translate("history_add_label") + "'"
|
||||
type="submit"
|
||||
)
|
||||
@@ -188,6 +188,15 @@ aside#left-menu.full-size(
|
||||
option(value="pdfjs") #{translate("built_in")}
|
||||
option(value="native") #{translate("native")}
|
||||
|
||||
if (getSessionUser() && getSessionUser().isAdmin && typeof(allowedImageNames) !== 'undefined' && allowedImageNames.length > 0)
|
||||
.form-controls(ng-show="permissions.write")
|
||||
label(for="imageName") #{translate("TeXLive")}
|
||||
select(
|
||||
name="imageName"
|
||||
ng-model="project.imageName"
|
||||
)
|
||||
each image in allowedImageNames
|
||||
option(value=image.imageName) #{image.imageDesc}
|
||||
|
||||
h4 #{translate("hotkeys")}
|
||||
ul.list-unstyled.nav
|
||||
|
||||
@@ -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'"
|
||||
)
|
||||
|
||||
|
||||
@@ -192,11 +192,14 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate')
|
||||
ng-click="cancel()"
|
||||
) ×
|
||||
h3(ng-if="action == 'delete'") #{translate("delete_projects")}
|
||||
h3(ng-if="action == 'archive'") #{translate("archive_projects")}
|
||||
h3(ng-if="action == 'leave'") #{translate("leave_projects")}
|
||||
h3(ng-if="action == 'delete-and-leave'") #{translate("delete_and_leave_projects")}
|
||||
h3(ng-if="action == 'archive-and-leave'") #{translate("archive_and_leave_projects")}
|
||||
.modal-body
|
||||
div(ng-show="projectsToDelete.length > 0")
|
||||
p #{translate("about_to_delete_projects")}
|
||||
p(ng-if="action == 'delete' || action == 'delete-and-leave'") #{translate("about_to_delete_projects")}
|
||||
p(ng-if="action == 'archive' || action == 'archive-and-leave'") #{translate("about_to_archive_projects")}
|
||||
ul
|
||||
li(ng-repeat="project in projectsToDelete | orderBy:'name'")
|
||||
strong {{project.name}}
|
||||
@@ -345,13 +348,11 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
|
||||
i.fa.fa-flask
|
||||
.v1-import-col
|
||||
h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental
|
||||
p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2 there is:
|
||||
p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2:
|
||||
ul
|
||||
li <strong>No Journals and Services</strong> menu to submit directly to our partners yet
|
||||
li <strong>No Rich Text (WYSIWYG)</strong> mode yet
|
||||
li <strong>No linked files</strong> (to URLs or to files in other Overleaf projects) yet
|
||||
li <strong>No Zotero and CiteULike</strong> integrations yet
|
||||
li <strong>No labelled versions</strong> yet
|
||||
li You may not be able to access all of your <strong>Labelled versions</strong> yet
|
||||
li There are <strong>no Zotero and CiteULike</strong> integrations yet
|
||||
li Some <strong>Journals and Services in the Submit menu</strong> don't support direct submissions yet
|
||||
p.row-spaced-small
|
||||
| If you currently use the <strong>Overleaf Git bridge</strong> with your v1 project, you can migrate your project to the Overleaf v2 GitHub integration.
|
||||
|
|
||||
|
||||
@@ -14,5 +14,5 @@ block content
|
||||
.content.plans(ng-controller="PlansController")
|
||||
.container(class="more-details" ng-cloak ng-if="plansVariant === 'more-details'")
|
||||
include _plans_page_details_more
|
||||
.container(ng-cloak ng-if="plansVariant === 'default' || !shouldABTestPlans")
|
||||
.container(ng-cloak ng-if="plansVariant === 'default' || !shouldABTestPlans || timeout")
|
||||
include _plans_page_details_less
|
||||
|
||||
@@ -9,7 +9,7 @@ block content
|
||||
.page-header
|
||||
h1 #{translate("account_settings")}
|
||||
.account-settings(ng-controller="AccountSettingsController", ng-cloak)
|
||||
if locals.showAffiliationsUI && 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")}
|
||||
|
||||
|
||||
@@ -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'}
|
||||
# ]
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
define [
|
||||
"base"
|
||||
"ide/colors/ColorManager"
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
historyEntryController = ($scope, $element, $attrs) ->
|
||||
], (App, ColorManager, displayNameForUser) ->
|
||||
historyEntryController = ($scope, $element, $attrs, _) ->
|
||||
ctrl = @
|
||||
# This method (and maybe the one below) will be removed soon. User details data will be
|
||||
# injected into the history API responses, so we won't need to fetch user data from other
|
||||
# local data structures.
|
||||
_getUserById = (id) ->
|
||||
_.find ctrl.users, (user) ->
|
||||
curUserId = user?._id or user?.id
|
||||
curUserId == id
|
||||
ctrl.displayName = displayNameForUser
|
||||
ctrl.displayNameById = (id) ->
|
||||
displayNameForUser(_getUserById(id))
|
||||
ctrl.getProjectOpDoc = (projectOp) ->
|
||||
if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }"
|
||||
else if projectOp.add? then "#{ projectOp.add.pathname}"
|
||||
else if projectOp.remove? then "#{ projectOp.remove.pathname}"
|
||||
ctrl.getUserCSSStyle = (user) ->
|
||||
hue = user?.hue or 100
|
||||
curUserId = user?._id or user?.id
|
||||
hue = ColorManager.getHueForUserId(curUserId) or 100
|
||||
if ctrl.entry.inSelection
|
||||
color : "#FFF"
|
||||
else
|
||||
color: "hsl(#{ hue }, 70%, 50%)"
|
||||
ctrl.$onInit = () ->
|
||||
ctrl.historyEntriesList.onEntryLinked ctrl.entry, $element.find "> .history-entry"
|
||||
return
|
||||
|
||||
App.component "historyEntry", {
|
||||
bindings:
|
||||
entry: "<"
|
||||
currentUser: "<"
|
||||
users: "<"
|
||||
onSelect: "&"
|
||||
onLabelDelete: "&"
|
||||
require:
|
||||
historyEntriesList: '^historyEntriesList'
|
||||
controller: historyEntryController
|
||||
templateUrl: "historyEntryTpl"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
historyLabelController = ($scope, $element, $attrs, $filter, _) ->
|
||||
ctrl = @
|
||||
ctrl.$onInit = () ->
|
||||
ctrl.showTooltip ?= true
|
||||
return
|
||||
|
||||
App.component "historyLabel", {
|
||||
bindings:
|
||||
labelText: "<"
|
||||
labelOwnerName: "<?"
|
||||
labelCreationDateTime: "<?"
|
||||
isOwnedByCurrentUser: "<"
|
||||
onLabelDelete: "&"
|
||||
showTooltip: "<?"
|
||||
controller: historyLabelController
|
||||
templateUrl: "historyLabelTpl"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
define [
|
||||
"base"
|
||||
"ide/colors/ColorManager"
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, ColorManager, displayNameForUser) ->
|
||||
historyLabelsListController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
# This method (and maybe the one below) will be removed soon. User details data will be
|
||||
# injected into the history API responses, so we won't need to fetch user data from other
|
||||
# local data structures.
|
||||
ctrl.getUserById = (id) ->
|
||||
_.find ctrl.users, (user) ->
|
||||
curUserId = user?._id or user?.id
|
||||
curUserId == id
|
||||
ctrl.displayName = displayNameForUser
|
||||
ctrl.getUserCSSStyle = (user, label) ->
|
||||
curUserId = user?._id or user?.id
|
||||
hue = ColorManager.getHueForUserId(curUserId) or 100
|
||||
if label.id == ctrl.selectedLabel?.id
|
||||
color : "#FFF"
|
||||
else
|
||||
color: "hsl(#{ hue }, 70%, 50%)"
|
||||
return
|
||||
|
||||
App.component "historyLabelsList", {
|
||||
bindings:
|
||||
labels: "<"
|
||||
users: "<"
|
||||
currentUser: "<"
|
||||
isLoading: "<"
|
||||
selectedLabel: "<"
|
||||
onLabelSelect: "&"
|
||||
onLabelDelete: "&"
|
||||
controller: historyLabelsListController
|
||||
templateUrl: "historyLabelsListTpl"
|
||||
}
|
||||
@@ -3,9 +3,34 @@ define [
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
|
||||
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
|
||||
App.controller "HistoryListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
|
||||
|
||||
$scope.projectUsers = []
|
||||
|
||||
$scope.$watch "project.members", (newVal) ->
|
||||
if newVal?
|
||||
$scope.projectUsers = newVal.concat $scope.project.owner
|
||||
|
||||
# This method (and maybe the one below) will be removed soon. User details data will be
|
||||
# injected into the history API responses, so we won't need to fetch user data from other
|
||||
# local data structures.
|
||||
_getUserById = (id) ->
|
||||
_.find $scope.projectUsers, (user) ->
|
||||
curUserId = user?._id or user?.id
|
||||
curUserId == id
|
||||
|
||||
$scope.getDisplayNameById = (id) ->
|
||||
displayNameForUser(_getUserById(id))
|
||||
|
||||
$scope.deleteLabel = (labelDetails) ->
|
||||
$modal.open(
|
||||
templateUrl: "historyV2DeleteLabelModalTemplate"
|
||||
controller: "HistoryV2DeleteLabelModalController"
|
||||
resolve:
|
||||
labelDetails: () -> labelDetails
|
||||
)
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
define [
|
||||
"base",
|
||||
], (App) ->
|
||||
App.controller "HistoryV2AddLabelModalController", ["$scope", "$modalInstance", "ide", "update", ($scope, $modalInstance, ide, update) ->
|
||||
$scope.update = update
|
||||
$scope.inputs =
|
||||
labelName: null
|
||||
$scope.state =
|
||||
inflight: false
|
||||
error: false
|
||||
|
||||
$modalInstance.opened.then () ->
|
||||
$scope.$applyAsync () ->
|
||||
$scope.$broadcast "open"
|
||||
|
||||
$scope.addLabelModalFormSubmit = () ->
|
||||
$scope.state.inflight = true
|
||||
ide.historyManager.labelCurrentVersion $scope.inputs.labelName
|
||||
.then (response) ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
.catch (response) ->
|
||||
{ data, status } = response
|
||||
$scope.state.inflight = false
|
||||
if status == 400
|
||||
$scope.state.error = { message: data }
|
||||
else
|
||||
$scope.state.error = true
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
define [
|
||||
"base",
|
||||
], (App) ->
|
||||
App.controller "HistoryV2DeleteLabelModalController", ["$scope", "$modalInstance", "ide", "labelDetails", ($scope, $modalInstance, ide, labelDetails) ->
|
||||
$scope.labelDetails = labelDetails
|
||||
$scope.state =
|
||||
inflight: false
|
||||
error: false
|
||||
|
||||
$scope.deleteLabel = () ->
|
||||
$scope.state.inflight = true
|
||||
ide.historyManager.deleteLabel labelDetails
|
||||
.then (response) ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
.catch (response) ->
|
||||
{ data, status } = response
|
||||
$scope.state.inflight = false
|
||||
if status == 400
|
||||
$scope.state.error = { message: data }
|
||||
else
|
||||
$scope.state.error = true
|
||||
]
|
||||
@@ -3,74 +3,31 @@ define [
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
|
||||
App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) ->
|
||||
App.controller "HistoryV2ListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
$scope.listConfig =
|
||||
showOnlyLabelled: false
|
||||
|
||||
$scope.projectUsers = []
|
||||
|
||||
$scope.$watch "project.members", (newVal) ->
|
||||
if newVal?
|
||||
$scope.projectUsers = newVal.concat $scope.project.owner
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
$scope.handleEntrySelect = (entry) ->
|
||||
# $scope.$applyAsync () ->
|
||||
ide.historyManager.selectUpdate(entry)
|
||||
$scope.recalculateSelectedUpdates()
|
||||
|
||||
$scope.recalculateSelectedUpdates = () ->
|
||||
beforeSelection = true
|
||||
afterSelection = false
|
||||
$scope.history.selection.updates = []
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
inSelection = true
|
||||
beforeSelection = false
|
||||
|
||||
update.beforeSelection = beforeSelection
|
||||
update.inSelection = inSelection
|
||||
update.afterSelection = afterSelection
|
||||
|
||||
if inSelection
|
||||
$scope.history.selection.updates.push update
|
||||
|
||||
if update.selectedFrom
|
||||
inSelection = false
|
||||
afterSelection = true
|
||||
|
||||
$scope.recalculateHoveredUpdates = () ->
|
||||
hoverSelectedFrom = false
|
||||
hoverSelectedTo = false
|
||||
for update in $scope.history.updates
|
||||
# Figure out whether the to or from selector is hovered over
|
||||
if update.hoverSelectedFrom
|
||||
hoverSelectedFrom = true
|
||||
if update.hoverSelectedTo
|
||||
hoverSelectedTo = true
|
||||
|
||||
if hoverSelectedFrom
|
||||
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
|
||||
inHoverSelection = false
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
update.hoverSelectedTo = true
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
if update.hoverSelectedFrom
|
||||
inHoverSelection = false
|
||||
if hoverSelectedTo
|
||||
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
|
||||
inHoverSelection = false
|
||||
for update in $scope.history.updates
|
||||
if update.hoverSelectedTo
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
if update.selectedFrom
|
||||
update.hoverSelectedFrom = true
|
||||
inHoverSelection = false
|
||||
|
||||
$scope.resetHoverState = () ->
|
||||
for update in $scope.history.updates
|
||||
delete update.hoverSelectedFrom
|
||||
delete update.hoverSelectedTo
|
||||
delete update.inHoverSelection
|
||||
|
||||
$scope.$watch "history.updates.length", () ->
|
||||
$scope.recalculateSelectedUpdates()
|
||||
$scope.handleLabelSelect = (label) ->
|
||||
ide.historyManager.selectLabel(label)
|
||||
|
||||
$scope.handleLabelDelete = (labelDetails) ->
|
||||
$modal.open(
|
||||
templateUrl: "historyV2DeleteLabelModalTemplate"
|
||||
controller: "HistoryV2DeleteLabelModalController"
|
||||
resolve:
|
||||
labelDetails: () -> labelDetails
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
define [
|
||||
"base",
|
||||
], (App) ->
|
||||
App.controller "HistoryV2ToolbarController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
|
||||
$scope.showAddLabelDialog = () ->
|
||||
$modal.open(
|
||||
templateUrl: "historyV2AddLabelModalTemplate"
|
||||
controller: "HistoryV2AddLabelModalController"
|
||||
resolve:
|
||||
update: () -> $scope.history.selection.updates[0]
|
||||
)
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -3,11 +3,13 @@ define [
|
||||
], (App) ->
|
||||
# We create and provide this as service so that we can access the global ide
|
||||
# from within other parts of the angular app.
|
||||
App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", ($http, queuedHttp, $modal, $q) ->
|
||||
App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", "$filter", "$timeout", ($http, queuedHttp, $modal, $q, $filter, $timeout) ->
|
||||
ide = {}
|
||||
ide.$http = $http
|
||||
ide.queuedHttp = queuedHttp
|
||||
ide.$q = $q
|
||||
ide.$filter = $filter
|
||||
ide.$timeout = $timeout
|
||||
|
||||
@recentEvents = []
|
||||
ide.pushEvent = (type, meta = {}) =>
|
||||
|
||||
@@ -67,6 +67,11 @@ define [
|
||||
if oldCompiler? and compiler != oldCompiler
|
||||
settings.saveProjectSettings({compiler: compiler})
|
||||
|
||||
$scope.$watch "project.imageName", (imageName, oldImageName) =>
|
||||
return if @ignoreUpdates
|
||||
if oldImageName? and imageName != oldImageName
|
||||
settings.saveProjectSettings({imageName: imageName})
|
||||
|
||||
$scope.$watch "project.rootDoc_id", (rootDoc_id, oldRootDoc_id) =>
|
||||
return if @ignoreUpdates
|
||||
# don't save on initialisation, Angular passes oldRootDoc_id as
|
||||
@@ -83,6 +88,12 @@ define [
|
||||
$scope.project.compiler = compiler
|
||||
delete @ignoreUpdates
|
||||
|
||||
ide.socket.on "imageNameUpdated", (imageName) =>
|
||||
@ignoreUpdates = true
|
||||
$scope.$apply () =>
|
||||
$scope.project.imageName = imageName
|
||||
delete @ignoreUpdates
|
||||
|
||||
ide.socket.on "spellCheckLanguageUpdated", (languageCode) =>
|
||||
@ignoreUpdates = true
|
||||
$scope.$apply () =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,20 +4,21 @@ define [
|
||||
"libs/recurly-4.8.5"
|
||||
], (App)->
|
||||
|
||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
|
||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils, ipCookie)->
|
||||
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
|
||||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
$scope.plans = MultiCurrencyPricing.plans
|
||||
$scope.planCode = window.plan_code
|
||||
$scope.plansVariant = ipCookie('plansVariant')
|
||||
|
||||
$scope.switchToStudent = ()->
|
||||
currentPlanCode = window.plan_code
|
||||
planCode = currentPlanCode.replace('collaborator', 'student')
|
||||
event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code }
|
||||
event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code, variant: $scope.plansVariant }
|
||||
window.location = "/user/subscription/new?planCode=#{planCode}¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
|
||||
|
||||
event_tracking.sendMB "subscription-form", { plan : window.plan_code }
|
||||
event_tracking.sendMB "subscription-form", { plan : window.plan_code, variant: $scope.plansVariant }
|
||||
|
||||
$scope.paymentMethod =
|
||||
value: "credit_card"
|
||||
@@ -143,13 +144,14 @@ define [
|
||||
currencyCode : postData.subscriptionDetails.currencyCode,
|
||||
plan_code : postData.subscriptionDetails.plan_code,
|
||||
coupon_code : postData.subscriptionDetails.coupon_code,
|
||||
isPaypal : postData.subscriptionDetails.isPaypal
|
||||
isPaypal : postData.subscriptionDetails.isPaypal,
|
||||
variant : $scope.plansVariant
|
||||
}
|
||||
|
||||
|
||||
$http.post("/user/subscription/create", postData)
|
||||
.then ()->
|
||||
event_tracking.sendMB "subscription-submission-success"
|
||||
event_tracking.sendMB "subscription-submission-success", { variant: $scope.plansVariant }
|
||||
window.location.href = "/user/subscription/thank-you"
|
||||
.catch ()->
|
||||
$scope.processing = false
|
||||
@@ -234,7 +236,4 @@ define [
|
||||
{code:'VU',name:'Vanuatu'},{code:'VA',name:'Vatican City'},{code:'VE',name:'Venezuela'},{code:'VN',name:'Vietnam'},
|
||||
{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
|
||||
{code:'ZM',name:'Zambia'},{code:'AX',name:'Åland Islandscode:'}
|
||||
]
|
||||
|
||||
sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
||||
]
|
||||
@@ -145,15 +145,21 @@ define [
|
||||
}
|
||||
|
||||
|
||||
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter) ->
|
||||
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter, ipCookie) ->
|
||||
|
||||
$scope.showPlans = false
|
||||
$scope.shouldABTestPlans = window.shouldABTestPlans
|
||||
|
||||
if $scope.shouldABTestPlans
|
||||
sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
||||
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
|
||||
if rawResponse?.status != 'failed'
|
||||
$scope.plansVariant = chosenVariation
|
||||
expiration = new Date();
|
||||
expiration.setDate(expiration.getDate() + 5);
|
||||
ipCookie('plansVariant', chosenVariation, {expires: expiration})
|
||||
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
|
||||
else
|
||||
$scope.timeout = true
|
||||
|
||||
$scope.showPlans = true
|
||||
|
||||
@@ -184,9 +190,9 @@ define [
|
||||
if $scope.ui.view == "annual"
|
||||
plan = "#{plan}_annual"
|
||||
plan = eventLabel(plan, location)
|
||||
event_tracking.sendMB 'plans-page-start-trial', {plan}
|
||||
event_tracking.sendMB 'plans-page-start-trial', {plan, variant: $scope.plansVariant}
|
||||
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
|
||||
if $scope.shouldABTestPlans
|
||||
if $scope.plansVariant
|
||||
sixpack.convert 'plans-details'
|
||||
|
||||
$scope.switchToMonthly = (e, location) ->
|
||||
|
||||
@@ -101,11 +101,19 @@ define [
|
||||
App.controller 'DeleteProjectsModalController', ($scope, $modalInstance, $timeout, projects) ->
|
||||
$scope.projectsToDelete = projects.filter (project) -> project.accessLevel == "owner"
|
||||
$scope.projectsToLeave = projects.filter (project) -> project.accessLevel != "owner"
|
||||
$scope.projectsToArchive = projects.filter (project) ->
|
||||
project.accessLevel == "owner" and !project.archived
|
||||
|
||||
if $scope.projectsToLeave.length > 0 and $scope.projectsToDelete.length > 0
|
||||
$scope.action = "delete-and-leave"
|
||||
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
|
||||
$scope.action = "archive-and-leave"
|
||||
else
|
||||
$scope.action = "delete-and-leave"
|
||||
else if $scope.projectsToLeave.length == 0 and $scope.projectsToDelete.length > 0
|
||||
$scope.action = "delete"
|
||||
if $scope.projectsToArchive.length > 0 and window.ExposedSettings.isOverleaf
|
||||
$scope.action = "archive"
|
||||
else
|
||||
$scope.action = "delete"
|
||||
else
|
||||
$scope.action = "leave"
|
||||
|
||||
|
||||
BIN
services/web/public/img/review-icon-sprite-ol.png
Normal file
BIN
services/web/public/img/review-icon-sprite-ol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 765 B |
BIN
services/web/public/img/review-icon-sprite-ol@2x.png
Normal file
BIN
services/web/public/img/review-icon-sprite-ol@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -43,6 +43,8 @@
|
||||
@import "components/hover.less";
|
||||
@import "components/ui-select.less";
|
||||
@import "components/input-suggestions.less";
|
||||
@import "components/nvd3.less";
|
||||
@import "components/nvd3_override.less";
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import "components/modals.less";
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**************************************
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.metric-tooltip {
|
||||
top: -1em;
|
||||
font-size: .5em;
|
||||
}
|
||||
}
|
||||
// END: Metrics header
|
||||
|
||||
|
||||
161
services/web/public/stylesheets/app/portals.less
Normal file
161
services/web/public/stylesheets/app/portals.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
9
services/web/public/stylesheets/components/icons.less
Normal file
9
services/web/public/stylesheets/components/icons.less
Normal file
@@ -0,0 +1,9 @@
|
||||
// Colors
|
||||
.icon-accent {
|
||||
color: @accent-color-secondary;
|
||||
}
|
||||
|
||||
// Sizes
|
||||
.icon-lg {
|
||||
font-size: @font-size-h1;
|
||||
}
|
||||
674
services/web/public/stylesheets/components/nvd3.less
Executable file
674
services/web/public/stylesheets/components/nvd3.less
Executable 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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.nvd3 {
|
||||
.nv-axis {
|
||||
.tick {
|
||||
line {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
path.domain {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg.nvd3-iddle {
|
||||
@extend svg.nvd3-svg;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
85
services/web/scripts/increase_compile_timeouts.js
Normal file
85
services/web/scripts/increase_compile_timeouts.js
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)=>
|
||||
|
||||
@@ -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", ->
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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([{
|
||||
|
||||
Reference in New Issue
Block a user