Merge branch 'master' into ns-no-duplicate-packages

This commit is contained in:
Nate Stemen 2018-01-05 11:46:44 -05:00
commit cf4d6c1165
80 changed files with 1533 additions and 629 deletions

View file

@ -39,6 +39,7 @@ data/*
app.js app.js
app/js/* app/js/*
test/unit/js/* test/unit/js/*
test/unit_frontend/js/*
test/smoke/js/* test/smoke/js/*
test/acceptance/js/* test/acceptance/js/*
cookies.txt cookies.txt
@ -67,6 +68,11 @@ public/minjs/
Gemfile.lock Gemfile.lock
public/stylesheets/ol-style.*.css
public/stylesheets/style.*.css
public/js/libs/require*.js
*.swp *.swp
.DS_Store .DS_Store

View file

@ -205,12 +205,12 @@ module.exports = (grunt) ->
modules: [ modules: [
{ {
name: "main", name: "main",
exclude: ["libs"] exclude: ["libraries"]
}, { }, {
name: "ide", name: "ide",
exclude: ["libs", "pdfjs-dist/build/pdf"] exclude: ["pdfjs-dist/build/pdf", "libraries"]
}, { },{
name: "libs" name: "libraries"
},{ },{
name: "ace/mode-latex" name: "ace/mode-latex"
},{ },{

View file

@ -60,7 +60,7 @@ pipeline {
sh 'git config --global core.logallrefupdates false' sh 'git config --global core.logallrefupdates false'
sh 'mv app/views/external/robots.txt public/robots.txt' sh 'mv app/views/external/robots.txt public/robots.txt'
sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html' sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html'
sh 'npm install' sh 'npm --quiet install'
sh 'npm rebuild' sh 'npm rebuild'
// It's too easy to end up shrinkwrapping to an outdated version of translations. // It's too easy to end up shrinkwrapping to an outdated version of translations.
// Ensure translations are always latest, regardless of shrinkwrap // Ensure translations are always latest, regardless of shrinkwrap
@ -71,16 +71,9 @@ pipeline {
} }
} }
stage('Unit Tests') { stage('Test') {
steps { steps {
sh 'make clean install' // Removes js files, so do before compile sh 'make ci'
sh 'make test_unit MOCHA_ARGS="--reporter=tap"'
}
}
stage('Acceptance Tests') {
steps {
sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"'
} }
} }
@ -155,6 +148,10 @@ pipeline {
} }
post { post {
always {
sh 'make ci_clean'
}
failure { failure {
mail(from: "${EMAIL_ALERT_FROM}", mail(from: "${EMAIL_ALERT_FROM}",
to: "${EMAIL_ALERT_TO}", to: "${EMAIL_ALERT_TO}",

View file

@ -16,9 +16,10 @@ add_dev: docker-shared.yml
$(NPM) install --save-dev ${P} $(NPM) install --save-dev ${P}
install: docker-shared.yml install: docker-shared.yml
bin/generate_volumes_file
$(NPM) install $(NPM) install
clean: clean: ci_clean
rm -f app.js rm -f app.js
rm -rf app/js rm -rf app/js
rm -rf test/unit/js rm -rf test/unit/js
@ -30,9 +31,8 @@ clean:
rm -rf $$dir/test/unit/js; \ rm -rf $$dir/test/unit/js; \
rm -rf $$dir/test/acceptance/js; \ rm -rf $$dir/test/acceptance/js; \
done done
# Regenerate docker-shared.yml - not stictly a 'clean',
# but lets `make clean install` work nicely ci_clean:
bin/generate_volumes_file
# Deletes node_modules volume # Deletes node_modules volume
docker-compose down --volumes docker-compose down --volumes
@ -40,11 +40,14 @@ clean:
docker-shared.yml: docker-shared.yml:
bin/generate_volumes_file bin/generate_volumes_file
test: test_unit test_acceptance test: test_unit test_frontend test_acceptance
test_unit: docker-shared.yml test_unit: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS} docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS}
test_frontend: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:frontend -- ${MOCHA_ARGS}
test_acceptance: test_acceptance_app test_acceptance_modules test_acceptance: test_acceptance_app test_acceptance_modules
test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service
@ -71,7 +74,11 @@ test_acceptance_modules: docker-shared.yml
test_acceptance_module: docker-shared.yml test_acceptance_module: docker-shared.yml
cd $(MODULE) && make test_acceptance cd $(MODULE) && make test_acceptance
ci:
MOCHA_ARGS="--reporter tap" \
$(MAKE) install test
.PHONY: .PHONY:
all add install update test test_unit test_acceptance \ all add install update test test_unit test_frontend test_acceptance \
test_acceptance_start_service test_acceptance_stop_service \ test_acceptance_start_service test_acceptance_stop_service \
test_acceptance_run test_acceptance_run ci ci_clean

View file

@ -205,10 +205,10 @@ module.exports = DocumentUpdaterHandler =
callback new Error("doc updater returned a non-success status code: #{res.statusCode}") callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
updateProjectStructure : (project_id, userId, changes, callback = (error) ->)-> updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
return callback() if !settings.apis.project_history?.enabled return callback() if !settings.apis.project_history?.sendProjectStructureOps
docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', changes.oldDocs, changes.newDocs) docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles) fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
timer = new metrics.Timer("set-document") timer = new metrics.Timer("set-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}" url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
@ -230,7 +230,7 @@ module.exports = DocumentUpdaterHandler =
logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}" logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}") callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
_getRenameUpdates: (entityType, oldEntities, newEntities) -> _getUpdates: (entityType, oldEntities, newEntities) ->
oldEntities ||= [] oldEntities ||= []
newEntities ||= [] newEntities ||= []
updates = [] updates = []
@ -255,6 +255,16 @@ module.exports = DocumentUpdaterHandler =
pathname: oldEntity.path pathname: oldEntity.path
newPathname: newEntity.path newPathname: newEntity.path
for id, oldEntity of oldEntitiesHash
newEntity = newEntitiesHash[id]
if !newEntity?
# entity deleted
updates.push
id: id
pathname: oldEntity.path
newPathname: ''
updates updates
PENDINGUPDATESKEY = "PendingUpdates" PENDINGUPDATESKEY = "PendingUpdates"

View file

@ -105,19 +105,19 @@ module.exports = EditorController =
async.series jobs, (err)-> async.series jobs, (err)->
callback err, newFolders, lastFolder callback err, newFolders, lastFolder
deleteEntity : (project_id, entity_id, entityType, source, callback)-> deleteEntity : (project_id, entity_id, entityType, source, userId, callback)->
LockManager.getLock project_id, (err)-> LockManager.getLock project_id, (err)->
if err? if err?
logger.err err:err, project_id:project_id, "could not get lock to deleteEntity" logger.err err:err, project_id:project_id, "could not get lock to deleteEntity"
return callback(err) return callback(err)
EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, (err)-> EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, userId, (err)->
LockManager.releaseLock project_id, ()-> LockManager.releaseLock project_id, ()->
callback(err) callback(err)
deleteEntityWithoutLock: (project_id, entity_id, entityType, source, callback)-> deleteEntityWithoutLock: (project_id, entity_id, entityType, source, userId, callback)->
logger.log {project_id, entity_id, entityType, source}, "start delete process of entity" logger.log {project_id, entity_id, entityType, source}, "start delete process of entity"
Metrics.inc "editor.delete-entity" Metrics.inc "editor.delete-entity"
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, (err)-> ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, userId, (err)->
if err? if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity" logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity"
return callback(err) return callback(err)

View file

@ -147,6 +147,7 @@ module.exports = EditorHttpController =
project_id = req.params.Project_id project_id = req.params.Project_id
entity_id = req.params.entity_id entity_id = req.params.entity_id
entity_type = req.params.entity_type entity_type = req.params.entity_type
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) -> user_id = AuthenticationController.getLoggedInUserId(req)
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", user_id, (error) ->
return next(error) if error? return next(error) if error?
res.sendStatus 204 res.sendStatus 204

View file

@ -26,8 +26,16 @@ InvalidNameError = (message) ->
return error return error
InvalidNameError.prototype.__proto__ = Error.prototype InvalidNameError.prototype.__proto__ = Error.prototype
UnsupportedFileTypeError = (message) ->
error = new Error(message)
error.name = "UnsupportedFileTypeError"
error.__proto__ = UnsupportedFileTypeError.prototype
return error
UnsupportedFileTypeError.prototype.__proto___ = Error.prototype
module.exports = Errors = module.exports = Errors =
NotFoundError: NotFoundError NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError ServiceNotConfiguredError: ServiceNotConfiguredError
TooManyRequestsError: TooManyRequestsError TooManyRequestsError: TooManyRequestsError
InvalidNameError: InvalidNameError InvalidNameError: InvalidNameError
UnsupportedFileTypeError: UnsupportedFileTypeError

View file

@ -5,35 +5,13 @@ AuthenticationController = require "../Authentication/AuthenticationController"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler" ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
module.exports = HistoryController = module.exports = HistoryController =
initializeProject: (callback = (error, history_id) ->) ->
return callback() if !settings.apis.project_history?.enabled
request.post {
url: "#{settings.apis.project_history.url}/project"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
try
project = JSON.parse(body)
catch error
return callback(error)
overleaf_id = project?.project?.id
if !overleaf_id
error = new Error("project-history did not provide an id", project)
return callback(error)
callback null, { overleaf_id }
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error
selectHistoryApi: (req, res, next = (error) ->) -> selectHistoryApi: (req, res, next = (error) ->) ->
project_id = req.params?.Project_id project_id = req.params?.Project_id
# find out which type of history service this project uses # find out which type of history service this project uses
ProjectDetailsHandler.getDetails project_id, (err, project) -> ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err? return next(err) if err?
if project?.overleaf?.history?.display history = project.overleaf?.history
if history?.id? and history?.display
req.useProjectHistory = true req.useProjectHistory = true
else else
req.useProjectHistory = false req.useProjectHistory = false
@ -58,7 +36,7 @@ module.exports = HistoryController =
buildHistoryServiceUrl: (useProjectHistory) -> buildHistoryServiceUrl: (useProjectHistory) ->
# choose a history service, either document-level (trackchanges) # choose a history service, either document-level (trackchanges)
# or project-level (project_history) # or project-level (project_history)
if settings.apis.project_history?.enabled && useProjectHistory if useProjectHistory
return settings.apis.project_history.url return settings.apis.project_history.url
else else
return settings.apis.trackchanges.url return settings.apis.trackchanges.url

View file

@ -0,0 +1,26 @@
request = require "request"
settings = require "settings-sharelatex"
module.exports = HistoryManager =
initializeProject: (callback = (error, history_id) ->) ->
return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects
request.post {
url: "#{settings.apis.project_history.url}/project"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
try
project = JSON.parse(body)
catch error
return callback(error)
overleaf_id = project?.project?.id
if !overleaf_id
error = new Error("project-history did not provide an id", project)
return callback(error)
callback null, { overleaf_id }
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error

View file

@ -216,7 +216,7 @@ module.exports = ProjectController =
project: (cb)-> project: (cb)->
ProjectGetter.getProject( ProjectGetter.getProject(
project_id, project_id,
{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1 }, { name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, 'overleaf.history.display': 1 },
cb cb
) )
user: (cb)-> user: (cb)->
@ -253,7 +253,7 @@ module.exports = ProjectController =
# Extract data from user's ObjectId # Extract data from user's ObjectId
timestamp = parseInt(user_id.toString().substring(0, 8), 16) timestamp = parseInt(user_id.toString().substring(0, 8), 16)
rolloutPercentage = 40 # Percentage of users to roll out to rolloutPercentage = 60 # Percentage of users to roll out to
if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage) if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
# Don't show if user is not part of roll out # Don't show if user is not part of roll out
return cb(null, { enabled: false, showOnboarding: false }) return cb(null, { enabled: false, showOnboarding: false })
@ -351,6 +351,7 @@ module.exports = ProjectController =
themes: THEME_LIST themes: THEME_LIST
maxDocLength: Settings.max_doc_length maxDocLength: Settings.max_doc_length
showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding
useV2History: !!project.overleaf?.history?.display
timer.done() timer.done()
_buildProjectList: (allProjects, v1Projects = [])-> _buildProjectList: (allProjects, v1Projects = [])->

View file

@ -7,7 +7,7 @@ Project = require('../../models/Project').Project
Folder = require('../../models/Folder').Folder Folder = require('../../models/Folder').Folder
ProjectEntityHandler = require('./ProjectEntityHandler') ProjectEntityHandler = require('./ProjectEntityHandler')
ProjectDetailsHandler = require('./ProjectDetailsHandler') ProjectDetailsHandler = require('./ProjectDetailsHandler')
HistoryController = require('../History/HistoryController') HistoryManager = require('../History/HistoryManager')
User = require('../../models/User').User User = require('../../models/User').User
fs = require('fs') fs = require('fs')
Path = require "path" Path = require "path"
@ -27,7 +27,7 @@ module.exports = ProjectCreationHandler =
if projectHistoryId? if projectHistoryId?
ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, callback ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, callback
else else
HistoryController.initializeProject (error, history) -> HistoryManager.initializeProject (error, history) ->
return callback(error) if error? return callback(error) if error?
ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, callback ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, callback

View file

@ -21,7 +21,7 @@ module.exports = ProjectDuplicator =
if !doc?._id? if !doc?._id?
return callback() return callback()
content = docContents[doc._id.toString()] content = docContents[doc._id.toString()]
projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)-> projectEntityHandler.addDoc newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)->
if err? if err?
logger.err err:err, "error copying doc" logger.err err:err, "error copying doc"
return callback(err) return callback(err)

View file

@ -149,14 +149,35 @@ module.exports = ProjectEntityHandler =
else else
DocstoreManager.getDoc project_id, doc_id, options, callback DocstoreManager.getDoc project_id, doc_id, options, callback
addDoc: (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> addDoc: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) -> ProjectEntityHandler.addDocWithoutUpdatingHistory project_or_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
return callback(error) if error?
newDocs = [
doc: doc
path: path
docLines: docLines.join('\n')
]
project_id = project_or_id._id or project_or_id
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
return callback(error) if error?
callback null, doc, folder_id
addDocWithoutUpdatingHistory: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
# This method should never be called directly, except when importing a project
# from Overleaf. It skips sending updates to the project history, which will break
# the history unless you are making sure it is updated in some other way.
getProject = (cb) ->
if project_or_id._id? # project
return cb(null, project_or_id)
else # id
return ProjectGetter.getProjectWithOnlyFolders project_or_id, cb
getProject (error, project) ->
if err? if err?
logger.err project_id:project_id, err:err, "error getting project for add doc" logger.err project_id:project_id, err:err, "error getting project for add doc"
return callback(err) return callback(err)
ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, userId, callback ProjectEntityHandler._addDocWithProject project, folder_id, docName, docLines, userId, callback
addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=> _addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id, path) ->)=>
project_id = project._id project_id = project._id
logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project" logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
confirmFolder project, folder_id, (folder_id)=> confirmFolder project, folder_id, (folder_id)=>
@ -176,14 +197,7 @@ module.exports = ProjectEntityHandler =
rev: 0 rev: 0
}, (err) -> }, (err) ->
return callback(err) if err? return callback(err) if err?
newDocs = [ callback(null, doc, folder_id, result?.path?.fileSystem)
doc: doc
path: result?.path?.fileSystem
docLines: docLines.join('\n')
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
return callback(error) if error?
callback null, doc, folder_id
restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) -> restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
# getDoc will return the deleted doc's lines, but we don't actually remove # getDoc will return the deleted doc's lines, but we don't actually remove
@ -192,37 +206,37 @@ module.exports = ProjectEntityHandler =
return callback(error) if error? return callback(error) if error?
ProjectEntityHandler.addDoc project_id, null, name, lines, callback ProjectEntityHandler.addDoc project_id, null, name, lines, callback
addFile: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)-> addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)->
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) -> ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
if err? if err?
logger.err project_id:project_id, err:err, "error getting project for add file" logger.err project_id:project_id, err:err, "error getting project for add file"
return callback(err) return callback(err)
ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, userId, callback logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
return callback(err) if err?
addFileWithProject: (project, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)-> confirmFolder project, folder_id, (folder_id)->
project_id = project._id fileRef = new File name : fileName
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file" FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)->
fileRef = new File name : fileName
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err)
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
if err? if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project" logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err) return callback(err)
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) -> ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
return callback(err) if err? if err?
newFiles = [ logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
file: fileRef return callback(err)
path: result?.path?.fileSystem tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
url: fileStoreUrl return callback(err) if err?
] callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
return callback(error) if error? addFile: (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id) ->)->
callback null, fileRef, folder_id ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, fsPath, userId, (error, fileRef, folder_id, path, fileStoreUrl) ->
newFiles = [
file: fileRef
path: path
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
replaceFile: (project_id, file_id, fsPath, userId, callback)-> replaceFile: (project_id, file_id, fsPath, userId, callback)->
self = ProjectEntityHandler self = ProjectEntityHandler
@ -412,7 +426,7 @@ module.exports = ProjectEntityHandler =
callback() callback()
deleteEntity: (project_id, entity_id, entityType, callback = (error) ->)-> deleteEntity: (project_id, entity_id, entityType, userId, callback = (error) ->)->
self = @ self = @
logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity"
if !entityType? if !entityType?
@ -423,7 +437,7 @@ module.exports = ProjectEntityHandler =
return callback(error) if error? return callback(error) if error?
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
return callback(error) if error? return callback(error) if error?
ProjectEntityHandler._cleanUpEntity project, entity, entityType, (error) -> ProjectEntityHandler._cleanUpEntity project, entity, entityType, path.fileSystem, userId, (error) ->
return callback(error) if error? return callback(error) if error?
tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) -> tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) ->
return callback(error) if error? return callback(error) if error?
@ -456,17 +470,17 @@ module.exports = ProjectEntityHandler =
return callback(error) if error? return callback(error) if error?
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback
_cleanUpEntity: (project, entity, entityType, callback = (error) ->) -> _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
if(entityType.indexOf("file") != -1) if(entityType.indexOf("file") != -1)
ProjectEntityHandler._cleanUpFile project, entity, callback ProjectEntityHandler._cleanUpFile project, entity, path, userId, callback
else if (entityType.indexOf("doc") != -1) else if (entityType.indexOf("doc") != -1)
ProjectEntityHandler._cleanUpDoc project, entity, callback ProjectEntityHandler._cleanUpDoc project, entity, path, userId, callback
else if (entityType.indexOf("folder") != -1) else if (entityType.indexOf("folder") != -1)
ProjectEntityHandler._cleanUpFolder project, entity, callback ProjectEntityHandler._cleanUpFolder project, entity, path, userId, callback
else else
callback() callback()
_cleanUpDoc: (project, doc, callback = (error) ->) -> _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
project_id = project._id.toString() project_id = project._id.toString()
doc_id = doc._id.toString() doc_id = doc._id.toString()
unsetRootDocIfRequired = (callback) => unsetRootDocIfRequired = (callback) =>
@ -483,26 +497,33 @@ module.exports = ProjectEntityHandler =
return callback(error) if error? return callback(error) if error?
DocstoreManager.deleteDoc project_id, doc_id, (error) -> DocstoreManager.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error? return callback(error) if error?
callback() changes = oldDocs: [ {doc, path} ]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
_cleanUpFile: (project, file, callback = (error) ->) -> _cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
project_id = project._id.toString() project_id = project._id.toString()
file_id = file._id.toString() file_id = file._id.toString()
FileStoreHandler.deleteFile project_id, file_id, callback FileStoreHandler.deleteFile project_id, file_id, (error) ->
return callback(error) if error?
changes = oldFiles: [ {file, path} ]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
_cleanUpFolder: (project, folder, callback = (error) ->) -> _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
jobs = [] jobs = []
for doc in folder.docs for doc in folder.docs
do (doc) -> do (doc) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, callback docPath = path.join(folderPath, doc.name)
jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, docPath, userId, callback
for file in folder.fileRefs for file in folder.fileRefs
do (file) -> do (file) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, callback filePath = path.join(folderPath, file.name)
jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, filePath, userId, callback
for childFolder in folder.folders for childFolder in folder.folders
do (childFolder) -> do (childFolder) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, callback folderPath = path.join(folderPath, childFolder.name)
jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, folderPath, userId, callback
async.series jobs, callback async.series jobs, callback

View file

@ -62,6 +62,9 @@ module.exports = SubscriptionUpdater =
invited_emails: email invited_emails: email
}, callback }, callback
refreshSubscription: (user_id, callback=(err)->) ->
SubscriptionUpdater._setUsersMinimumFeatures user_id, callback
deleteSubscription: (subscription_id, callback = (error) ->) -> deleteSubscription: (subscription_id, callback = (error) ->) ->
SubscriptionLocator.getSubscription subscription_id, (err, subscription) -> SubscriptionLocator.getSubscription subscription_id, (err, subscription) ->
return callback(err) if err? return callback(err) if err?
@ -106,17 +109,29 @@ module.exports = SubscriptionUpdater =
SubscriptionLocator.getUsersSubscription user_id, cb SubscriptionLocator.getUsersSubscription user_id, cb
groupSubscription: (cb)-> groupSubscription: (cb)->
SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb
v1PlanCode: (cb) ->
Modules = require '../../infrastructure/Modules'
Modules.hooks.fire 'getV1PlanCode', user_id, (err, results) ->
cb(err, results?[0] || null)
async.series jobs, (err, results)-> async.series jobs, (err, results)->
if err? if err?
logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures" logger.err err:err, user_id:user_id,
"error getting subscription or group for _setUsersMinimumFeatures"
return callback(err) return callback(err)
{subscription, groupSubscription} = results {subscription, groupSubscription, v1PlanCode} = results
if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode # Group Subscription
logger.log user_id:user_id, "using users subscription plan code for features" if groupSubscription? and groupSubscription.planCode?
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
else if groupSubscription? and groupSubscription.planCode?
logger.log user_id:user_id, "using group which user is memor of for features" logger.log user_id:user_id, "using group which user is memor of for features"
UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback
# Personal Subscription
else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
logger.log user_id:user_id, "using users subscription plan code for features"
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
# V1 Subscription
else if v1PlanCode?
logger.log user_id: user_id, "using the V1 plan for features"
UserFeaturesUpdater.updateFeatures user_id, v1PlanCode, callback
# Default
else else
logger.log user_id:user_id, "using default features for user with no subscription or group" logger.log user_id:user_id, "using default features for user with no subscription or group"
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)-> UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)->

View file

@ -47,7 +47,7 @@ module.exports =
logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted" logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted"
return projectDeleter.markAsDeletedByExternalSource project._id, callback return projectDeleter.markAsDeletedByExternalSource project._id, callback
else else
updateMerger.deleteUpdate project._id, path, source, (err)-> updateMerger.deleteUpdate user_id, project._id, path, source, (err)->
callback(err) callback(err)

View file

@ -32,13 +32,13 @@ module.exports =
else else
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
deleteUpdate: (project_id, path, source, callback)-> deleteUpdate: (user_id, project_id, path, source, callback)->
projectLocator.findElementByPath project_id, path, (err, element, type)-> projectLocator.findElementByPath project_id, path, (err, element, type)->
if err? || !element? if err? || !element?
logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted" logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
return callback() return callback()
logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds" logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds"
editorController.deleteEntity project_id, element._id, type, source, (err)-> editorController.deleteEntity project_id, element._id, type, source, user_id, (err)->
logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds" logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds"
callback() callback()

View file

@ -12,7 +12,7 @@ Modules = require "./Modules"
Url = require "url" Url = require "url"
PackageVersions = require "./PackageVersions" PackageVersions = require "./PackageVersions"
htmlEncoder = new require("node-html-encoder").Encoder("numerical") htmlEncoder = new require("node-html-encoder").Encoder("numerical")
fingerprints = {} hashedFiles = {}
Path = require 'path' Path = require 'path'
Features = require "./Features" Features = require "./Features"
@ -30,43 +30,42 @@ getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}" filePath = Path.join __dirname, "../../../", "public#{filePath}"
exists = fs.existsSync filePath exists = fs.existsSync filePath
if exists if exists
content = fs.readFileSync filePath content = fs.readFileSync filePath, "UTF-8"
return content return content
else else
logger.log filePath:filePath, "file does not exist for fingerprints" logger.log filePath:filePath, "file does not exist for hashing"
return "" return ""
logger.log "Generating file fingerprints..."
pathList = [ pathList = [
["#{jsPath}libs/#{fineuploader}.js"] "#{jsPath}libs/require.js"
["#{jsPath}libs/require.js"] "#{jsPath}ide.js"
["#{jsPath}ide.js"] "#{jsPath}main.js"
["#{jsPath}main.js"] "#{jsPath}libraries.js"
["#{jsPath}libs.js"] "/stylesheets/style.css"
["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"] "/stylesheets/ol-style.css"
["#{jsPath}libs/#{pdfjs}/pdf.js"]
["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
["#{jsPath}libs/#{pdfjs}/compatibility.js"]
["/stylesheets/style.css"]
["/stylesheets/ol-style.css"]
] ]
for paths in pathList if !Settings.useMinifiedJs
contentList = _.map(paths, getFileContent) logger.log "not using minified JS, not hashing static files"
content = contentList.join("") else
hash = crypto.createHash("md5").update(content).digest("hex") logger.log "Generating file hashes..."
_.each paths, (filePath)-> for path in pathList
logger.log "#{filePath}: #{hash}" content = getFileContent(path)
fingerprints[filePath] = hash hash = crypto.createHash("md5").update(content).digest("hex")
splitPath = path.split("/")
filenameSplit = splitPath.pop().split(".")
filenameSplit.splice(filenameSplit.length-1, 0, hash)
splitPath.push(filenameSplit.join("."))
getFingerprint = (path) -> hashPath = splitPath.join("/")
if fingerprints[path]? hashedFiles[path] = hashPath
return fingerprints[path]
else
logger.err "No fingerprint for file: #{path}"
return ""
logger.log "Finished generating file fingerprints" fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
fs.writeFileSync(fsHashPath, content)
logger.log "Finished hashing static content"
cdnAvailable = Settings.cdn?.web?.host? cdnAvailable = Settings.cdn?.web?.host?
darkCdnAvailable = Settings.cdn?.web?.darkHost? darkCdnAvailable = Settings.cdn?.web?.darkHost?
@ -120,29 +119,35 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath) res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
res.locals.lib = PackageVersions.lib res.locals.lib = PackageVersions.lib
res.locals.buildJsPath = (jsFile, opts = {})-> res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile) path = Path.join(jsPath, jsFile)
doFingerPrint = opts.fingerprint != false if opts.hashedPath && hashedFiles[path]?
path = hashedFiles[path]
if !opts.qs? if !opts.qs?
opts.qs = {} opts.qs = {}
if !opts.qs?.fingerprint? and doFingerPrint
opts.qs.fingerprint = getFingerprint(path)
if opts.cdn != false if opts.cdn != false
path = Url.resolve(staticFilesBase, path) path = Url.resolve(staticFilesBase, path)
qs = querystring.stringify(opts.qs) qs = querystring.stringify(opts.qs)
if opts.removeExtension == true
path = path.slice(0,-3)
if qs? and qs.length > 0 if qs? and qs.length > 0
path = path + "?" + qs path = path + "?" + qs
return path return path
res.locals.buildCssPath = (cssFile)-> res.locals.buildCssPath = (cssFile, opts)->
path = Path.join("/stylesheets/", cssFile) path = Path.join("/stylesheets/", cssFile)
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path) if opts?.hashedPath && hashedFiles[path]?
hashedPath = hashedFiles[path]
return Url.resolve(staticFilesBase, hashedPath)
return Url.resolve(staticFilesBase, path)
res.locals.buildImgPath = (imgFile)-> res.locals.buildImgPath = (imgFile)->
path = Path.join("/img/", imgFile) path = Path.join("/img/", imgFile)
@ -227,10 +232,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
return req.query?[field] return req.query?[field]
next() next()
webRouter.use (req, res, next)->
res.locals.fingerprint = getFingerprint
next()
webRouter.use (req, res, next)-> webRouter.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice res.locals.formatPrice = SubscriptionFormatters.formatPrice
next() next()
@ -296,10 +297,14 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
webRouter.use (req, res, next) -> webRouter.use (req, res, next) ->
isOl = (Settings.brandPrefix == 'ol-') isOl = (Settings.brandPrefix == 'ol-')
res.locals.uiConfig = res.locals.uiConfig =
defaultResizerSizeOpen : if isOl then 2 else 24 defaultResizerSizeOpen : if isOl then 2 else 24
defaultResizerSizeClosed : if isOl then 2 else 24 defaultResizerSizeClosed : if isOl then 2 else 24
eastResizerCursor : if isOl then "ew-resize" else null eastResizerCursor : if isOl then "ew-resize" else null
westResizerCursor : if isOl then "ew-resize" else null westResizerCursor : if isOl then "ew-resize" else null
chatResizerSizeOpen : if isOl then 2 else 12 chatResizerSizeOpen : if isOl then 2 else 12
chatResizerSizeClosed : 0 chatResizerSizeClosed : 0
chatMessageBorderSaturation: if isOl then "85%" else "70%"
chatMessageBorderLightness : if isOl then "40%" else "70%"
chatMessageBgSaturation : if isOl then "85%" else "60%"
chatMessageBgLightness : if isOl then "40%" else "97%"
next() next()

View file

@ -56,6 +56,7 @@ ProjectSchema = new Schema
read_token : { type: String } read_token : { type: String }
history : history :
id : { type: Number } id : { type: Number }
display : { type: Boolean }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id? if project_or_id._id?

View file

@ -197,6 +197,7 @@ module.exports = class Router
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
@ -324,6 +325,10 @@ module.exports = class Router
headers: req.headers headers: req.headers
}) })
webRouter.get "/no-cache", (req, res, next)->
res.header("Cache-Control", "max-age=0")
res.sendStatus(404)
webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error"))
webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error")
webRouter.get '/oops-mongo', (req, res, next) -> webRouter.get '/oops-mongo', (req, res, next) ->

View file

@ -23,7 +23,7 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png") link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png")
link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529") link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529")
link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css")) link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css", {hashedPath:true}))
block _headLinks block _headLinks
@ -57,7 +57,7 @@ html(itemscope, itemtype='http://schema.org/Product')
script(type="text/javascript"). script(type="text/javascript").
window.csrfToken = "#{csrfToken}"; window.csrfToken = "#{csrfToken}";
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false})) script(src=buildJsPath("libs/jquery-1.11.1.min.js"))
script(type="text/javascript"). script(type="text/javascript").
var noCdnKey = "nocdn=true" var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined' var cdnBlocked = typeof jQuery === 'undefined'
@ -68,7 +68,7 @@ html(itemscope, itemtype='http://schema.org/Product')
block scripts block scripts
script(src=buildJsPath("libs/angular-1.6.4.min.js", {fingerprint:false})) script(src=buildJsPath("libs/angular-1.6.4.min.js"))
script. script.
window.sharelatex = { window.sharelatex = {
@ -95,7 +95,11 @@ html(itemscope, itemtype='http://schema.org/Product')
cdnDomain : '!{settings.templates.cdnDomain}', cdnDomain : '!{settings.templates.cdnDomain}',
indexName : '!{settings.templates.indexName}' indexName : '!{settings.templates.indexName}'
} }
- if (settings.overleaf && settings.overleaf.useOLFreeTrial)
script.
window.redirectToOLFreeTrialUrl = '!{settings.overleaf.host}/users/trial'
body body
if(settings.recaptcha) if(settings.recaptcha)
script(src="https://www.google.com/recaptcha/api.js?render=explicit") script(src="https://www.google.com/recaptcha/api.js?render=explicit")
@ -142,9 +146,10 @@ html(itemscope, itemtype='http://schema.org/Product')
window.requirejs = { window.requirejs = {
"paths" : { "paths" : {
"moment": "libs/#{lib('moment')}", "moment": "libs/#{lib('moment')}",
"fineuploader": "libs/#{lib('fineuploader')}" "fineuploader": "libs/#{lib('fineuploader')}",
"main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
}, },
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"config":{ "config":{
"moment":{ "moment":{
"noGlobal": true "noGlobal": true
@ -152,9 +157,9 @@ html(itemscope, itemtype='http://schema.org/Product')
} }
}; };
script( script(
data-main=buildJsPath('main.js', {fingerprint:false}), data-main=buildJsPath('main.js', {hashedPath:false}),
baseurl=fullJsPath, baseurl=fullJsPath,
src=buildJsPath('libs/require.js') src=buildJsPath('libs/require.js', {hashedPath:true})
) )
include contact-us-modal include contact-us-modal

View file

@ -98,14 +98,14 @@ block requirejs
script(type="text/javascript" src='/socket.io/socket.io.js') script(type="text/javascript" src='/socket.io/socket.io.js')
//- don't use cdn for workers //- don't use cdn for workers
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false}) - var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false})
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false}) - var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false})
- var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false,fingerprint:false}) - var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false})
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>' //- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
//- and doesn't prematurely end the script tag. //- and doesn't prematurely end the script tag.
script#data(type="application/json"). script#data(type="application/json").
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')} !{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History}).replace(/\//g, '\\/')}
script(type="text/javascript"). script(type="text/javascript").
window.data = JSON.parse($("#data").text()); window.data = JSON.parse($("#data").text());
@ -126,14 +126,15 @@ block requirejs
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = { window.requirejs = {
"paths" : { "paths" : {
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}", "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, qs:{config:'TeX-AMS_HTML'}})}",
"moment": "libs/#{lib('moment')}", "moment": "libs/#{lib('moment')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"ace": "#{lib('ace')}", "ace": "#{lib('ace')}",
"fineuploader": "libs/#{lib('fineuploader')}" "fineuploader": "libs/#{lib('fineuploader')}",
"ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
}, },
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0, "waitSeconds": 0,
"shim": { "shim": {
"pdfjs-dist/build/pdf": { "pdfjs-dist/build/pdf": {
@ -155,14 +156,13 @@ block requirejs
} }
} }
}; };
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
window.aceWorkerPath = "#{aceWorkerPath}"; window.aceWorkerPath = "#{aceWorkerPath}";
window.pdfCMapsPath = "#{pdfCMapsPath}" window.pdfCMapsPath = "#{pdfCMapsPath}"
window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
script( script(
data-main=buildJsPath("ide.js", {fingerprint:false}), data-main=buildJsPath("ide.js", {hashedPath:false}),
baseurl=fullJsPath, baseurl=fullJsPath,
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), data-ace-base=buildJsPath(lib('ace')),
src=buildJsPath('libs/require.js') src=buildJsPath('libs/require.js', {hashedPath:true})
) )

View file

@ -35,12 +35,9 @@ aside.chat(
span(ng-if="message.user.first_name") {{ message.user.first_name }} span(ng-if="message.user.first_name") {{ message.user.first_name }}
span(ng-if="!message.user.first_name") {{ message.user.email }} span(ng-if="!message.user.first_name") {{ message.user.email }}
.message( .message(
ng-style="{\ ng-style="getMessageStyle(message.user);"
'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)',\
'background-color': 'hsl({{ hue(message.user) }}, 60%, 97%)'\
}"
) )
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}") .arrow(ng-style="getArrowStyle(message.user)")
.message-content .message-content
p( p(
mathjax, mathjax,

View file

@ -134,8 +134,17 @@ div#history(ng-show="ui.view == 'history'")
div.description(ng-click="select()") div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
div.docs(ng-repeat="(doc_id, doc) in update.docs") div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
span.doc {{ doc.entity.name }} | Edited
div.docs(ng-repeat="pathname in update.pathnames")
.doc {{ pathname }}
div.docs(ng-repeat="project_op in update.project_ops")
div(ng-if="project_op.rename")
.action Renamed
.doc {{ project_op.rename.pathname }} &rarr; {{ project_op.rename.newPathname }}
div(ng-if="project_op.add")
.action Created
.doc {{ project_op.add.pathname }}
div.users div.users
div.user(ng-repeat="update_user in update.meta.users") div.user(ng-repeat="update_user in update.meta.users")
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}") .color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
@ -165,8 +174,8 @@ div#history(ng-show="ui.view == 'history'")
'other': 'changes'\ 'other': 'changes'\
}" }"
) )
| in <strong>{{history.diff.doc.name}}</strong> | in <strong>{{history.diff.pathname}}</strong>
.toolbar-right .toolbar-right(ng-if="!history.isV2")
a.btn.btn-danger.btn-sm( a.btn.btn-danger.btn-sm(
href, href,
ng-click="openRestoreDiffModal()" ng-click="openRestoreDiffModal()"

View file

@ -63,6 +63,14 @@ block content
aside.project-list-sidebar.col-md-2.col-xs-3 aside.project-list-sidebar.col-md-2.col-xs-3
include ./list/side-bar include ./list/side-bar
if isShowingV1Projects && settings.overleaf && settings.overleaf.host
.project-list-sidebar-v2-pane.col-md-2.col-xs-3
span Welcome to the Overleaf v2 alpha! #[a(href="https://www.overleaf.com/help/v2") Find out more].
span To tag or rename your v1 projects, please go back to Overleaf v1.
a.project-list-sidebar-v1-link(
href=settings.overleaf.host + "/dash?prefer-v1-dash=1"
) Go back to v1
.project-list-main.col-md-10.col-xs-9 .project-list-main.col-md-10.col-xs-9
include ./list/notifications include ./list/notifications
include ./list/project-list include ./list/project-list

View file

@ -2,6 +2,7 @@
input.select-item( input.select-item(
select-individual, select-individual,
type="checkbox", type="checkbox",
ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected" ng-model="project.selected"
stop-propagation="click" stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'" aria-label=translate('select_project') + " '{{ project.name }}'"

View file

@ -317,38 +317,56 @@ script(type="text/ng-template", id="userProfileModalTemplate")
script(type="text/ng-template", id="v1ImportModalTemplate") script(type="text/ng-template", id="v1ImportModalTemplate")
.modal-header .modal-header
button.close(ng-click="dismiss()") &times; button.close(ng-click="dismiss()") &times;
h3 #{translate("import_project_to_v2")} h3 Move Project to Overleaf v2
.modal-body.v1-import-wrapper .modal-body.v1-import-wrapper
.v1-import-step-1(ng-show="step === 1") .v1-import-step-1(ng-show="step === 1")
img.v1-import-img( .v1-import-row
src="/img/v1-import/v2-editor.png" .v1-import-col
alt="The new V2 Editor." img.v1-import-img(
) src="/img/v1-import/v2-editor.png"
h2.v1-import-title Try importing your project to V2! alt="The new V2 Editor."
p Some exciting copy about the new features: )
ul .v1-import-col
li Some stuff h2.v1-import-title Try the Overleaf v2 Editor
li Some more stuff p The Overleaf v2 editor has many great new features including:
li Yet more stuff ul
li Faster real-time collaboration
li See your coauthors cursors
li Chat with math support
li Tracked changes and commenting
li Improved LaTeX autocomplete
li Two-way Dropbox sync
p.v1-import-cta Would you like to move #[strong {{project.name}}] into Overleaf v2?
.v1-import-step-2(ng-show="step === 2") .v1-import-step-2(ng-show="step === 2")
div.v1-import-warning(aria-label="Warning symbol.") .v1-import-row
i.fa.fa-exclamation-triangle .v1-import-warning.v1-import-col(aria-label="Warning symbol.")
h2.v1-import-title #[strong Warning:] Overleaf V2 is in beta i.fa.fa-exclamation-triangle
p Once importing your project you will lose access to the some of the features of Overleaf V1. This includes the git bridge, journal integrations, WYSIWYG and linked files. Were working on bringing these features to V2! .v1-import-col
p Once you have imported a project to V2 you #[strong cannot go back to V1]. h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental
p Are you sure you want to import to V2? p We are still working hard to bring some Overleaf v1 features to the v2 editor. If you move this project to v2 now, you will:
ul
li Lose access your project via git
li Not be able to use the Journals and Services menu to submit directly to our partners
li Not be able to use the Rich Text (WYSIWYG) mode
li Not be able to use linked files (to URLs or to files in other Overleaf projects)
li Not be able to use some bibliography integrations (Zotero, CiteULike)
li Lose access to your labelled versions and not be able to create new labelled versions
.v1-import-cta
p
strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2.
p Are you sure you want to move #[strong {{project.name}}] into Overleaf v2?
.modal-footer.v1-import-footer .modal-footer.v1-import-footer
div(ng-show="step === 1") div(ng-show="step === 1")
if settings.overleaf && settings.overleaf.host if settings.overleaf && settings.overleaf.host
a.btn.btn-primary.v1-import-btn( a.btn.btn-primary.v1-import-btn(
ng-href=settings.overleaf.host + "/{{project.id}}" ng-href=settings.overleaf.host + "/{{project.id}}"
) #{translate("open_in_v1")} ) No thanks, open in v1
button.btn.btn-primary.v1-import-btn( button.btn.btn-primary.v1-import-btn(
ng-click="moveToConfirmation()" ng-click="moveToConfirmation()"
) #{translate("import_to_v2")} ) Yes, move project to v2
div(ng-show="step === 2") div(ng-show="step === 2")
form( form(
async-form="v1Import", async-form="v1Import",
@ -363,9 +381,9 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
a.btn.btn-primary.v1-import-btn( a.btn.btn-primary.v1-import-btn(
ng-href=settings.overleaf.host + "/{{project.id}}" ng-href=settings.overleaf.host + "/{{project.id}}"
ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}" ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}"
) #{translate("never_mind_open_in_v1")} ) No thanks, open in v1
input.btn.btn-primary.v1-import-btn( input.btn.btn-primary.v1-import-btn(
type="submit", type="submit",
value=translate('yes_im_sure') value="Yes, move project to v2"
ng-disabled="v1ImportForm.inflight || v1ImportForm.success" ng-disabled="v1ImportForm.inflight || v1ImportForm.success"
) )

View file

@ -210,7 +210,7 @@ block content
h3 #{translate("group_plan_enquiry")} h3 #{translate("group_plan_enquiry")}
.modal-body .modal-body
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak) form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
span(ng-show="sent == false") span(ng-show="sent == false && error == false")
.form-group .form-group
label#title9(for='Field9') label#title9(for='Field9')
| Name | Name
@ -228,11 +228,13 @@ block content
.form-group .form-group
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='') input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
.form-group .form-group
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';") input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
.form-group.text-center .form-group.text-center
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote') input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
span(ng-show="sent") span(ng-show="sent == true && error == false")
p Request Sent, Thank you. p Request Sent, Thank you.
span(ng-show="error")
p Error sending request.
.row .row
.col-md-12 .col-md-12

View file

@ -1,6 +1,6 @@
script(type="text/ng-template", id="v1ProjectTooltipTemplate") script(type="text/ng-template", id="v1ProjectTooltipTemplate")
span This project is from Overleaf version 1 and has not been imported to the beta yet span This project is from Overleaf v1 and has not been imported to v2 yet.
script(type="text/ng-template", id="v1TagTooltipTemplate") script(type="text/ng-template", id="v1TagTooltipTemplate")
span This folder is from Overleaf version 1 and has not been imported to the beta yet span This folder/tag is from Overleaf v1. Please go back to v1 to manage.

View file

@ -0,0 +1,5 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling public/coffee;
$COFFEE -o public/js -c public/coffee;

View file

@ -0,0 +1,5 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling test/unit_frontend/coffee;
$COFFEE -o test/unit_frontend/js -c test/unit_frontend/coffee;

5
services/web/bin/frontend_test Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
set -e;
MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
$MOCHA "$@" test/unit_frontend/js

View file

@ -111,7 +111,8 @@ module.exports = settings =
trackchanges: trackchanges:
url : "http://localhost:3015" url : "http://localhost:3015"
project_history: project_history:
enabled: process.env.PROJECT_HISTORY_ENABLED == 'true' or false sendProjectStructureOps: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
initializeHistoryForNewProjects: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
url : "http://localhost:3054" url : "http://localhost:3054"
docstore: docstore:
url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016" url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"

View file

@ -13,17 +13,15 @@ services:
- ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json - ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json
- node_modules:/app/node_modules - node_modules:/app/node_modules
- ./bin:/app/bin - ./bin:/app/bin
# Copying the whole public dir is fine for now, and needed for - ./public/coffee:/app/public/coffee:ro
# some unit tests to pass, but we will want to isolate the coffee - ./public/js/ace-1.2.5:/app/public/js/ace-1.2.5
# and vendor js files, so that the compiled js files are not written
# back to the local filesystem.
- ./public:/app/public
- ./app.coffee:/app/app.coffee:ro - ./app.coffee:/app/app.coffee:ro
- ./app/coffee:/app/app/coffee:ro - ./app/coffee:/app/app/coffee:ro
- ./app/templates:/app/app/templates:ro - ./app/templates:/app/app/templates:ro
- ./app/views:/app/app/views:ro - ./app/views:/app/app/views:ro
- ./config:/app/config - ./config:/app/config
- ./test/unit/coffee:/app/test/unit/coffee:ro - ./test/unit/coffee:/app/test/unit/coffee:ro
- ./test/unit_frontend/coffee:/app/test/unit_frontend/coffee:ro
- ./test/acceptance/coffee:/app/test/acceptance/coffee:ro - ./test/acceptance/coffee:/app/test/acceptance/coffee:ro
- ./test/acceptance/files:/app/test/acceptance/files:ro - ./test/acceptance/files:/app/test/acceptance/files:ro
- ./test/smoke/coffee:/app/test/smoke/coffee:ro - ./test/smoke/coffee:/app/test/smoke/coffee:ro

View file

@ -14,11 +14,15 @@
"test:acceptance:run": "bin/acceptance_test $@", "test:acceptance:run": "bin/acceptance_test $@",
"test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", "test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@",
"test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js", "test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js",
"test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@", "test:unit": "npm -q run compile:backend && npm -q run compile:unit_tests && bin/unit_test $@",
"test:frontend": "npm -q run compile:frontend && npm -q run compile:frontend_tests && bin/frontend_test $@",
"compile:unit_tests": "bin/compile_unit_tests", "compile:unit_tests": "bin/compile_unit_tests",
"compile:frontend_tests": "bin/compile_frontend_tests",
"compile:acceptance_tests": "bin/compile_acceptance_tests", "compile:acceptance_tests": "bin/compile_acceptance_tests",
"compile:app": "bin/compile_app", "compile:frontend": "bin/compile_frontend",
"start": "npm -q run compile:app && node app.js" "compile:backend": "bin/compile_backend",
"compile": "npm -q run compile:backend && npm -q run compile:frontend",
"start": "npm -q run compile && node app.js"
}, },
"dependencies": { "dependencies": {
"archiver": "0.9.0", "archiver": "0.9.0",

View file

@ -1,5 +1,5 @@
define [ define [
"libs" "libraries"
"modules/recursionHelper" "modules/recursionHelper"
"modules/errorCatcher" "modules/errorCatcher"
"modules/localStorage" "modules/localStorage"

View file

@ -49,18 +49,21 @@ define [
selectAllListController.clearSelectAllState() selectAllListController.clearSelectAllState()
scope.$on "select-all:select", () -> scope.$on "select-all:select", () ->
return if element.prop('disabled')
ignoreChanges = true ignoreChanges = true
scope.$apply () -> scope.$apply () ->
scope.ngModel = true scope.ngModel = true
ignoreChanges = false ignoreChanges = false
scope.$on "select-all:deselect", () -> scope.$on "select-all:deselect", () ->
return if element.prop('disabled')
ignoreChanges = true ignoreChanges = true
scope.$apply () -> scope.$apply () ->
scope.ngModel = false scope.ngModel = false
ignoreChanges = false ignoreChanges = false
scope.$on "select-all:row-clicked", () -> scope.$on "select-all:row-clicked", () ->
return if element.prop('disabled')
ignoreChanges = true ignoreChanges = true
scope.$apply () -> scope.$apply () ->
scope.ngModel = !scope.ngModel scope.ngModel = !scope.ngModel
@ -75,4 +78,4 @@ define [
link: (scope, element, attrs) -> link: (scope, element, attrs) ->
element.on "click", (e) -> element.on "click", (e) ->
scope.$broadcast "select-all:row-clicked" scope.$broadcast "select-all:row-clicked"
} }

View file

@ -5,6 +5,7 @@ define [
"ide/editor/EditorManager" "ide/editor/EditorManager"
"ide/online-users/OnlineUsersManager" "ide/online-users/OnlineUsersManager"
"ide/history/HistoryManager" "ide/history/HistoryManager"
"ide/history/HistoryV2Manager"
"ide/permissions/PermissionsManager" "ide/permissions/PermissionsManager"
"ide/pdf/PdfManager" "ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager" "ide/binary-files/BinaryFilesManager"
@ -44,6 +45,7 @@ define [
EditorManager EditorManager
OnlineUsersManager OnlineUsersManager
HistoryManager HistoryManager
HistoryV2Manager
PermissionsManager PermissionsManager
PdfManager PdfManager
BinaryFilesManager BinaryFilesManager
@ -137,7 +139,10 @@ define [
ide.fileTreeManager = new FileTreeManager(ide, $scope) ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(ide, $scope) ide.editorManager = new EditorManager(ide, $scope)
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope) ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
ide.historyManager = new HistoryManager(ide, $scope) if window.data.useV2History
ide.historyManager = new HistoryV2Manager(ide, $scope)
else
ide.historyManager = new HistoryManager(ide, $scope)
ide.pdfManager = new PdfManager(ide, $scope) ide.pdfManager = new PdfManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope) ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope) ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)

View file

@ -3,9 +3,22 @@ define [
"ide/colors/ColorManager" "ide/colors/ColorManager"
], (App, ColorManager) -> ], (App, ColorManager) ->
App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) -> App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) ->
$scope.hue = (user) -> hslColorConfigs =
borderSaturation: window.uiConfig?.chatMessageBorderSaturation or "70%"
borderLightness : window.uiConfig?.chatMessageBorderLightness or "70%"
bgSaturation : window.uiConfig?.chatMessageBgSaturation or "60%"
bgLightness : window.uiConfig?.chatMessageBgLightness or "97%"
hue = (user) ->
if !user? if !user?
return 0 return 0
else else
return ColorManager.getHueForUserId(user.id) return ColorManager.getHueForUserId(user.id)
$scope.getMessageStyle = (user) ->
"border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
"background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })"
$scope.getArrowStyle = (user) ->
"border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
] ]

View file

@ -33,7 +33,7 @@ define [
if !ace.config._moduleUrl? if !ace.config._moduleUrl?
ace.config._moduleUrl = ace.config.moduleUrl ace.config._moduleUrl = ace.config.moduleUrl
ace.config.moduleUrl = (args...) -> ace.config.moduleUrl = (args...) ->
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}" url = ace.config._moduleUrl(args...)
return url return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) -> App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) ->

View file

@ -100,6 +100,7 @@ define [
end_ts: end_ts end_ts: end_ts
doc: doc doc: doc
error: false error: false
pathname: doc.name
} }
if !doc.deleted if !doc.deleted
@ -190,8 +191,10 @@ define [
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1] previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates for update in updates
update.pathnames = [] # Used for display
for doc_id, doc of update.docs or {} for doc_id, doc of update.docs or {}
doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true) doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
update.pathnames.push doc.entity.name
for user in update.meta.users or [] for user in update.meta.users or []
if user? if user?

View file

@ -0,0 +1,280 @@
define [
"moment"
"ide/colors/ColorManager"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager) ->
class HistoryManager
constructor: (@ide, @$scope) ->
@reset()
@$scope.toggleHistory = () =>
if @$scope.ui.view == "history"
@hide()
else
@show()
@$scope.$watch "history.selection.updates", (updates) =>
if updates? and updates.length > 0
@_selectDocFromUpdates()
@reloadDiff()
@$scope.$on "entity:selected", (event, entity) =>
if (@$scope.ui.view == "history") and (entity.type == "doc")
@$scope.history.selection.pathname = _ide.fileTreeManager.getEntityPath(entity)
@reloadDiff()
show: () ->
@$scope.ui.view = "history"
@reset()
hide: () ->
@$scope.ui.view = "editor"
# Make sure we run the 'open' logic for whatever is currently selected
@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
reset: () ->
@$scope.history = {
isV2: true
updates: []
nextBeforeTimestamp: null
atEnd: false
selection: {
updates: []
pathname: null
range: {
fromV: null
toV: null
}
}
diff: null
}
MAX_RECENT_UPDATES_TO_SELECT: 2
autoSelectRecentUpdates: () ->
return if @$scope.history.updates.length == 0
@$scope.history.updates[0].selectedTo = true
indexOfLastUpdateNotByMe = 0
for update, i in @$scope.history.updates
if @_updateContainsUserId(update, @$scope.user.id) or i > @MAX_RECENT_UPDATES_TO_SELECT
break
indexOfLastUpdateNotByMe = i
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
BATCH_SIZE: 10
fetchNextBatchOfUpdates: () ->
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
if @$scope.history.nextBeforeTimestamp?
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
@$scope.history.loading = true
@ide.$http
.get(url)
.then (response) =>
{ data } = response
@_loadUpdates(data.updates)
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
if !data.nextBeforeTimestamp?
@$scope.history.atEnd = true
@$scope.history.loading = false
reloadDiff: () ->
diff = @$scope.history.diff
{updates} = @$scope.history.selection
{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
if !pathname?
@$scope.history.diff = null
return
return if diff? and
diff.pathname == pathname and
diff.fromV == fromV and
diff.toV == toV
@$scope.history.diff = diff = {
fromV: fromV
toV: toV
pathname: pathname
error: false
}
diff.loading = true
url = "/project/#{@$scope.project_id}/diff"
query = ["pathname=#{encodeURIComponent(pathname)}"]
if diff.fromV? and diff.toV?
query.push "from=#{diff.fromV}", "to=#{diff.toV}"
url += "?" + query.join("&")
@ide.$http
.get(url)
.then (response) =>
{ data } = response
diff.loading = false
{text, highlights} = @_parseDiff(data)
diff.text = text
diff.highlights = highlights
.catch () ->
diff.loading = false
diff.error = true
_parseDiff: (diff) ->
row = 0
column = 0
highlights = []
text = ""
for entry, i in diff.diff or []
content = entry.u or entry.i or entry.d
content ||= ""
text += content
lines = content.split("\n")
startRow = row
startColumn = column
if lines.length > 1
endRow = startRow + lines.length - 1
endColumn = lines[lines.length - 1].length
else
endRow = startRow
endColumn = startColumn + lines[0].length
row = endRow
column = endColumn
range = {
start:
row: startRow
column: startColumn
end:
row: endRow
column: endColumn
}
if entry.i? or entry.d?
if entry.meta.user?
name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
else
name = "Anonymous"
if entry.meta.user?.id == @$scope.user.id
name = "you"
date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
if entry.i?
highlights.push {
label: "Added by #{name} on #{date}"
highlight: range
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
}
else if entry.d?
highlights.push {
label: "Deleted by #{name} on #{date}"
strikeThrough: range
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
}
return {text, highlights}
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update 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
firstLoad = @$scope.history.updates.length == 0
@$scope.history.updates =
@$scope.history.updates.concat(updates)
@autoSelectRecentUpdates() if firstLoad
_perDocSummaryOfUpdates: (updates) ->
# Track current_pathname -> original_pathname
original_pathnames = {}
# Map of original pathname -> doc summary
docs_summary = {}
updatePathnameWithUpdateVersions = (pathname, update) ->
# docs_summary is indexed by the original pathname the doc
# had at the start, so we have to look this up from the current
# pathname via original_pathname first
if !original_pathnames[pathname]?
original_pathnames[pathname] = pathname
original_pathname = original_pathnames[pathname]
doc_summary = docs_summary[original_pathname] ?= {
fromV: update.fromV, toV: update.toV,
}
doc_summary.fromV = Math.min(
doc_summary.fromV,
update.fromV
)
doc_summary.toV = Math.max(
doc_summary.toV,
update.toV
)
# Put updates in ascending chronological order
updates = updates.slice().reverse()
for update in updates
for pathname in update.pathnames or []
updatePathnameWithUpdateVersions(pathname, update)
for project_op in update.project_ops or []
if project_op.rename?
rename = project_op.rename
updatePathnameWithUpdateVersions(rename.pathname, update)
original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
delete original_pathnames[rename.pathname]
if project_op.add?
add = project_op.add
updatePathnameWithUpdateVersions(add.pathname, update)
return docs_summary
_calculateDiffDataFromSelection: () ->
fromV = toV = pathname = null
selected_pathname = @$scope.history.selection.pathname
for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
if pathname == selected_pathname
{fromV, toV} = doc
return {fromV, toV, pathname}
return {}
# Set the track changes selected doc to one of the docs in the range
# of currently selected updates. If we already have a selected doc
# then prefer this one if present.
_selectDocFromUpdates: () ->
affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
selected_pathname = @$scope.history.selection.pathname
if selected_pathname? and affected_docs[selected_pathname]
# Selected doc is already open
else
# Set to first possible candidate
for pathname, doc of affected_docs
selected_pathname = pathname
break
@$scope.history.selection.pathname = selected_pathname
if selected_pathname?
entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
if entity?
@ide.fileTreeManager.selectEntity(entity)
_updateContainsUserId: (update, user_id) ->
for user in update.meta.users
return true if user?.id == user_id
return false

View file

@ -90,7 +90,9 @@ define [
# to block auto compiles. It also causes problems where server-provided # to block auto compiles. It also causes problems where server-provided
# linting errors aren't cleared after typing # linting errors aren't cleared after typing
if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError) if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError)
$scope.recompile(isAutoCompileOnChange: true) $scope.recompile(isAutoCompileOnChange: true) # compile if no linting errors
else if !ide.$scope.settings.syntaxValidation
$scope.recompile(isAutoCompileOnChange: true) # always recompile
else else
# Extend remainder of timeout # Extend remainder of timeout
autoCompileTimeout = setTimeout () -> autoCompileTimeout = setTimeout () ->
@ -533,14 +535,6 @@ define [
else else
$scope.switchToSideBySideLayout() $scope.switchToSideBySideLayout()
$scope.startFreeTrial = (source) ->
ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
event_tracking.sendMB "subscription-start-trial", { source }
window.open("/user/subscription/new?planCode=#{$scope.startTrialPlanCode}")
$scope.startedFreeTrial = true
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) -> App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
# enable per-user containers by default # enable per-user containers by default
perUserCompile = true perUserCompile = true

View file

@ -4,8 +4,3 @@ define [
App.controller "TrackChangesUpgradeModalController", ($scope, $modalInstance) -> App.controller "TrackChangesUpgradeModalController", ($scope, $modalInstance) ->
$scope.cancel = () -> $scope.cancel = () ->
$modalInstance.dismiss() $modalInstance.dismiss()
$scope.startFreeTrial = (source) ->
ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
window.open("/user/subscription/new?planCode=student_free_trial_7_days")
$scope.startedFreeTrial = true

View file

@ -9,7 +9,6 @@ define [
"libs/angular-cookie" "libs/angular-cookie"
"libs/passfield" "libs/passfield"
"libs/sixpack" "libs/sixpack"
"libs/groove"
"libs/angular-sixpack" "libs/angular-sixpack"
"libs/ng-tags-input-3.0.0" "libs/ng-tags-input-3.0.0"
], () -> ], () ->

View file

@ -11,9 +11,12 @@ define [
w = window.open() w = window.open()
go = () -> go = () ->
ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
url = "/user/subscription/new?planCode=#{plan}&ssp=true" if window.redirectToOLFreeTrialUrl?
if couponCode? url = window.redirectToOLFreeTrialUrl
url = "#{url}&cc=#{couponCode}" else
url = "/user/subscription/new?planCode=#{plan}&ssp=true"
if couponCode?
url = "#{url}&cc=#{couponCode}"
$scope.startedFreeTrial = true $scope.startedFreeTrial = true
switch source switch source
@ -27,7 +30,7 @@ define [
else else
event_tracking.sendMB "subscription-start-trial", { source, plan } event_tracking.sendMB "subscription-start-trial", { source, plan }
w.location = url w.location = url
if $scope.shouldABTestPlans if $scope.shouldABTestPlans

View file

@ -74,27 +74,34 @@ define [
$modalInstance.close() $modalInstance.close()
App.controller 'UniverstiesContactController', ($scope, $modal) -> App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
$scope.form = {} $scope.form = {}
$scope.sent = false $scope.sent = false
$scope.sending = false $scope.sending = false
$scope.error = false
$scope.contactUs = -> $scope.contactUs = ->
if !$scope.form.email? if !$scope.form.email?
console.log "email not set" console.log "email not set"
return return
$scope.sending = true $scope.sending = true
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
params = data =
_csrf : window.csrfToken
name: $scope.form.name || $scope.form.email name: $scope.form.name || $scope.form.email
email: $scope.form.email email: $scope.form.email
labels: "#{$scope.form.source} accounts" labels: "#{$scope.form.source} accounts"
message: "Please contact me with more details" message: "Please contact me with more details"
subject: $scope.form.subject + " - [#{ticketNumber}]" subject: "#{$scope.form.name} - General Enquiry - #{$scope.form.position} - #{$scope.form.university}"
about : "#{$scope.form.position || ''} #{$scope.form.university || ''}" inbox: "accounts"
Groove.createTicket params, (err, json)-> request = $http.post "/support", data
$scope.sent = true
request.catch ()->
$scope.error = true
$scope.$apply() $scope.$apply()
request.then (response)->
$scope.sent = true
$scope.error = (response.status != 200)
$scope.$apply()

View file

@ -462,6 +462,9 @@ define [
App.controller "ProjectListItemController", ($scope) -> App.controller "ProjectListItemController", ($scope) ->
$scope.shouldDisableCheckbox = (project) ->
$scope.filter == 'archived' && project.accessLevel != 'owner'
$scope.projectLink = (project) -> $scope.projectLink = (project) ->
if project.accessLevel == 'readAndWrite' and project.source == 'token' if project.accessLevel == 'readAndWrite' and project.source == 'token'
"/#{project.tokens.readAndWrite}" "/#{project.tokens.readAndWrite}"

View file

@ -1,5 +1,5 @@
define [ define [
"libs" "libraries"
], () -> ], () ->
angular.module('underscore', []).factory '_', -> angular.module('underscore', []).factory '_', ->
return window._ return window._

View file

@ -1,84 +0,0 @@
!function(window) {
window.Groove = {
init: function(options) {
this._options = options;
if (typeof grooveOnReady != 'undefined') {grooveOnReady();}
},
createTicket: function(params, callback) {
var postData = serialize({
"ticket[enduser_name]": params["name"],
"ticket[enduser_email]": params["email"],
"ticket[title]": params["subject"],
"ticket[enduser_about]": params["about"],
"ticket[label_string]": params["labels"],
"ticket[comments_attributes][0][body]": params["message"]
});
sendRequest(this._options.widget_ticket_url, function(req) {
if (callback) {callback(req);}
}, postData);
}
};
// http://www.quirksmode.org/js/xmlhttp.html
function sendRequest(url, callback, postData) {
var req = createXMLHTTPObject();
if (!req) return;
var method = (postData) ? "POST" : "GET";
req.open(method, url, true);
if (postData){
try {
req.setRequestHeader('Content-type','application/x-www-form-urlencoded');
}
catch(e) {
req.contentType = 'application/x-www-form-urlencoded';
};
};
req.onreadystatechange = function () {
if (req.readyState != 4) return;
callback(req);
}
if (req.readyState == 4) return;
req.send(postData);
}
var XMLHttpFactories = [
function () {return new XDomainRequest()},
function () {return new XMLHttpRequest()},
function () {return new ActiveXObject("Msxml2.XMLHTTP")},
function () {return new ActiveXObject("Msxml3.XMLHTTP")},
function () {return new ActiveXObject("Microsoft.XMLHTTP")}
];
function createXMLHTTPObject() {
var xmlhttp = false;
for (var i = 0; i < XMLHttpFactories.length; i++) {
try {
xmlhttp = XMLHttpFactories[i]();
}
catch (e) {
continue;
}
break;
}
return xmlhttp;
}
function serialize(obj) {
var str = [];
for(var p in obj) {
if (obj[p]) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
}
return str.join("&");
}
if (typeof grooveOnLoad != 'undefined') {grooveOnLoad();}
}(window);
Groove.init({"widget_ticket_url":"https://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.json"});

View file

@ -0,0 +1 @@
@import "app/sidebar-v2-dash-pane.less";

View file

@ -31,13 +31,18 @@
right: 0; right: 0;
bottom: @new-message-height; bottom: @new-message-height;
overflow-x: hidden; overflow-x: hidden;
background-color: @chat-bg;
li.message { li.message {
margin: @line-height-computed / 2; margin: @line-height-computed / 2;
.date { .date {
font-size: 12px; font-size: 12px;
color: @gray-light; color: @chat-message-date-color;
border-bottom: 1px solid @gray-lightest;
margin-bottom: @line-height-computed / 2; margin-bottom: @line-height-computed / 2;
text-align: right;
}
.date when (@is-overleaf = false) {
border-bottom: 1px solid @gray-lightest;
text-align: center; text-align: center;
} }
.avatar { .avatar {
@ -56,20 +61,22 @@
.name { .name {
font-size: 12px; font-size: 12px;
color: @gray-light; color: @chat-message-name-color;
margin-bottom: 4px; margin-bottom: 4px;
min-height: 16px; min-height: 16px;
} }
.message { .message {
border-left: 3px solid transparent; border-left: 3px solid transparent;
font-size: 14px; font-size: 14px;
box-shadow: -1px 2px 3px #ddd; box-shadow: @chat-message-box-shadow;
border-raduis: @border-radius-base; border-radius: @chat-message-border-radius;
position: relative; position: relative;
.message-content { .message-content {
padding: @line-height-computed / 2; padding: @chat-message-padding;
overflow-x: auto; overflow-x: auto;
color: @chat-message-color;
font-weight: @chat-message-weight;
} }
.arrow { .arrow {
@ -124,7 +131,7 @@
.full-size; .full-size;
top: auto; top: auto;
height: @new-message-height; height: @new-message-height;
background-color: @gray-lightest; background-color: @chat-new-message-bg;
padding: @line-height-computed / 4; padding: @line-height-computed / 4;
border-top: 1px solid @editor-border-color; border-top: 1px solid @editor-border-color;
textarea { textarea {
@ -134,9 +141,10 @@
border: 1px solid @editor-border-color; border: 1px solid @editor-border-color;
height: 100%; height: 100%;
width: 100%; width: 100%;
color: @gray-dark; color: @chat-new-message-textarea-color;
font-size: 14px; font-size: 14px;
padding: @line-height-computed / 4; padding: @line-height-computed / 4;
background-color: @chat-new-message-textarea-bg;
} }
} }
} }
@ -145,7 +153,7 @@
word-break: break-all; word-break: break-all;
} }
.editor-dark { .editor-dark when (@is-overleaf = false) {
.chat { .chat {
.new-message { .new-message {
background-color: lighten(@editor-dark-background-color, 10%); background-color: lighten(@editor-dark-background-color, 10%);

View file

@ -169,9 +169,19 @@
font-size: 0.8rem; font-size: 0.8rem;
line-height: @line-height-computed; line-height: @line-height-computed;
} }
.docs { .doc {
font-weight: bold;
font-size: 0.9rem; 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 { li.loading-changes, li.empty-message {

View file

@ -10,9 +10,13 @@
padding: 0 (@line-height-computed / 2); padding: 0 (@line-height-computed / 2);
} }
.pdf {
background-color: @pdf-bg;
}
.pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled { .pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled {
.full-size; .full-size;
top: 58px; top: @pdf-top-offset;
} }
.pdf-logs, .pdf-errors, .pdf-uncompiled, .pdf-validation-problems{ .pdf-logs, .pdf-errors, .pdf-uncompiled, .pdf-validation-problems{
@ -69,11 +73,11 @@
} }
.pdfjs-viewer { .pdfjs-viewer {
.full-size; .full-size;
background-color: @gray-lighter; background-color: @pdfjs-bg;
overflow: scroll; overflow: scroll;
canvas, div.pdf-canvas { canvas, div.pdf-canvas {
background: white; background: white;
box-shadow: black 0px 0px 10px; box-shadow: @pdf-page-shadow-color 0px 0px 10px;
} }
div.pdf-canvas.pdfng-empty { div.pdf-canvas.pdfng-empty {
background-color: white; background-color: white;
@ -179,7 +183,7 @@
cursor: pointer; cursor: pointer;
.line-no { .line-no {
float: right; float: right;
color: @gray; color: @log-line-no-color;
font-weight: 700; font-weight: 700;
.fa { .fa {
@ -203,16 +207,25 @@
} }
} }
&.alert-danger:hover { &.alert-danger {
background-color: darken(@alert-danger-bg, 5%); background-color: tint(@alert-danger-bg, 15%);
&:hover {
background-color: @alert-danger-bg;
}
} }
&.alert-warning:hover { &.alert-warning {
background-color: darken(@alert-warning-bg, 5%); background-color: tint(@alert-warning-bg, 15%);
&:hover {
background-color: @alert-warning-bg;
}
} }
&.alert-info:hover { &.alert-info {
background-color: darken(@alert-info-bg, 5%); background-color: tint(@alert-info-bg, 15%);
&:hover {
background-color: @alert-info-bg;
}
} }
} }
@ -354,22 +367,22 @@
} }
.alert-danger & { .alert-danger & {
color: @alert-danger-border; color: @state-danger-border;
} }
.alert-warning & { .alert-warning & {
color: @alert-warning-border; color: @state-warning-border;
} }
.alert-info & { .alert-info & {
color: @alert-info-border; color: @state-info-border;
} }
} }
&-text, &-text,
&-feedback-label { &-feedback-label {
color: @gray-dark; color: @log-hints-color;
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -394,25 +407,25 @@
&-actions a, &-actions a,
&-text a { &-text a {
.alert-danger & { .alert-danger & {
color: @alert-danger-text; color: @state-danger-text;
} }
.alert-warning & { .alert-warning & {
color: @alert-warning-text; color: @state-warning-text;
} }
.alert-info & { .alert-info & {
color: @alert-info-text; color: @state-info-text;
} }
} }
&-feedback { &-feedback {
color: @gray-dark; color: @log-hints-color;
float: right; float: right;
} }
&-extra-feedback { &-extra-feedback {
color: @gray-dark; color: @log-hints-color;
font-size: 0.8rem; font-size: 0.8rem;
margin-top: 10px; margin-top: 10px;
padding-bottom: 5px; padding-bottom: 5px;

View file

@ -129,11 +129,11 @@
} }
.toolbar-small-mixin() { .toolbar-small-mixin() {
height: 32px; height: @toolbar-small-height;
} }
.toolbar-tall-mixin() { .toolbar-tall-mixin() {
height: 58px; height: @toolbar-tall-height;
padding-top: 10px; padding-top: 10px;
} }
.toolbar-alt-mixin() { .toolbar-alt-mixin() {
@ -143,7 +143,7 @@
.toolbar-label { .toolbar-label {
display: none; display: none;
margin: 0 4px; margin: 0 4px;
font-size: 12px; font-size: @toolbar-font-size;
font-weight: 600; font-weight: 600;
margin-bottom: 2px; margin-bottom: 2px;
vertical-align: middle; vertical-align: middle;

View file

@ -2,8 +2,34 @@
text-align: center; text-align: center;
} }
.v1-import-row {
display: flex;
align-items: center;
}
.v1-import-col {
flex-basis: 50%;
flex-grow: 0;
flex-shrink: 0;
padding-left: 15px;
padding-right: 15px;
}
.v1-import-col ul {
margin-bottom: 0;
}
.v1-import-img { .v1-import-img {
width: 100%; width: 100%;
margin-top: 30px;
}
.v1-import-cta {
margin-top: 20px;
margin-left: auto;
margin-right: auto;
width: 90%;
text-align: center;
} }
.v1-import-warning { .v1-import-warning {

View file

@ -57,7 +57,6 @@
} }
.project-list-sidebar when (@is-overleaf) { .project-list-sidebar when (@is-overleaf) {
height: 100%;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-ms-overflow-style: -ms-autohiding-scrollbar; -ms-overflow-style: -ms-autohiding-scrollbar;

View file

@ -0,0 +1,27 @@
@v2-dash-pane-link-height: 130px;
.project-list-sidebar {
height: calc(~"100% -" @v2-dash-pane-link-height);
}
.project-list-sidebar-v2-pane {
position: absolute;
bottom: 0;
height: @v2-dash-pane-link-height;
background-color: @v2-dash-pane-bg;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
color: white;
font-size: 13px;
}
.project-list-sidebar-v2-pane a {
color: @v2-dash-pane-link-color;
text-decoration: underline;
}
.project-list-sidebar-v2-pane a:hover {
text-decoration: none;
}

View file

@ -10,7 +10,7 @@
padding: @alert-padding; padding: @alert-padding;
margin-bottom: @line-height-computed; margin-bottom: @line-height-computed;
border-left: 3px solid transparent; border-left: 3px solid transparent;
// border-radius: @alert-border-radius; border-radius: @alert-border-radius;
// Headings for larger alerts // Headings for larger alerts
h4 { h4 {

View file

@ -1,7 +1,6 @@
.card { .card {
background-color: white; background-color: white;
border-radius: @border-radius-base; border-radius: @border-radius-base;
-webkit-box-shadow: @card-box-shadow;
box-shadow: @card-box-shadow; box-shadow: @card-box-shadow;
padding: @line-height-computed; padding: @line-height-computed;
.page-header { .page-header {

View file

@ -20,14 +20,9 @@
//== Typography //== Typography
// //
//## Font, line-height, and color for body text, headings, and more. //## Font, line-height, and color for body text, headings, and more.
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700);
//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i);
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
@font-family-sans-serif: "Open Sans", sans-serif; @font-family-sans-serif: "Open Sans", sans-serif;
@font-family-serif: "Merriweather", serif; @font-family-serif: "Merriweather", serif;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. //** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; @font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
@font-family-base: @font-family-sans-serif; @font-family-base: @font-family-sans-serif;
@ -550,7 +545,7 @@
//## Define alert colors, border radius, and padding. //## Define alert colors, border radius, and padding.
@alert-padding: 15px; @alert-padding: 15px;
@alert-border-radius: @border-radius-base; @alert-border-radius: 0;
@alert-link-font-weight: bold; @alert-link-font-weight: bold;
@alert-success-bg: @state-success-bg; @alert-success-bg: @state-success-bg;
@ -898,25 +893,28 @@
@toolbar-btn-active-color : white; @toolbar-btn-active-color : white;
@toolbar-btn-active-bg-color : @link-color; @toolbar-btn-active-bg-color : @link-color;
@toolbar-btn-active-shadow : inset 0 3px 5px rgba(0, 0, 0, 0.225); @toolbar-btn-active-shadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
@toolbar-font-size : 12px;
@toolbar-alt-bg-color : #fafafa; @toolbar-alt-bg-color : #fafafa;
@toolbar-icon-btn-color : @gray-light; @toolbar-icon-btn-color : @gray-light;
@toolbar-icon-btn-hover-color : @gray-dark; @toolbar-icon-btn-hover-color : @gray-dark;
@toolbar-icon-btn-hover-shadow : 0 1px 0 rgba(0, 0, 0, 0.25); @toolbar-icon-btn-hover-shadow : 0 1px 0 rgba(0, 0, 0, 0.25);
@toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225); @toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
@toolbar-border-bottom : 1px solid @toolbar-border-color; @toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-small-height : 32px;
@toolbar-tall-height : 58px;
// Editor file-tree // Editor file-tree
@file-tree-bg : transparent; @file-tree-bg : transparent;
@file-tree-line-height : 2.6; @file-tree-line-height : 2.6;
@file-tree-item-color : @gray-darker; @file-tree-item-color : @gray-darker;
@file-tree-item-toggle-color : @gray; @file-tree-item-toggle-color : @gray;
@file-tree-item-icon-color : @gray-light; @file-tree-item-icon-color : @gray-light;
@file-tree-item-input-color : inherit; @file-tree-item-input-color : inherit;
@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%); @file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
@file-tree-item-hover-bg : @gray-lightest; @file-tree-item-hover-bg : @gray-lightest;
@file-tree-item-selected-bg : transparent; @file-tree-item-selected-bg : transparent;
@file-tree-multiselect-bg : lighten(@brand-info, 40%); @file-tree-multiselect-bg : lighten(@brand-info, 40%);
@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%); @file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
// Editor resizers // Editor resizers
@editor-resizer-bg-color : #F4F4F4; @editor-resizer-bg-color : #F4F4F4;
@ -925,6 +923,28 @@
@editor-toggler-hover-bg-color : #DDD; @editor-toggler-hover-bg-color : #DDD;
@synctex-controls-z-index : 3; @synctex-controls-z-index : 3;
@synctex-controls-padding : 0 2px; @synctex-controls-padding : 0 2px;
// Chat
@chat-bg : transparent;
@chat-message-color : @text-color;
@chat-message-date-color : @gray-light;
@chat-message-name-color : @gray-light;
@chat-message-box-shadow : -1px 2px 3px #ddd;
@chat-message-border-radius : 0;
@chat-message-padding : @line-height-computed / 2;
@chat-message-weight : normal;
@chat-new-message-bg : @gray-lightest;
@chat-new-message-textarea-bg : #FFF;
@chat-new-message-textarea-color : @gray-dark;
// PDF
@pdf-top-offset : @toolbar-tall-height;
@pdf-bg : transparent;
@pdfjs-bg : @gray-lighter;
@pdf-page-shadow-color : #000;
@log-line-no-color : @gray;
@log-hints-color : @gray-dark;
// Tags // Tags
@tag-border-radius : 0.25em; @tag-border-radius : 0.25em;
@tag-bg-color : @label-default-bg; @tag-bg-color : @label-default-bg;

View file

@ -1,6 +1,9 @@
@import "./_common-variables.less"; @import "./_common-variables.less";
@is-overleaf: true; @is-overleaf: true;
@font-family-sans-serif: "Lato", sans-serif;
@header-height: 68px; @header-height: 68px;
@footer-height: 50px; @footer-height: 50px;
@ -64,6 +67,28 @@
@btn-info-bg : @ol-blue; @btn-info-bg : @ol-blue;
@btn-info-border : transparent; @btn-info-border : transparent;
// Alerts
@alert-padding : 15px;
@alert-border-radius : @border-radius-base;
@alert-link-font-weight : bold;
@alert-success-bg : @brand-success;
@alert-success-text : #FFF;
@alert-success-border: transparent;
@alert-info-bg : @brand-info;
@alert-info-text : #FFF;
@alert-info-border : transparent;
@alert-warning-bg : @brand-warning;
@alert-warning-text : #FFF;
@alert-warning-border: transparent;
@alert-danger-bg : @brand-danger;
@alert-danger-text : #FFF;
@alert-danger-border : transparent;
// Tags // Tags
@tag-border-radius : 9999px; @tag-border-radius : 9999px;
@tag-bg-color : @ol-green; @tag-bg-color : @ol-green;
@ -110,6 +135,10 @@
@sidebar-active-bg : @ol-blue-gray-6; @sidebar-active-bg : @ol-blue-gray-6;
@sidebar-hover-bg : @ol-blue-gray-4; @sidebar-hover-bg : @ol-blue-gray-4;
@sidebar-hover-text-decoration : none; @sidebar-hover-text-decoration : none;
@v2-dash-pane-bg : @ol-blue-gray-4;
@v2-dash-pane-link-color : #FFF;
@v2-dash-pane-btn-bg : @ol-blue-gray-5;
@v2-dash-pane-btn-hover-bg : @ol-blue-gray-6;
@folders-menu-margin : 0 -(@grid-gutter-width / 2); @folders-menu-margin : 0 -(@grid-gutter-width / 2);
@folders-menu-line-height : @structured-list-line-height; @folders-menu-line-height : @structured-list-line-height;
@ -173,19 +202,22 @@
@toolbar-icon-btn-hover-shadow : none; @toolbar-icon-btn-hover-shadow : none;
@toolbar-border-bottom : 1px solid @toolbar-border-color; @toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-icon-btn-hover-boxshadow : none; @toolbar-icon-btn-hover-boxshadow : none;
@toolbar-font-size : 13px;
// Editor file-tree // Editor file-tree
@file-tree-bg : @ol-blue-gray-4; @file-tree-bg : @ol-blue-gray-4;
@file-tree-line-height : 2.05; @file-tree-line-height : 2.05;
@file-tree-item-color : #FFF; @file-tree-item-color : #FFF;
@file-tree-item-input-color : @ol-blue-gray-5; @file-tree-item-input-color : @ol-blue-gray-5;
@file-tree-item-toggle-color : @ol-blue-gray-2; @file-tree-item-toggle-color : @ol-blue-gray-2;
@file-tree-item-icon-color : @ol-blue-gray-2; @file-tree-item-icon-color : @ol-blue-gray-2;
@file-tree-item-folder-color : @ol-blue-gray-2; @file-tree-item-folder-color : @ol-blue-gray-2;
@file-tree-item-hover-bg : @ol-blue-gray-5; @file-tree-item-hover-bg : @ol-blue-gray-5;
@file-tree-item-selected-bg : @ol-green; @file-tree-item-selected-bg : @ol-green;
@file-tree-multiselect-bg : @ol-blue; @file-tree-multiselect-bg : @ol-blue;
@file-tree-multiselect-hover-bg : @ol-dark-blue; @file-tree-multiselect-hover-bg : @ol-dark-blue;
@file-tree-droppable-bg-color : tint(@ol-green, 5%); @file-tree-droppable-bg-color : tint(@ol-green, 5%);
// Editor resizers // Editor resizers
@editor-resizer-bg-color : @ol-blue-gray-6; @editor-resizer-bg-color : @ol-blue-gray-6;
@editor-resizer-bg-color-dragging : transparent; @editor-resizer-bg-color-dragging : transparent;
@ -193,6 +225,31 @@
@editor-toggler-hover-bg-color : @ol-green; @editor-toggler-hover-bg-color : @ol-green;
@synctex-controls-z-index : 6; @synctex-controls-z-index : 6;
@synctex-controls-padding : 0; @synctex-controls-padding : 0;
@editor-border-color : @ol-blue-gray-5;
// Chat
@chat-bg : @ol-blue-gray-5;
@chat-message-color : #FFF;
@chat-message-name-color : #FFF;
@chat-message-date-color : @ol-blue-gray-2;
@chat-message-box-shadow : none;
@chat-message-padding : 5px 10px;
@chat-message-border-radius : @border-radius-large;
@chat-message-weight : bold;
@chat-new-message-bg : @ol-blue-gray-4;
@chat-new-message-textarea-bg : @ol-blue-gray-1;
@chat-new-message-textarea-color : @ol-blue-gray-6;
// PDF
@pdf-top-offset : @toolbar-small-height;
@pdf-bg : @ol-blue-gray-1;
@pdfjs-bg : transparent;
@pdf-page-shadow-color : rgba(0, 0, 0, 0.5);
@log-line-no-color : #FFF;
@log-hints-color : @ol-blue-gray-4;
//== Colors
//
//## Gray and brand colors for use across Bootstrap. //## Gray and brand colors for use across Bootstrap.
@gray-darker: #252525; @gray-darker: #252525;
@gray-dark: #505050; @gray-dark: #505050;
@ -212,9 +269,9 @@
@brand-primary: @ol-green; @brand-primary: @ol-green;
@brand-success: @green; @brand-success: @green;
@brand-info: @ol-dark-green; @brand-info: @ol-blue;
@brand-warning: @orange; @brand-warning: @orange;
@brand-danger: #E03A06; @brand-danger: @ol-red;
@editor-loading-logo-padding-top: 115.44%; @editor-loading-logo-padding-top: 115.44%;
@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg); @editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);

View file

@ -1,4 +1,8 @@
@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700&subset=latin-ext);
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
// Core variables and mixins // Core variables and mixins
@import "core/ol-variables.less"; @import "core/ol-variables.less";
@import "app/ol-style-guide.less"; @import "app/ol-style-guide.less";
@import "_style_includes.less"; @import "_style_includes.less";
@import "_ol_style_includes.less";

View file

@ -1,3 +1,6 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
// Core variables and mixins // Core variables and mixins
@import "core/variables.less"; @import "core/variables.less";
@import "_style_includes.less"; @import "_style_includes.less";

View file

@ -59,7 +59,7 @@ describe "ProjectStructureChanges", ->
@dup_project_id = body.project_id @dup_project_id = body.project_id
done() done()
it "should version the dosc created", -> it "should version the docs created", ->
updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
expect(updates.length).to.equal(2) expect(updates.length).to.equal(2)
_.each updates, (update) => _.each updates, (update) =>
@ -91,6 +91,7 @@ describe "ProjectStructureChanges", ->
throw error if error? throw error if error?
if res.statusCode < 200 || res.statusCode >= 300 if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to add doc #{res.statusCode}") throw new Error("failed to add doc #{res.statusCode}")
@example_doc_id = body._id
done() done()
it "should version the doc added", -> it "should version the doc added", ->
@ -162,6 +163,8 @@ describe "ProjectStructureChanges", ->
if res.statusCode < 200 || res.statusCode >= 300 if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to upload file #{res.statusCode}") throw new Error("failed to upload file #{res.statusCode}")
@example_file_id = JSON.parse(body).entity_id
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1) expect(updates.length).to.equal(1)
update = updates[0] update = updates[0]
@ -199,6 +202,120 @@ describe "ProjectStructureChanges", ->
done() done()
describe "moving entities", ->
before (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/folder",
formData:
name: 'foo'
}, (error, res, body) =>
throw error if error?
@example_folder_id_1 = JSON.parse(body)._id
done()
beforeEach () ->
MockDocUpdaterApi.clearProjectStructureUpdates()
it "should version moving a doc", (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/Doc/#{@example_doc_id}/move",
json:
folder_id: @example_folder_id_1
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to move doc #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/new.tex")
expect(update.newPathname).to.equal("/foo/new.tex")
done()
it "should version moving a file", (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/File/#{@example_file_id}/move",
json:
folder_id: @example_folder_id_1
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to move file #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/1pixel.png")
expect(update.newPathname).to.equal("/foo/1pixel.png")
done()
it "should version moving a folder", (done) ->
@owner.request.post {
uri: "project/#{@example_project_id}/folder",
formData:
name: 'bar'
}, (error, res, body) =>
throw error if error?
@example_folder_id_2 = JSON.parse(body)._id
@owner.request.post {
uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_1}/move",
json:
folder_id: @example_folder_id_2
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to move folder #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/foo/new.tex")
expect(update.newPathname).to.equal("/bar/foo/new.tex")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/foo/1pixel.png")
expect(update.newPathname).to.equal("/bar/foo/1pixel.png")
done()
describe "deleting entities", ->
beforeEach () ->
MockDocUpdaterApi.clearProjectStructureUpdates()
it "should version deleting a folder", (done) ->
@owner.request.delete {
uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_2}",
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to delete folder #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/bar/foo/new.tex")
expect(update.newPathname).to.equal("")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/bar/foo/1pixel.png")
expect(update.newPathname).to.equal("")
done()
describe "tpds", -> describe "tpds", ->
before (done) -> before (done) ->
@tpds_project_name = "tpds-project-#{new ObjectId().toString()}" @tpds_project_name = "tpds-project-#{new ObjectId().toString()}"
@ -305,3 +422,25 @@ describe "ProjectStructureChanges", ->
done() done()
image_file.pipe(req) image_file.pipe(req)
it "should version deleting a doc", (done) ->
req = @owner.request.delete {
uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex",
auth:
user: _.keys(Settings.httpAuthUsers)[0]
pass: _.values(Settings.httpAuthUsers)[0]
sendImmediately: true
}, (error, res, body) =>
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to delete doc #{res.statusCode}")
updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates
expect(updates.length).to.equal(1)
update = updates[0]
expect(update.userId).to.equal(@owner._id)
expect(update.pathname).to.equal("/test.tex")
expect(update.newPathname).to.equal("")
done()

View file

@ -33,6 +33,9 @@ module.exports = MockDocUpdaterApi =
@addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates) @addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates)
res.sendStatus 200 res.sendStatus 200
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
res.send 204
app.listen 3003, (error) -> app.listen 3003, (error) ->
throw error if error? throw error if error?
.on "error", (error) -> .on "error", (error) ->

View file

@ -23,6 +23,16 @@ module.exports = MockDocStoreApi =
docs = (doc for doc_id, doc of @docs[req.params.project_id]) docs = (doc for doc_id, doc of @docs[req.params.project_id])
res.send JSON.stringify docs res.send JSON.stringify docs
app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
{project_id, doc_id} = req.params
if !@docs[project_id]?
res.send 404
else if !@docs[project_id][doc_id]?
res.send 404
else
@docs[project_id][doc_id] = undefined
res.send 204
app.listen 3016, (error) -> app.listen 3016, (error) ->
throw error if error? throw error if error?
.on "error", (error) -> .on "error", (error) ->

View file

@ -393,7 +393,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history disabled", -> describe "with project history disabled", ->
beforeEach -> beforeEach ->
@settings.apis.project_history.enabled = false @settings.apis.project_history.sendProjectStructureOps = false
@request.post = sinon.stub() @request.post = sinon.stub()
@handler.updateProjectStructure @project_id, @user_id, {}, @callback @handler.updateProjectStructure @project_id, @user_id, {}, @callback
@ -406,7 +406,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history enabled", -> describe "with project history enabled", ->
beforeEach -> beforeEach ->
@settings.apis.project_history.enabled = true @settings.apis.project_history.sendProjectStructureOps = true
@url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}" @url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
@ -478,14 +478,22 @@ describe 'DocumentUpdaterHandler', ->
.should.equal true .should.equal true
done() done()
describe "when a doc has been deleted", -> describe "when an entity has been deleted", ->
it 'should do nothing', (done) -> it 'should end the structure update to the document updater', (done) ->
@docId = new ObjectId() @docId = new ObjectId()
@changes = oldDocs: [ @changes = oldDocs: [
{ path: '/foo', docLines: 'a\nb', doc: _id: @docId } { path: '/foo', docLines: 'a\nb', doc: _id: @docId }
] ]
docUpdates = [
id: @docId.toString(),
pathname: '/foo',
newPathname: ''
]
@handler.updateProjectStructure @project_id, @user_id, @changes, () => @handler.updateProjectStructure @project_id, @user_id, @changes, () =>
@request.post.called.should.equal false @request.post
.calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id})
.should.equal true
done() done()

View file

@ -376,58 +376,53 @@ describe "EditorController", ->
err.should.equal "timed out" err.should.equal "timed out"
done() done()
describe "deleteEntity", -> describe "deleteEntity", ->
beforeEach -> beforeEach ->
@LockManager.getLock.callsArgWith(1) @LockManager.getLock.callsArgWith(1)
@LockManager.releaseLock.callsArgWith(1) @LockManager.releaseLock.callsArgWith(1)
@EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(4) @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5)
it "should call deleteEntityWithoutLock", (done)-> it "should call deleteEntityWithoutLock", (done)->
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, => @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
@EditorController.deleteEntityWithoutLock.calledWith(@project_id, @entity_id, @type, @source).should.equal true @EditorController.deleteEntityWithoutLock
.calledWith(@project_id, @entity_id, @type, @source, @user_id)
.should.equal true
done() done()
it "should take the lock", (done)-> it "should take the lock", (done)->
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, => @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
@LockManager.getLock.calledWith(@project_id).should.equal true @LockManager.getLock.calledWith(@project_id).should.equal true
done() done()
it "should release the lock", (done)-> it "should release the lock", (done)->
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, (error)=> @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
@LockManager.releaseLock.calledWith(@project_id).should.equal true @LockManager.releaseLock.calledWith(@project_id).should.equal true
done() done()
it "should error if it can't cat the lock", (done)-> it "should error if it can't cat the lock", (done)->
@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out") @LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
@EditorController.deleteEntity @project_id, @entity_id, @type, @source, (err)=> @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
expect(err).to.exist expect(error).to.exist
err.should.equal "timed out" error.should.equal "timed out"
done() done()
describe 'deleteEntityWithoutLock', -> describe 'deleteEntityWithoutLock', ->
beforeEach -> beforeEach (done) ->
@ProjectEntityHandler.deleteEntity = (project_id, entity_id, type, callback)-> callback()
@entity_id = "entity_id_here" @entity_id = "entity_id_here"
@type = "doc" @type = "doc"
@EditorRealTimeController.emitToRoom = sinon.stub() @EditorRealTimeController.emitToRoom = sinon.stub()
@ProjectEntityHandler.deleteEntity = sinon.stub().callsArg(4)
@EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, @user_id, done
it 'should delete the folder using the project entity handler', (done)-> it 'should delete the folder using the project entity handler', ->
mock = sinon.mock(@ProjectEntityHandler).expects("deleteEntity").withArgs(@project_id, @entity_id, @type).callsArg(3) @ProjectEntityHandler.deleteEntity
.calledWith(@project_id, @entity_id, @type, @user_id)
.should.equal.true
@EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, -> it 'notify users an entity has been deleted', ->
mock.verify() @EditorRealTimeController.emitToRoom
done() .calledWith(@project_id, "removeEntity", @entity_id, @source)
.should.equal true
it 'notify users an entity has been deleted', (done)->
@EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, =>
@EditorRealTimeController.emitToRoom
.calledWith(@project_id, "removeEntity", @entity_id, @source)
.should.equal true
done()
describe "getting a list of project paths", -> describe "getting a list of project paths", ->

View file

@ -331,12 +331,12 @@ describe "EditorHttpController", ->
Project_id: @project_id Project_id: @project_id
entity_id: @entity_id = "entity-id-123" entity_id: @entity_id = "entity-id-123"
entity_type: @entity_type = "entity-type" entity_type: @entity_type = "entity-type"
@EditorController.deleteEntity = sinon.stub().callsArg(4) @EditorController.deleteEntity = sinon.stub().callsArg(5)
@EditorHttpController.deleteEntity @req, @res @EditorHttpController.deleteEntity @req, @res
it "should call EditorController.deleteEntity", -> it "should call EditorController.deleteEntity", ->
@EditorController.deleteEntity @EditorController.deleteEntity
.calledWith(@project_id, @entity_id, @entity_type, "editor") .calledWith(@project_id, @entity_id, @entity_type, "editor", @userId)
.should.equal true .should.equal true
it "should send back a success response", -> it "should send back a success response", ->

View file

@ -31,7 +31,7 @@ describe "HistoryController", ->
describe "for a project with project history", -> describe "for a project with project history", ->
beforeEach -> beforeEach ->
@ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{display:true}}}) @ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{id: 42, display:true}}})
@HistoryController.selectHistoryApi @req, @res, @next @HistoryController.selectHistoryApi @req, @res, @next
it "should set the flag for project history to true", -> it "should set the flag for project history to true", ->
@ -57,93 +57,55 @@ describe "HistoryController", ->
on: (event, handler) -> @events[event] = handler on: (event, handler) -> @events[event] = handler
@request.returns @proxy @request.returns @proxy
describe "with project history enabled", -> describe "for a project with the project history flag", ->
beforeEach -> beforeEach ->
@settings.apis.project_history.enabled = true @req.useProjectHistory = true
@HistoryController.proxyToHistoryApi @req, @res, @next
describe "for a project with the project history flag", -> it "should get the user id", ->
beforeEach -> @AuthenticationController.getLoggedInUserId
@req.useProjectHistory = true .calledWith(@req)
@HistoryController.proxyToHistoryApi @req, @res, @next .should.equal true
it "should get the user id", -> it "should call the project history api", ->
@AuthenticationController.getLoggedInUserId @request
.calledWith(@req) .calledWith({
.should.equal true url: "#{@settings.apis.project_history.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should call the project history api", -> it "should pipe the response to the client", ->
@request @proxy.pipe
.calledWith({ .calledWith(@res)
url: "#{@settings.apis.project_history.url}#{@req.url}" .should.equal true
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should pipe the response to the client", -> describe "for a project without the project history flag", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
describe "for a project without the project history flag", ->
beforeEach ->
@req.useProjectHistory = false
@HistoryController.proxyToHistoryApi @req, @res, @next
it "should get the user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should call the track changes api", ->
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should pipe the response to the client", ->
@proxy.pipe
.calledWith(@res)
.should.equal true
describe "with project history disabled", ->
beforeEach -> beforeEach ->
@settings.apis.project_history.enabled = false @req.useProjectHistory = false
@HistoryController.proxyToHistoryApi @req, @res, @next
describe "for a project with the project history flag", -> it "should get the user id", ->
beforeEach -> @AuthenticationController.getLoggedInUserId
@req.useProjectHistory = true .calledWith(@req)
@HistoryController.proxyToHistoryApi @req, @res, @next .should.equal true
it "should call the track changes api", -> it "should call the track changes api", ->
@request @request
.calledWith({ .calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}" url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method method: @req.method
headers: headers:
"X-User-Id": @user_id "X-User-Id": @user_id
}) })
.should.equal true .should.equal true
describe "for a project without the project history flag", -> it "should pipe the response to the client", ->
beforeEach -> @proxy.pipe
@req.useProjectHistory = false .calledWith(@res)
@HistoryController.proxyToHistoryApi @req, @res, @next .should.equal true
it "should call the track changes api", ->
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
describe "with an error", -> describe "with an error", ->
beforeEach -> beforeEach ->
@ -152,68 +114,3 @@ describe "HistoryController", ->
it "should pass the error up the call chain", -> it "should pass the error up the call chain", ->
@next.calledWith(@error).should.equal true @next.calledWith(@error).should.equal true
describe "initializeProject", ->
describe "with project history enabled", ->
beforeEach ->
@settings.apis.project_history.enabled = true
describe "project history returns a successful response", ->
beforeEach ->
@overleaf_id = 1234
@res = statusCode: 200
@body = JSON.stringify(project: id: @overleaf_id)
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryController.initializeProject @callback
it "should call the project history api", ->
@request.post.calledWith(
url: "#{@settings.apis.project_history.url}/project"
).should.equal true
it "should return the callback with the overleaf id", ->
@callback.calledWithExactly(null, { @overleaf_id }).should.equal true
describe "project history returns a response without the project id", ->
beforeEach ->
@res = statusCode: 200
@body = JSON.stringify(project: {})
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryController.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history did not provide an id"))
.should.equal true
describe "project history returns a unsuccessful response", ->
beforeEach ->
@res = statusCode: 404
@request.post = sinon.stub().callsArgWith(1, null, @res)
@HistoryController.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
.should.equal true
describe "project history errors", ->
beforeEach ->
@error = sinon.stub()
@request.post = sinon.stub().callsArgWith(1, @error)
@HistoryController.initializeProject @callback
it "should return the callback with the error", ->
@callback.calledWithExactly(@error).should.equal true
describe "with project history disabled", ->
beforeEach ->
@settings.apis.project_history.enabled = false
@HistoryController.initializeProject @callback
it "should return the callback", ->
@callback.calledWithExactly().should.equal true

View file

@ -0,0 +1,86 @@
chai = require('chai')
chai.should()
sinon = require("sinon")
modulePath = "../../../../app/js/Features/History/HistoryManager"
SandboxedModule = require('sandboxed-module')
describe "HistoryManager", ->
beforeEach ->
@callback = sinon.stub()
@user_id = "user-id-123"
@AuthenticationController =
getLoggedInUserId: sinon.stub().returns(@user_id)
@HistoryManager = SandboxedModule.require modulePath, requires:
"request" : @request = sinon.stub()
"settings-sharelatex": @settings = {}
@settings.apis =
trackchanges:
enabled: false
url: "http://trackchanges.example.com"
project_history:
url: "http://project_history.example.com"
describe "initializeProject", ->
describe "with project history enabled", ->
beforeEach ->
@settings.apis.project_history.initializeHistoryForNewProjects = true
describe "project history returns a successful response", ->
beforeEach ->
@overleaf_id = 1234
@res = statusCode: 200
@body = JSON.stringify(project: id: @overleaf_id)
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryManager.initializeProject @callback
it "should call the project history api", ->
@request.post.calledWith(
url: "#{@settings.apis.project_history.url}/project"
).should.equal true
it "should return the callback with the overleaf id", ->
@callback.calledWithExactly(null, { @overleaf_id }).should.equal true
describe "project history returns a response without the project id", ->
beforeEach ->
@res = statusCode: 200
@body = JSON.stringify(project: {})
@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
@HistoryManager.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history did not provide an id"))
.should.equal true
describe "project history returns a unsuccessful response", ->
beforeEach ->
@res = statusCode: 404
@request.post = sinon.stub().callsArgWith(1, null, @res)
@HistoryManager.initializeProject @callback
it "should return the callback with an error", ->
@callback
.calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
.should.equal true
describe "project history errors", ->
beforeEach ->
@error = sinon.stub()
@request.post = sinon.stub().callsArgWith(1, @error)
@HistoryManager.initializeProject @callback
it "should return the callback with the error", ->
@callback.calledWithExactly(@error).should.equal true
describe "with project history disabled", ->
beforeEach ->
@settings.apis.project_history.initializeHistoryForNewProjects = false
@HistoryManager.initializeProject @callback
it "should return the callback", ->
@callback.calledWithExactly().should.equal true

View file

@ -38,7 +38,7 @@ describe 'ProjectCreationHandler', ->
setRootDoc: sinon.stub().callsArg(2) setRootDoc: sinon.stub().callsArg(2)
@ProjectDetailsHandler = @ProjectDetailsHandler =
validateProjectName: sinon.stub().yields() validateProjectName: sinon.stub().yields()
@HistoryController = @HistoryManager =
initializeProject: sinon.stub().callsArg(0) initializeProject: sinon.stub().callsArg(0)
@user = @user =
@ -53,7 +53,7 @@ describe 'ProjectCreationHandler', ->
'../../models/User': User:@User '../../models/User': User:@User
'../../models/Project':{Project:@ProjectModel} '../../models/Project':{Project:@ProjectModel}
'../../models/Folder':{Folder:@FolderModel} '../../models/Folder':{Folder:@FolderModel}
'../History/HistoryController': @HistoryController '../History/HistoryManager': @HistoryManager
'./ProjectEntityHandler':@ProjectEntityHandler './ProjectEntityHandler':@ProjectEntityHandler
"./ProjectDetailsHandler":@ProjectDetailsHandler "./ProjectDetailsHandler":@ProjectDetailsHandler
"settings-sharelatex": @Settings = {} "settings-sharelatex": @Settings = {}
@ -66,7 +66,7 @@ describe 'ProjectCreationHandler', ->
describe 'Creating a Blank project', -> describe 'Creating a Blank project', ->
beforeEach -> beforeEach ->
@overleaf_id = 1234 @overleaf_id = 1234
@HistoryController.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id }) @HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
@ProjectModel::save = sinon.stub().callsArg(0) @ProjectModel::save = sinon.stub().callsArg(0)
describe "successfully", -> describe "successfully", ->
@ -83,7 +83,7 @@ describe 'ProjectCreationHandler', ->
it "should initialize the project overleaf if history id not provided", (done)-> it "should initialize the project overleaf if history id not provided", (done)->
@handler.createBlankProject ownerId, projectName, done @handler.createBlankProject ownerId, projectName, done
@HistoryController.initializeProject.calledWith().should.equal true @HistoryManager.initializeProject.calledWith().should.equal true
it "should set the overleaf id if overleaf id not provided", (done)-> it "should set the overleaf id if overleaf id not provided", (done)->
@handler.createBlankProject ownerId, projectName, (err, project)=> @handler.createBlankProject ownerId, projectName, (err, project)=>

View file

@ -64,7 +64,7 @@ describe 'ProjectDuplicator', ->
@projectOptionsHandler = @projectOptionsHandler =
setCompiler : sinon.stub() setCompiler : sinon.stub()
@entityHandler = @entityHandler =
addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"}) addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5) copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
setRootDoc: sinon.stub() setRootDoc: sinon.stub()
addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder) addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
done() done()
it 'should use the same compiler', (done)-> it 'should use the same compiler', (done)->
@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) @entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true @projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
done() done()
it 'should use the same root doc', (done)-> it 'should use the same root doc', (done)->
@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id) @entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true @entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
done() done()
@ -139,13 +139,13 @@ describe 'ProjectDuplicator', ->
it 'should copy all the docs', (done)-> it 'should copy all the docs', (done)->
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=> @duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true @DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true
@entityHandler.addDocWithProject @entityHandler.addDoc
.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id) .calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id)
.should.equal true .should.equal true
@entityHandler.addDocWithProject @entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id) .calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
.should.equal true .should.equal true
@entityHandler.addDocWithProject @entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id) .calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
.should.equal true .should.equal true
done() done()

View file

@ -157,13 +157,13 @@ describe 'ProjectEntityHandler', ->
@ProjectGetter.getProject.callsArgWith(2, null, @project) @ProjectGetter.getProject.callsArgWith(2, null, @project)
@tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1) @tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1)
@ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3) @ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3)
@ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(3) @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(5)
@path = mongo: "mongo.path", fileSystem: "/file/system/path" @path = mongo: "mongo.path", fileSystem: "/file/system/path"
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path) @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path)
describe "deleting from Mongo", -> describe "deleting from Mongo", ->
beforeEach (done) -> beforeEach (done) ->
@ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', done @ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', userId, done
it "should retreive the path", -> it "should retreive the path", ->
@projectLocator.findElement.called.should.equal true @projectLocator.findElement.called.should.equal true
@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', ->
it "should clean up the entity from the rest of the system", -> it "should clean up the entity from the rest of the system", ->
@ProjectEntityHandler._cleanUpEntity @ProjectEntityHandler._cleanUpEntity
.calledWith(@project, @entity, @type) .calledWith(@project, @entity, @type, @path.fileSystem, userId)
.should.equal true .should.equal true
describe "_cleanUpEntity", -> describe "_cleanUpEntity", ->
@ -193,7 +193,9 @@ describe 'ProjectEntityHandler', ->
describe "a file", -> describe "a file", ->
beforeEach (done) -> beforeEach (done) ->
@ProjectEntityHandler._cleanUpEntity @project, _id: @entity_id, 'file', done @path = "/file/system/path.png"
@entity = _id: @entity_id
@ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done
it "should delete the file from FileStoreHandler", -> it "should delete the file from FileStoreHandler", ->
@FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true
@ -201,38 +203,56 @@ describe 'ProjectEntityHandler', ->
it "should not attempt to delete from the document updater", -> it "should not attempt to delete from the document updater", ->
@documentUpdaterHandler.deleteDoc.called.should.equal false @documentUpdaterHandler.deleteDoc.called.should.equal false
it "should should send the update to the doc updater", ->
oldFiles = [ file: @entity, path: @path ]
@documentUpdaterHandler.updateProjectStructure
.calledWith(project_id, userId, {oldFiles})
.should.equal true
describe "a doc", -> describe "a doc", ->
beforeEach (done) -> beforeEach (done) ->
@ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2) @path = "/file/system/path.tex"
@ProjectEntityHandler._cleanUpEntity @project, @entity = {_id: @entity_id}, 'doc', done @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
@entity = {_id: @entity_id}
@ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done
it "should clean up the doc", -> it "should clean up the doc", ->
@ProjectEntityHandler._cleanUpDoc @ProjectEntityHandler._cleanUpDoc
.calledWith(@project, @entity) .calledWith(@project, @entity, @path, userId)
.should.equal true .should.equal true
describe "a folder", -> describe "a folder", ->
beforeEach (done) -> beforeEach (done) ->
@folder = @folder =
folders: [ folders: [
fileRefs: [ @file1 = {_id: "file-id-1" } ] name: "subfolder"
docs: [ @doc1 = { _id: "doc-id-1" } ] fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ]
docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ]
folders: [] folders: []
] ]
fileRefs: [ @file2 = { _id: "file-id-2" } ] fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ]
docs: [ @doc2 = { _id: "doc-id-2" } ] docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ]
@ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2) @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
@ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(2) @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4)
@ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", done path = "/folder"
@ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, userId, done
it "should clean up all sub files", -> it "should clean up all sub files", ->
@ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1).should.equal true @ProjectEntityHandler._cleanUpFile
@ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2).should.equal true .calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId)
.should.equal true
@ProjectEntityHandler._cleanUpFile
.calledWith(@project, @file2, "/folder/file-name-2", userId)
.should.equal true
it "should clean up all sub docs", -> it "should clean up all sub docs", ->
@ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1).should.equal true @ProjectEntityHandler._cleanUpDoc
@ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2).should.equal true .calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId)
.should.equal true
@ProjectEntityHandler._cleanUpDoc
.calledWith(@project, @doc2, "/folder/doc-name-2", userId)
.should.equal true
describe 'moveEntity', -> describe 'moveEntity', ->
beforeEach -> beforeEach ->
@ -496,6 +516,51 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, userId, {newDocs}) .calledWith(project_id, userId, {newDocs})
.should.equal true .should.equal true
describe 'addDocWithoutUpdatingHistory', ->
beforeEach ->
@name = "some new doc"
@lines = ['1234','abc']
@path = "/path/to/doc"
@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}})
@callback = sinon.stub()
@tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
@DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0)
@ProjectEntityHandler.addDocWithoutUpdatingHistory project_id, folder_id, @name, @lines, userId, @callback
# Created doc
@doc = @ProjectEntityHandler._putElement.args[0][2]
@doc.name.should.equal @name
expect(@doc.lines).to.be.undefined
it 'should call put element', ->
@ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc).should.equal true
it 'should return doc and parent folder', ->
@callback.calledWith(null, @doc, folder_id).should.equal true
it 'should call third party data store', ->
@tpdsUpdateSender.addDoc
.calledWith({
project_id: project_id
doc_id: doc_id
path: @path
project_name: @project.name
rev: 0
})
.should.equal true
it "should send the doc lines to the doc store", ->
@DocstoreManager.updateDoc
.calledWith(project_id, @doc._id.toString(), @lines)
.should.equal true
it "should not should send the change in project structure to the doc updater", () ->
@documentUpdaterHandler.updateProjectStructure
.called
.should.equal false
describe "restoreDoc", -> describe "restoreDoc", ->
beforeEach -> beforeEach ->
@name = "doc-name" @name = "doc-name"
@ -584,6 +649,12 @@ describe 'ProjectEntityHandler', ->
@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () -> @ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () ->
it "should not send the change in project structure to the doc updater when called as addFileWithoutUpdatingHistory", (done) ->
@documentUpdaterHandler.updateProjectStructure = sinon.stub().yields()
@ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, {}, userId, () =>
@documentUpdaterHandler.updateProjectStructure.called.should.equal false
done()
describe 'replaceFile', -> describe 'replaceFile', ->
beforeEach -> beforeEach ->
@projectLocator @projectLocator
@ -1116,6 +1187,7 @@ describe 'ProjectEntityHandler', ->
@doc = @doc =
_id: ObjectId() _id: ObjectId()
name: "test.tex" name: "test.tex"
@path = "/path/to/doc"
@ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1) @ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1)
@ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2) @ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2)
@documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2) @documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2)
@ -1125,7 +1197,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc is the root doc", -> describe "when the doc is the root doc", ->
beforeEach -> beforeEach ->
@project.rootDoc_id = @doc._id @project.rootDoc_id = @doc._id
@ProjectEntityHandler._cleanUpDoc @project, @doc, @callback @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should unset the root doc", -> it "should unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc @ProjectEntityHandler.unsetRootDoc
@ -1146,13 +1218,19 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, @doc._id.toString()) .calledWith(project_id, @doc._id.toString())
.should.equal true .should.equal true
it "should should send the update to the doc updater", ->
oldDocs = [ doc: @doc, path: @path ]
@documentUpdaterHandler.updateProjectStructure
.calledWith(project_id, userId, {oldDocs})
.should.equal true
it "should call the callback", -> it "should call the callback", ->
@callback.called.should.equal true @callback.called.should.equal true
describe "when the doc is not the root doc", -> describe "when the doc is not the root doc", ->
beforeEach -> beforeEach ->
@project.rootDoc_id = ObjectId() @project.rootDoc_id = ObjectId()
@ProjectEntityHandler._cleanUpDoc @project, @doc, @callback @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should not unset the root doc", -> it "should not unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc.called.should.equal false @ProjectEntityHandler.unsetRootDoc.called.should.equal false

View file

@ -57,6 +57,7 @@ describe "SubscriptionUpdater", ->
@ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1) @ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1)
@ReferalAllocator.cock = true @ReferalAllocator.cock = true
@Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
@SubscriptionUpdater = SandboxedModule.require modulePath, requires: @SubscriptionUpdater = SandboxedModule.require modulePath, requires:
'../../models/Subscription': Subscription:@SubscriptionModel '../../models/Subscription': Subscription:@SubscriptionModel
'./UserFeaturesUpdater': @UserFeaturesUpdater './UserFeaturesUpdater': @UserFeaturesUpdater
@ -65,6 +66,7 @@ describe "SubscriptionUpdater", ->
"logger-sharelatex": log:-> "logger-sharelatex": log:->
'settings-sharelatex': @Settings 'settings-sharelatex': @Settings
"../Referal/ReferalAllocator" : @ReferalAllocator "../Referal/ReferalAllocator" : @ReferalAllocator
'../../infrastructure/Modules': @Modules
describe "syncSubscription", -> describe "syncSubscription", ->
@ -204,10 +206,22 @@ describe "SubscriptionUpdater", ->
assert.equal args[1], @groupSubscription.planCode assert.equal args[1], @groupSubscription.planCode
done() done()
it "should call updateFeatures with the overleaf subscription if set", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, ['ol_pro'])
@SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id
assert.equal args[1], 'ol_pro'
done()
it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)-> it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)->
@subscription.planCode = @Settings.defaultPlanCode @subscription.planCode = @Settings.defaultPlanCode
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription) @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0] args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id assert.equal args[0], @adminUser._id
@ -218,6 +232,7 @@ describe "SubscriptionUpdater", ->
it "should call updateFeatures with default if there are no subscriptions for user", (done)-> it "should call updateFeatures with default if there are no subscriptions for user", (done)->
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null) @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
args = @UserFeaturesUpdater.updateFeatures.args[0] args = @UserFeaturesUpdater.updateFeatures.args[0]
assert.equal args[0], @adminUser._id assert.equal args[0], @adminUser._id
@ -263,3 +278,13 @@ describe "SubscriptionUpdater", ->
@SubscriptionUpdater._setUsersMinimumFeatures @SubscriptionUpdater._setUsersMinimumFeatures
.calledWith(user_id) .calledWith(user_id)
.should.equal true .should.equal true
describe 'refreshSubscription', ->
beforeEach ->
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub()
.callsArgWith(1, null)
it 'should call to _setUsersMinimumFeatures', ->
@SubscriptionUpdater.refreshSubscription(@adminUser._id, ()->)
@SubscriptionUpdater._setUsersMinimumFeatures.callCount.should.equal 1
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true

View file

@ -8,7 +8,7 @@ describe 'TpdsUpdateHandler', ->
beforeEach -> beforeEach ->
@requestQueuer = {} @requestQueuer = {}
@updateMerger = @updateMerger =
deleteUpdate: (user_id, path, source, cb)->cb() deleteUpdate: (user_id, project_id, path, source, cb)->cb()
mergeUpdate:(user_id, project_id, path, update, source, cb)->cb() mergeUpdate:(user_id, project_id, path, update, source, cb)->cb()
@editorController = {} @editorController = {}
@project_id = "dsjajilknaksdn" @project_id = "dsjajilknaksdn"
@ -107,11 +107,13 @@ describe 'TpdsUpdateHandler', ->
it 'should call deleteEntity in the collaberation manager', (done)-> it 'should call deleteEntity in the collaberation manager', (done)->
path = "/delete/this" path = "/delete/this"
update = {} update = {}
@updateMerger.deleteUpdate = sinon.stub().callsArg(3) @updateMerger.deleteUpdate = sinon.stub().callsArg(4)
@handler.deleteUpdate @user_id, @project.name, path, @source, => @handler.deleteUpdate @user_id, @project.name, path, @source, =>
@projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal false @projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal false
@updateMerger.deleteUpdate.calledWith(@project_id, path, @source).should.equal true @updateMerger.deleteUpdate
.calledWith(@user_id, @project_id, path, @source)
.should.equal true
done() done()
it 'should mark the project as deleted by external source if path is a single slash', (done)-> it 'should mark the project as deleted by external source if path is a single slash', (done)->

View file

@ -145,13 +145,13 @@ describe 'UpdateMerger :', ->
it 'should get the element id', -> it 'should get the element id', ->
@projectLocator.findElementByPath = sinon.spy() @projectLocator.findElementByPath = sinon.spy()
@updateMerger.deleteUpdate @project_id, @path, @source, -> @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
@projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true @projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true
it 'should delete the entity in the editor controller with the correct type', (done)-> it 'should delete the entity in the editor controller with the correct type', (done)->
@entity.lines = [] @entity.lines = []
mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source).callsArg(4) mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source, @user_id).callsArg(5)
@updateMerger.deleteUpdate @project_id, @path, @source, -> @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
mock.verify() mock.verify()
done() done()

View file

@ -0,0 +1,134 @@
Path = require 'path'
SandboxedModule = require "sandboxed-module"
modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryV2Manager'
sinon = require("sinon")
expect = require("chai").expect
describe "HistoryV2Manager", ->
beforeEach ->
@moment = {}
@ColorManager = {}
SandboxedModule.require modulePath, globals:
"define": (dependencies, builder) =>
@HistoryV2Manager = builder(@moment, @ColorManager)
@scope =
$watch: sinon.stub()
$on: sinon.stub()
@ide = {}
@historyManager = new @HistoryV2Manager(@ide, @scope)
it "should setup the history scope on intialization", ->
expect(@scope.history).to.deep.equal({
isV2: true
updates: []
nextBeforeTimestamp: null
atEnd: false
selection: {
updates: []
pathname: null
range: {
fromV: null
toV: null
}
}
diff: null
})
describe "_perDocSummaryOfUpdates", ->
it "should return the range of updates for the docs", ->
result = @historyManager._perDocSummaryOfUpdates([{
pathnames: ["main.tex"]
fromV: 7, toV: 9
},{
pathnames: ["main.tex", "foo.tex"]
fromV: 4, toV: 6
},{
pathnames: ["main.tex"]
fromV: 3, toV: 3
},{
pathnames: ["foo.tex"]
fromV: 0, toV: 2
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 3, toV: 9 },
"foo.tex": { fromV: 0, toV: 6 }
})
it "should track renames", ->
result = @historyManager._perDocSummaryOfUpdates([{
pathnames: ["main2.tex"]
fromV: 5, toV: 9
},{
project_ops: [{
rename: {
pathname: "main1.tex",
newPathname: "main2.tex"
}
}],
fromV: 4, toV: 4
},{
pathnames: ["main1.tex"]
fromV: 3, toV: 3
},{
project_ops: [{
rename: {
pathname: "main0.tex",
newPathname: "main1.tex"
}
}],
fromV: 2, toV: 2
},{
pathnames: ["main0.tex"]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
"main0.tex": { fromV: 0, toV: 9 }
})
it "should track single renames", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
rename: {
pathname: "main1.tex",
newPathname: "main2.tex"
}
}],
fromV: 4, toV: 5
}])
expect(result).to.deep.equal({
"main1.tex": { fromV: 4, toV: 5 }
})
it "should track additions", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
add:
pathname: "main.tex"
}]
fromV: 0, toV: 1
}, {
pathnames: ["main.tex"]
fromV: 1, toV: 4
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 0, toV: 4 }
})
it "should track single additions", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
add:
pathname: "main.tex"
}]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 0, toV: 1 }
})