diff --git a/services/web/.gitignore b/services/web/.gitignore
index a48481690a..5ab3a0e3f6 100644
--- a/services/web/.gitignore
+++ b/services/web/.gitignore
@@ -39,6 +39,7 @@ data/*
app.js
app/js/*
test/unit/js/*
+test/unit_frontend/js/*
test/smoke/js/*
test/acceptance/js/*
cookies.txt
@@ -67,6 +68,11 @@ public/minjs/
Gemfile.lock
+public/stylesheets/ol-style.*.css
+public/stylesheets/style.*.css
+public/js/libs/require*.js
+
+
*.swp
.DS_Store
diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee
index c49add4a91..a0000360b3 100644
--- a/services/web/Gruntfile.coffee
+++ b/services/web/Gruntfile.coffee
@@ -205,12 +205,12 @@ module.exports = (grunt) ->
modules: [
{
name: "main",
- exclude: ["libs"]
+ exclude: ["libraries"]
}, {
name: "ide",
- exclude: ["libs", "pdfjs-dist/build/pdf"]
- }, {
- name: "libs"
+ exclude: ["pdfjs-dist/build/pdf", "libraries"]
+ },{
+ name: "libraries"
},{
name: "ace/mode-latex"
},{
diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile
index 6421296330..152d980787 100644
--- a/services/web/Jenkinsfile
+++ b/services/web/Jenkinsfile
@@ -60,7 +60,7 @@ pipeline {
sh 'git config --global core.logallrefupdates false'
sh 'mv app/views/external/robots.txt public/robots.txt'
sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html'
- sh 'npm install'
+ sh 'npm --quiet install'
sh 'npm rebuild'
// It's too easy to end up shrinkwrapping to an outdated version of translations.
// Ensure translations are always latest, regardless of shrinkwrap
@@ -71,16 +71,9 @@ pipeline {
}
}
- stage('Unit Tests') {
+ stage('Test') {
steps {
- sh 'make clean install' // Removes js files, so do before compile
- sh 'make test_unit MOCHA_ARGS="--reporter=tap"'
- }
- }
-
- stage('Acceptance Tests') {
- steps {
- sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"'
+ sh 'make ci'
}
}
@@ -155,6 +148,10 @@ pipeline {
}
post {
+ always {
+ sh 'make ci_clean'
+ }
+
failure {
mail(from: "${EMAIL_ALERT_FROM}",
to: "${EMAIL_ALERT_TO}",
diff --git a/services/web/Makefile b/services/web/Makefile
index 98695a8c1f..5db188e2b7 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -16,9 +16,10 @@ add_dev: docker-shared.yml
$(NPM) install --save-dev ${P}
install: docker-shared.yml
+ bin/generate_volumes_file
$(NPM) install
-clean:
+clean: ci_clean
rm -f app.js
rm -rf app/js
rm -rf test/unit/js
@@ -30,9 +31,8 @@ clean:
rm -rf $$dir/test/unit/js; \
rm -rf $$dir/test/acceptance/js; \
done
- # Regenerate docker-shared.yml - not stictly a 'clean',
- # but lets `make clean install` work nicely
- bin/generate_volumes_file
+
+ci_clean:
# Deletes node_modules volume
docker-compose down --volumes
@@ -40,11 +40,14 @@ clean:
docker-shared.yml:
bin/generate_volumes_file
-test: test_unit test_acceptance
+test: test_unit test_frontend test_acceptance
test_unit: docker-shared.yml
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_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
cd $(MODULE) && make test_acceptance
+ci:
+ MOCHA_ARGS="--reporter tap" \
+ $(MAKE) install test
+
.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_run
+ test_acceptance_run ci ci_clean
diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
index f7dd9f3e17..e18b8c8123 100644
--- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
+++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
@@ -205,10 +205,10 @@ module.exports = DocumentUpdaterHandler =
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
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)
- fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles)
+ docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
+ fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
timer = new metrics.Timer("set-document")
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}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
- _getRenameUpdates: (entityType, oldEntities, newEntities) ->
+ _getUpdates: (entityType, oldEntities, newEntities) ->
oldEntities ||= []
newEntities ||= []
updates = []
@@ -255,6 +255,16 @@ module.exports = DocumentUpdaterHandler =
pathname: oldEntity.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
PENDINGUPDATESKEY = "PendingUpdates"
diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee
index f66094f9e8..532f515a10 100644
--- a/services/web/app/coffee/Features/Editor/EditorController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorController.coffee
@@ -105,19 +105,19 @@ module.exports = EditorController =
async.series jobs, (err)->
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)->
if err?
logger.err err:err, project_id:project_id, "could not get lock to deleteEntity"
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, ()->
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"
Metrics.inc "editor.delete-entity"
- ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, (err)->
+ ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, userId, (err)->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity"
return callback(err)
diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
index cbc296b69f..16b1a79e31 100644
--- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
+++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee
@@ -147,6 +147,7 @@ module.exports = EditorHttpController =
project_id = req.params.Project_id
entity_id = req.params.entity_id
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?
res.sendStatus 204
diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee
index 70e0828160..76476c8248 100644
--- a/services/web/app/coffee/Features/History/HistoryController.coffee
+++ b/services/web/app/coffee/Features/History/HistoryController.coffee
@@ -5,35 +5,13 @@ AuthenticationController = require "../Authentication/AuthenticationController"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
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) ->) ->
project_id = req.params?.Project_id
# find out which type of history service this project uses
ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err?
- if project?.overleaf?.history?.display
+ history = project.overleaf?.history
+ if history?.id? and history?.display
req.useProjectHistory = true
else
req.useProjectHistory = false
@@ -58,7 +36,7 @@ module.exports = HistoryController =
buildHistoryServiceUrl: (useProjectHistory) ->
# choose a history service, either document-level (trackchanges)
# or project-level (project_history)
- if settings.apis.project_history?.enabled && useProjectHistory
+ if useProjectHistory
return settings.apis.project_history.url
else
return settings.apis.trackchanges.url
diff --git a/services/web/app/coffee/Features/History/HistoryManager.coffee b/services/web/app/coffee/Features/History/HistoryManager.coffee
new file mode 100644
index 0000000000..75f552c907
--- /dev/null
+++ b/services/web/app/coffee/Features/History/HistoryManager.coffee
@@ -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
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index da22373870..fb27611bca 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -216,7 +216,7 @@ module.exports = ProjectController =
project: (cb)->
ProjectGetter.getProject(
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
)
user: (cb)->
@@ -351,6 +351,7 @@ module.exports = ProjectController =
themes: THEME_LIST
maxDocLength: Settings.max_doc_length
showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding
+ useV2History: !!project.overleaf?.history?.display
timer.done()
_buildProjectList: (allProjects, v1Projects = [])->
diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
index cd78cddc09..ceddc2ad61 100644
--- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
@@ -7,7 +7,7 @@ Project = require('../../models/Project').Project
Folder = require('../../models/Folder').Folder
ProjectEntityHandler = require('./ProjectEntityHandler')
ProjectDetailsHandler = require('./ProjectDetailsHandler')
-HistoryController = require('../History/HistoryController')
+HistoryManager = require('../History/HistoryManager')
User = require('../../models/User').User
fs = require('fs')
Path = require "path"
@@ -27,7 +27,7 @@ module.exports = ProjectCreationHandler =
if projectHistoryId?
ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, callback
else
- HistoryController.initializeProject (error, history) ->
+ HistoryManager.initializeProject (error, history) ->
return callback(error) if error?
ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, callback
diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
index 815e31eb27..d4640db966 100644
--- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
@@ -21,7 +21,7 @@ module.exports = ProjectDuplicator =
if !doc?._id?
return callback()
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?
logger.err err:err, "error copying doc"
return callback(err)
diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
index 446786fa57..30e573ea42 100644
--- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -149,14 +149,35 @@ module.exports = ProjectEntityHandler =
else
DocstoreManager.getDoc project_id, doc_id, options, callback
- addDoc: (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
- ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
+ addDoc: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
+ 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?
logger.err project_id:project_id, err:err, "error getting project for add doc"
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
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)=>
@@ -176,14 +197,7 @@ module.exports = ProjectEntityHandler =
rev: 0
}, (err) ->
return callback(err) if err?
- newDocs = [
- 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
+ callback(null, doc, folder_id, result?.path?.fileSystem)
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
@@ -192,37 +206,37 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
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) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add file"
return callback(err)
- ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, userId, callback
-
- addFileWithProject: (project, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
- project_id = project._id
- logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
- 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)=>
+ logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
+ 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 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)
- tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
- return callback(err) if err?
- newFiles = [
- file: fileRef
- path: result?.path?.fileSystem
- url: fileStoreUrl
- ]
- DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
- return callback(error) if error?
- callback null, fileRef, folder_id
+ ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
+ if err?
+ logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
+ 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) ->
+ return callback(err) if err?
+ callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
+
+ addFile: (project_id, folder_id, fileName, fsPath, userId, callback = (error, 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)->
self = ProjectEntityHandler
@@ -412,7 +426,7 @@ module.exports = ProjectEntityHandler =
callback()
- deleteEntity: (project_id, entity_id, entityType, callback = (error) ->)->
+ deleteEntity: (project_id, entity_id, entityType, userId, callback = (error) ->)->
self = @
logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity"
if !entityType?
@@ -423,7 +437,7 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
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?
tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) ->
return callback(error) if error?
@@ -456,17 +470,17 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
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)
- ProjectEntityHandler._cleanUpFile project, entity, callback
+ ProjectEntityHandler._cleanUpFile project, entity, path, userId, callback
else if (entityType.indexOf("doc") != -1)
- ProjectEntityHandler._cleanUpDoc project, entity, callback
+ ProjectEntityHandler._cleanUpDoc project, entity, path, userId, callback
else if (entityType.indexOf("folder") != -1)
- ProjectEntityHandler._cleanUpFolder project, entity, callback
+ ProjectEntityHandler._cleanUpFolder project, entity, path, userId, callback
else
callback()
- _cleanUpDoc: (project, doc, callback = (error) ->) ->
+ _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
project_id = project._id.toString()
doc_id = doc._id.toString()
unsetRootDocIfRequired = (callback) =>
@@ -483,26 +497,33 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
DocstoreManager.deleteDoc project_id, doc_id, (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()
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 = []
for doc in folder.docs
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
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
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
diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee
index c78b588e8a..78e3f12ed6 100644
--- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee
+++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee
@@ -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"
return projectDeleter.markAsDeletedByExternalSource project._id, callback
else
- updateMerger.deleteUpdate project._id, path, source, (err)->
+ updateMerger.deleteUpdate user_id, project._id, path, source, (err)->
callback(err)
diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
index 010594d16a..e49c52aab2 100644
--- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
+++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee
@@ -32,13 +32,13 @@ module.exports =
else
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)->
if err? || !element?
logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
return callback()
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"
callback()
diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 54e04f51e5..c99cc2935e 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -12,7 +12,7 @@ Modules = require "./Modules"
Url = require "url"
PackageVersions = require "./PackageVersions"
htmlEncoder = new require("node-html-encoder").Encoder("numerical")
-fingerprints = {}
+hashedFiles = {}
Path = require 'path'
Features = require "./Features"
@@ -30,43 +30,42 @@ getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}"
exists = fs.existsSync filePath
if exists
- content = fs.readFileSync filePath
+ content = fs.readFileSync filePath, "UTF-8"
return content
else
- logger.log filePath:filePath, "file does not exist for fingerprints"
+ logger.log filePath:filePath, "file does not exist for hashing"
return ""
-logger.log "Generating file fingerprints..."
pathList = [
- ["#{jsPath}libs/#{fineuploader}.js"]
- ["#{jsPath}libs/require.js"]
- ["#{jsPath}ide.js"]
- ["#{jsPath}main.js"]
- ["#{jsPath}libs.js"]
- ["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"]
- ["#{jsPath}libs/#{pdfjs}/pdf.js"]
- ["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
- ["#{jsPath}libs/#{pdfjs}/compatibility.js"]
- ["/stylesheets/style.css"]
- ["/stylesheets/ol-style.css"]
+ "#{jsPath}libs/require.js"
+ "#{jsPath}ide.js"
+ "#{jsPath}main.js"
+ "#{jsPath}libraries.js"
+ "/stylesheets/style.css"
+ "/stylesheets/ol-style.css"
]
-for paths in pathList
- contentList = _.map(paths, getFileContent)
- content = contentList.join("")
- hash = crypto.createHash("md5").update(content).digest("hex")
- _.each paths, (filePath)->
- logger.log "#{filePath}: #{hash}"
- fingerprints[filePath] = hash
+if !Settings.useMinifiedJs
+ logger.log "not using minified JS, not hashing static files"
+else
+ logger.log "Generating file hashes..."
+ for path in pathList
+ content = getFileContent(path)
+ 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) ->
- if fingerprints[path]?
- return fingerprints[path]
- else
- logger.err "No fingerprint for file: #{path}"
- return ""
+ hashPath = splitPath.join("/")
+ hashedFiles[path] = hashPath
-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?
darkCdnAvailable = Settings.cdn?.web?.darkHost?
@@ -120,29 +119,35 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
res.locals.lib = PackageVersions.lib
+
+
res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile)
- doFingerPrint = opts.fingerprint != false
+ if opts.hashedPath && hashedFiles[path]?
+ path = hashedFiles[path]
if !opts.qs?
opts.qs = {}
- if !opts.qs?.fingerprint? and doFingerPrint
- opts.qs.fingerprint = getFingerprint(path)
-
if opts.cdn != false
path = Url.resolve(staticFilesBase, path)
qs = querystring.stringify(opts.qs)
+ if opts.removeExtension == true
+ path = path.slice(0,-3)
+
if qs? and qs.length > 0
path = path + "?" + qs
return path
- res.locals.buildCssPath = (cssFile)->
+ res.locals.buildCssPath = (cssFile, opts)->
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)->
path = Path.join("/img/", imgFile)
@@ -227,10 +232,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
return req.query?[field]
next()
- webRouter.use (req, res, next)->
- res.locals.fingerprint = getFingerprint
- next()
-
webRouter.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee
index e0013f8c5f..b434d35ab9 100644
--- a/services/web/app/coffee/models/Project.coffee
+++ b/services/web/app/coffee/models/Project.coffee
@@ -56,6 +56,7 @@ ProjectSchema = new Schema
read_token : { type: String }
history :
id : { type: Number }
+ display : { type: Boolean }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id?
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index f8ec65ac53..2199bdd08e 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -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/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.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
@@ -324,6 +325,10 @@ module.exports = class Router
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-internal', (req, res, next) -> throw new Error("Test error")
webRouter.get '/oops-mongo', (req, res, next) ->
diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug
index e12d7d6f81..151df0b0ac 100644
--- a/services/web/app/views/layout.pug
+++ b/services/web/app/views/layout.pug
@@ -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="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
@@ -57,7 +57,7 @@ html(itemscope, itemtype='http://schema.org/Product')
script(type="text/javascript").
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").
var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined'
@@ -68,7 +68,7 @@ html(itemscope, itemtype='http://schema.org/Product')
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.
window.sharelatex = {
@@ -95,7 +95,11 @@ html(itemscope, itemtype='http://schema.org/Product')
cdnDomain : '!{settings.templates.cdnDomain}',
indexName : '!{settings.templates.indexName}'
}
-
+
+ - if (settings.overleaf && settings.overleaf.useOLFreeTrial)
+ script.
+ window.redirectToOLFreeTrialUrl = '!{settings.overleaf.host}/users/trial'
+
body
if(settings.recaptcha)
script(src="https://www.google.com/recaptcha/api.js?render=explicit")
@@ -142,9 +146,10 @@ html(itemscope, itemtype='http://schema.org/Product')
window.requirejs = {
"paths" : {
"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":{
"moment":{
"noGlobal": true
@@ -152,9 +157,9 @@ html(itemscope, itemtype='http://schema.org/Product')
}
};
script(
- data-main=buildJsPath('main.js', {fingerprint:false}),
+ data-main=buildJsPath('main.js', {hashedPath:false}),
baseurl=fullJsPath,
- src=buildJsPath('libs/require.js')
+ src=buildJsPath('libs/require.js', {hashedPath:true})
)
include contact-us-modal
diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug
index ec1d129714..4a35b930ff 100644
--- a/services/web/app/views/project/editor.pug
+++ b/services/web/app/views/project/editor.pug
@@ -98,14 +98,14 @@ block requirejs
script(type="text/javascript" src='/socket.io/socket.io.js')
//- don't use cdn for workers
- - var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false})
- - var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
- - var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false,fingerprint:false})
+ - var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false})
+ - var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false})
+ - var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false})
//- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>'
//- and doesn't prematurely end the script tag.
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").
window.data = JSON.parse($("#data").text());
@@ -126,14 +126,15 @@ block requirejs
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"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')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"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,
"shim": {
"pdfjs-dist/build/pdf": {
@@ -155,14 +156,13 @@ block requirejs
}
}
};
- window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
window.aceWorkerPath = "#{aceWorkerPath}";
window.pdfCMapsPath = "#{pdfCMapsPath}"
window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
script(
- data-main=buildJsPath("ide.js", {fingerprint:false}),
+ data-main=buildJsPath("ide.js", {hashedPath:false}),
baseurl=fullJsPath,
- data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
- src=buildJsPath('libs/require.js')
+ data-ace-base=buildJsPath(lib('ace')),
+ src=buildJsPath('libs/require.js', {hashedPath:true})
)
diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index 4806b0a9b8..6621cdb2d2 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -134,8 +134,17 @@ div#history(ng-show="ui.view == 'history'")
div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
- div.docs(ng-repeat="(doc_id, doc) in update.docs")
- span.doc {{ doc.entity.name }}
+ div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
+ | 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 }} → {{ project_op.rename.newPathname }}
+ div(ng-if="project_op.add")
+ .action Created
+ .doc {{ project_op.add.pathname }}
div.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%)'}")
@@ -165,8 +174,8 @@ div#history(ng-show="ui.view == 'history'")
'other': 'changes'\
}"
)
- | in {{history.diff.doc.name}}
- .toolbar-right
+ | in {{history.diff.pathname}}
+ .toolbar-right(ng-if="!history.isV2")
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"
diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug
index f246ad34d4..a4bb240c4b 100644
--- a/services/web/app/views/project/list/item.pug
+++ b/services/web/app/views/project/list/item.pug
@@ -2,6 +2,7 @@
input.select-item(
select-individual,
type="checkbox",
+ ng-disabled="shouldDisableCheckbox(project)",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"
diff --git a/services/web/bin/compile_app b/services/web/bin/compile_backend
similarity index 100%
rename from services/web/bin/compile_app
rename to services/web/bin/compile_backend
diff --git a/services/web/bin/compile_frontend b/services/web/bin/compile_frontend
new file mode 100755
index 0000000000..bb1dde7dbb
--- /dev/null
+++ b/services/web/bin/compile_frontend
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e;
+COFFEE=node_modules/.bin/coffee
+echo Compiling public/coffee;
+$COFFEE -o public/js -c public/coffee;
diff --git a/services/web/bin/compile_frontend_tests b/services/web/bin/compile_frontend_tests
new file mode 100755
index 0000000000..0351ad70cd
--- /dev/null
+++ b/services/web/bin/compile_frontend_tests
@@ -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;
diff --git a/services/web/bin/frontend_test b/services/web/bin/frontend_test
new file mode 100755
index 0000000000..599055803a
--- /dev/null
+++ b/services/web/bin/frontend_test
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e;
+MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
+$MOCHA "$@" test/unit_frontend/js
+
diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index 65a8bf1e91..844bffae3c 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -111,7 +111,8 @@ module.exports = settings =
trackchanges:
url : "http://localhost:3015"
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"
docstore:
url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"
diff --git a/services/web/docker-shared.template.yml b/services/web/docker-shared.template.yml
index d697e59e96..8acc63ef81 100644
--- a/services/web/docker-shared.template.yml
+++ b/services/web/docker-shared.template.yml
@@ -13,17 +13,15 @@ services:
- ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json
- node_modules:/app/node_modules
- ./bin:/app/bin
- # Copying the whole public dir is fine for now, and needed for
- # some unit tests to pass, but we will want to isolate the coffee
- # and vendor js files, so that the compiled js files are not written
- # back to the local filesystem.
- - ./public:/app/public
+ - ./public/coffee:/app/public/coffee:ro
+ - ./public/js/ace-1.2.5:/app/public/js/ace-1.2.5
- ./app.coffee:/app/app.coffee:ro
- ./app/coffee:/app/app/coffee:ro
- ./app/templates:/app/app/templates:ro
- ./app/views:/app/app/views:ro
- ./config:/app/config
- ./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/files:/app/test/acceptance/files:ro
- ./test/smoke/coffee:/app/test/smoke/coffee:ro
diff --git a/services/web/package.json b/services/web/package.json
index a1cf0fd6e0..398ce3dc78 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -14,11 +14,15 @@
"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": "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:frontend_tests": "bin/compile_frontend_tests",
"compile:acceptance_tests": "bin/compile_acceptance_tests",
- "compile:app": "bin/compile_app",
- "start": "npm -q run compile:app && node app.js"
+ "compile:frontend": "bin/compile_frontend",
+ "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": {
"archiver": "0.9.0",
diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee
index 03d0ada365..c3847c9342 100644
--- a/services/web/public/coffee/base.coffee
+++ b/services/web/public/coffee/base.coffee
@@ -1,5 +1,5 @@
define [
- "libs"
+ "libraries"
"modules/recursionHelper"
"modules/errorCatcher"
"modules/localStorage"
diff --git a/services/web/public/coffee/directives/selectAll.coffee b/services/web/public/coffee/directives/selectAll.coffee
index 4194d050e8..eb0bd05e86 100644
--- a/services/web/public/coffee/directives/selectAll.coffee
+++ b/services/web/public/coffee/directives/selectAll.coffee
@@ -49,18 +49,21 @@ define [
selectAllListController.clearSelectAllState()
scope.$on "select-all:select", () ->
+ return if element.prop('disabled')
ignoreChanges = true
scope.$apply () ->
scope.ngModel = true
ignoreChanges = false
scope.$on "select-all:deselect", () ->
+ return if element.prop('disabled')
ignoreChanges = true
scope.$apply () ->
scope.ngModel = false
ignoreChanges = false
scope.$on "select-all:row-clicked", () ->
+ return if element.prop('disabled')
ignoreChanges = true
scope.$apply () ->
scope.ngModel = !scope.ngModel
@@ -75,4 +78,4 @@ define [
link: (scope, element, attrs) ->
element.on "click", (e) ->
scope.$broadcast "select-all:row-clicked"
- }
\ No newline at end of file
+ }
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index dbf6a2724e..3d514d093c 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -5,6 +5,7 @@ define [
"ide/editor/EditorManager"
"ide/online-users/OnlineUsersManager"
"ide/history/HistoryManager"
+ "ide/history/HistoryV2Manager"
"ide/permissions/PermissionsManager"
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
@@ -44,6 +45,7 @@ define [
EditorManager
OnlineUsersManager
HistoryManager
+ HistoryV2Manager
PermissionsManager
PdfManager
BinaryFilesManager
@@ -137,7 +139,10 @@ define [
ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(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.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
index 6908b25bce..ff5d1554e2 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -33,7 +33,7 @@ define [
if !ace.config._moduleUrl?
ace.config._moduleUrl = ace.config.moduleUrl
ace.config.moduleUrl = (args...) ->
- url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
+ url = ace.config._moduleUrl(args...)
return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) ->
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index 6b42714e79..f896fbb8b4 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -100,6 +100,7 @@ define [
end_ts: end_ts
doc: doc
error: false
+ pathname: doc.name
}
if !doc.deleted
@@ -190,8 +191,10 @@ define [
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates
+ update.pathnames = [] # Used for display
for doc_id, doc of update.docs or {}
doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
+ update.pathnames.push doc.entity.name
for user in update.meta.users or []
if user?
diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
new file mode 100644
index 0000000000..8198738e16
--- /dev/null
+++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
@@ -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
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 17afd8bbcb..7bce498ba8 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -90,7 +90,9 @@ define [
# to block auto compiles. It also causes problems where server-provided
# linting errors aren't cleared after typing
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
# Extend remainder of timeout
autoCompileTimeout = setTimeout () ->
@@ -533,14 +535,6 @@ define [
else
$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) ->
# enable per-user containers by default
perUserCompile = true
diff --git a/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee b/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee
index ae8c049f69..3fcc5e416f 100644
--- a/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee
+++ b/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee
@@ -4,8 +4,3 @@ define [
App.controller "TrackChangesUpgradeModalController", ($scope, $modalInstance) ->
$scope.cancel = () ->
$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
\ No newline at end of file
diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libraries.coffee
similarity index 100%
rename from services/web/public/coffee/libs.coffee
rename to services/web/public/coffee/libraries.coffee
diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee
index 6144bea9ef..0cabc224a0 100644
--- a/services/web/public/coffee/main/account-upgrade.coffee
+++ b/services/web/public/coffee/main/account-upgrade.coffee
@@ -11,9 +11,12 @@ define [
w = window.open()
go = () ->
ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
- url = "/user/subscription/new?planCode=#{plan}&ssp=true"
- if couponCode?
- url = "#{url}&cc=#{couponCode}"
+ if window.redirectToOLFreeTrialUrl?
+ url = window.redirectToOLFreeTrialUrl
+ else
+ url = "/user/subscription/new?planCode=#{plan}&ssp=true"
+ if couponCode?
+ url = "#{url}&cc=#{couponCode}"
$scope.startedFreeTrial = true
switch source
@@ -27,7 +30,7 @@ define [
else
event_tracking.sendMB "subscription-start-trial", { source, plan }
-
+
w.location = url
if $scope.shouldABTestPlans
diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee
index f4a6951e4a..75d4767ec0 100644
--- a/services/web/public/coffee/main/project-list/project-list.coffee
+++ b/services/web/public/coffee/main/project-list/project-list.coffee
@@ -462,6 +462,9 @@ define [
App.controller "ProjectListItemController", ($scope) ->
+ $scope.shouldDisableCheckbox = (project) ->
+ $scope.filter == 'archived' && project.accessLevel != 'owner'
+
$scope.projectLink = (project) ->
if project.accessLevel == 'readAndWrite' and project.source == 'token'
"/#{project.tokens.readAndWrite}"
diff --git a/services/web/public/coffee/utils/underscore.coffee b/services/web/public/coffee/utils/underscore.coffee
index 7a1551f90b..944abccb5d 100644
--- a/services/web/public/coffee/utils/underscore.coffee
+++ b/services/web/public/coffee/utils/underscore.coffee
@@ -1,5 +1,5 @@
define [
- "libs"
+ "libraries"
], () ->
angular.module('underscore', []).factory '_', ->
return window._
diff --git a/services/web/public/stylesheets/app/editor/history.less b/services/web/public/stylesheets/app/editor/history.less
index b6dca4b7cc..2824fd2e32 100644
--- a/services/web/public/stylesheets/app/editor/history.less
+++ b/services/web/public/stylesheets/app/editor/history.less
@@ -169,9 +169,19 @@
font-size: 0.8rem;
line-height: @line-height-computed;
}
- .docs {
- font-weight: bold;
+ .doc {
font-size: 0.9rem;
+ font-weight: bold;
+ }
+ .action {
+ color: @gray;
+ text-transform: uppercase;
+ font-size: 0.7em;
+ margin-bottom: -2px;
+ margin-top: 2px;
+ &-edited {
+ margin-top: 0;
+ }
}
}
li.loading-changes, li.empty-message {
diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less
index e602d2d99b..d59ca93069 100644
--- a/services/web/public/stylesheets/app/editor/pdf.less
+++ b/services/web/public/stylesheets/app/editor/pdf.less
@@ -10,9 +10,13 @@
padding: 0 (@line-height-computed / 2);
}
+.pdf {
+ background-color: @pdf-bg;
+}
+
.pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled {
.full-size;
- top: 58px;
+ top: @pdf-top-offset;
}
.pdf-logs, .pdf-errors, .pdf-uncompiled, .pdf-validation-problems{
@@ -69,11 +73,11 @@
}
.pdfjs-viewer {
.full-size;
- background-color: @gray-lighter;
+ background-color: @pdfjs-bg;
overflow: scroll;
canvas, div.pdf-canvas {
background: white;
- box-shadow: black 0px 0px 10px;
+ box-shadow: @pdf-page-shadow-color 0px 0px 10px;
}
div.pdf-canvas.pdfng-empty {
background-color: white;
@@ -179,7 +183,7 @@
cursor: pointer;
.line-no {
float: right;
- color: @gray;
+ color: @log-line-no-color;
font-weight: 700;
.fa {
@@ -203,16 +207,25 @@
}
}
- &.alert-danger:hover {
- background-color: darken(@alert-danger-bg, 5%);
+ &.alert-danger {
+ background-color: tint(@alert-danger-bg, 15%);
+ &:hover {
+ background-color: @alert-danger-bg;
+ }
}
- &.alert-warning:hover {
- background-color: darken(@alert-warning-bg, 5%);
+ &.alert-warning {
+ background-color: tint(@alert-warning-bg, 15%);
+ &:hover {
+ background-color: @alert-warning-bg;
+ }
}
- &.alert-info:hover {
- background-color: darken(@alert-info-bg, 5%);
+ &.alert-info {
+ background-color: tint(@alert-info-bg, 15%);
+ &:hover {
+ background-color: @alert-info-bg;
+ }
}
}
@@ -354,22 +367,22 @@
}
.alert-danger & {
- color: @alert-danger-border;
+ color: @state-danger-border;
}
.alert-warning & {
- color: @alert-warning-border;
+ color: @state-warning-border;
}
.alert-info & {
- color: @alert-info-border;
+ color: @state-info-border;
}
}
&-text,
&-feedback-label {
- color: @gray-dark;
+ color: @log-hints-color;
font-size: 0.9rem;
margin-bottom: 20px;
}
@@ -394,25 +407,25 @@
&-actions a,
&-text a {
.alert-danger & {
- color: @alert-danger-text;
+ color: @state-danger-text;
}
.alert-warning & {
- color: @alert-warning-text;
+ color: @state-warning-text;
}
.alert-info & {
- color: @alert-info-text;
+ color: @state-info-text;
}
}
&-feedback {
- color: @gray-dark;
+ color: @log-hints-color;
float: right;
}
&-extra-feedback {
- color: @gray-dark;
+ color: @log-hints-color;
font-size: 0.8rem;
margin-top: 10px;
padding-bottom: 5px;
diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less
index e164f4fc02..8370e40959 100644
--- a/services/web/public/stylesheets/app/editor/toolbar.less
+++ b/services/web/public/stylesheets/app/editor/toolbar.less
@@ -129,11 +129,11 @@
}
.toolbar-small-mixin() {
- height: 32px;
+ height: @toolbar-small-height;
}
.toolbar-tall-mixin() {
- height: 58px;
+ height: @toolbar-tall-height;
padding-top: 10px;
}
.toolbar-alt-mixin() {
diff --git a/services/web/public/stylesheets/components/alerts.less b/services/web/public/stylesheets/components/alerts.less
index 527c97b151..6ed93d29d4 100755
--- a/services/web/public/stylesheets/components/alerts.less
+++ b/services/web/public/stylesheets/components/alerts.less
@@ -10,7 +10,7 @@
padding: @alert-padding;
margin-bottom: @line-height-computed;
border-left: 3px solid transparent;
- // border-radius: @alert-border-radius;
+ border-radius: @alert-border-radius;
// Headings for larger alerts
h4 {
diff --git a/services/web/public/stylesheets/components/card.less b/services/web/public/stylesheets/components/card.less
index 1e06fbe3b4..ac43c038b7 100644
--- a/services/web/public/stylesheets/components/card.less
+++ b/services/web/public/stylesheets/components/card.less
@@ -1,7 +1,6 @@
.card {
background-color: white;
border-radius: @border-radius-base;
- -webkit-box-shadow: @card-box-shadow;
box-shadow: @card-box-shadow;
padding: @line-height-computed;
.page-header {
diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less
index 5885235ddc..f354893661 100644
--- a/services/web/public/stylesheets/core/_common-variables.less
+++ b/services/web/public/stylesheets/core/_common-variables.less
@@ -545,7 +545,7 @@
//## Define alert colors, border radius, and padding.
@alert-padding: 15px;
-@alert-border-radius: @border-radius-base;
+@alert-border-radius: 0;
@alert-link-font-weight: bold;
@alert-success-bg: @state-success-bg;
@@ -899,20 +899,22 @@
@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-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
-@file-tree-bg : transparent;
-@file-tree-line-height : 2.6;
-@file-tree-item-color : @gray-darker;
-@file-tree-item-toggle-color : @gray;
-@file-tree-item-icon-color : @gray-light;
-@file-tree-item-input-color : inherit;
-@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
-@file-tree-item-hover-bg : @gray-lightest;
-@file-tree-item-selected-bg : transparent;
-@file-tree-multiselect-bg : lighten(@brand-info, 40%);
-@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
+@file-tree-bg : transparent;
+@file-tree-line-height : 2.6;
+@file-tree-item-color : @gray-darker;
+@file-tree-item-toggle-color : @gray;
+@file-tree-item-icon-color : @gray-light;
+@file-tree-item-input-color : inherit;
+@file-tree-item-folder-color : lighten(desaturate(@link-color, 10%), 5%);
+@file-tree-item-hover-bg : @gray-lightest;
+@file-tree-item-selected-bg : transparent;
+@file-tree-multiselect-bg : lighten(@brand-info, 40%);
+@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
// Editor resizers
@editor-resizer-bg-color : #F4F4F4;
@@ -935,6 +937,14 @@
@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
@tag-border-radius : 0.25em;
@tag-bg-color : @label-default-bg;
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index def0e408f9..8433c374b8 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -67,6 +67,28 @@
@btn-info-bg : @ol-blue;
@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
@tag-border-radius : 9999px;
@tag-bg-color : @ol-green;
@@ -179,21 +201,25 @@
@toolbar-icon-btn-hover-shadow : none;
@toolbar-border-bottom : 1px solid @toolbar-border-color;
@toolbar-icon-btn-hover-boxshadow : none;
+<<<<<<< HEAD
@toolbar-font-size : 13px;
+=======
+>>>>>>> master
// Editor file-tree
-@file-tree-bg : @ol-blue-gray-4;
-@file-tree-line-height : 2.05;
-@file-tree-item-color : #FFF;
-@file-tree-item-input-color : @ol-blue-gray-5;
-@file-tree-item-toggle-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-hover-bg : @ol-blue-gray-5;
-@file-tree-item-selected-bg : @ol-green;
-@file-tree-multiselect-bg : @ol-blue;
-@file-tree-multiselect-hover-bg : @ol-dark-blue;
-@file-tree-droppable-bg-color : tint(@ol-green, 5%);
+@file-tree-bg : @ol-blue-gray-4;
+@file-tree-line-height : 2.05;
+@file-tree-item-color : #FFF;
+@file-tree-item-input-color : @ol-blue-gray-5;
+@file-tree-item-toggle-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-hover-bg : @ol-blue-gray-5;
+@file-tree-item-selected-bg : @ol-green;
+@file-tree-multiselect-bg : @ol-blue;
+@file-tree-multiselect-hover-bg : @ol-dark-blue;
+@file-tree-droppable-bg-color : tint(@ol-green, 5%);
+
// Editor resizers
@editor-resizer-bg-color : @ol-blue-gray-6;
@editor-resizer-bg-color-dragging : transparent;
@@ -216,6 +242,16 @@
@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-darker: #252525;
@gray-dark: #505050;
@@ -235,9 +271,9 @@
@brand-primary: @ol-green;
@brand-success: @green;
-@brand-info: @ol-dark-green;
+@brand-info: @ol-blue;
@brand-warning: @orange;
-@brand-danger: #E03A06;
+@brand-danger: @ol-red;
@editor-loading-logo-padding-top: 115.44%;
@editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);
diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
index e54d4fac9d..e037deb5a3 100644
--- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
@@ -59,7 +59,7 @@ describe "ProjectStructureChanges", ->
@dup_project_id = body.project_id
done()
- it "should version the dosc created", ->
+ it "should version the docs created", ->
updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
expect(updates.length).to.equal(2)
_.each updates, (update) =>
@@ -91,6 +91,7 @@ describe "ProjectStructureChanges", ->
throw error if error?
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to add doc #{res.statusCode}")
+ @example_doc_id = body._id
done()
it "should version the doc added", ->
@@ -162,6 +163,8 @@ describe "ProjectStructureChanges", ->
if res.statusCode < 200 || res.statusCode >= 300
throw new Error("failed to upload file #{res.statusCode}")
+ @example_file_id = JSON.parse(body).entity_id
+
updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates
expect(updates.length).to.equal(1)
update = updates[0]
@@ -199,6 +202,120 @@ describe "ProjectStructureChanges", ->
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", ->
before (done) ->
@tpds_project_name = "tpds-project-#{new ObjectId().toString()}"
@@ -305,3 +422,25 @@ describe "ProjectStructureChanges", ->
done()
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()
+
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
index b00cd6b173..b21fb1adab 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee
@@ -33,6 +33,9 @@ module.exports = MockDocUpdaterApi =
@addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates)
res.sendStatus 200
+ app.delete "/project/:project_id/doc/:doc_id", (req, res, next) =>
+ res.send 204
+
app.listen 3003, (error) ->
throw error if error?
.on "error", (error) ->
diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
index c5b003ac75..631538fd89 100644
--- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee
@@ -23,6 +23,16 @@ module.exports = MockDocStoreApi =
docs = (doc for doc_id, doc of @docs[req.params.project_id])
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) ->
throw error if error?
.on "error", (error) ->
diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
index 14ccaa3a33..a4e0a4dc53 100644
--- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
+++ b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
@@ -393,7 +393,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history disabled", ->
beforeEach ->
- @settings.apis.project_history.enabled = false
+ @settings.apis.project_history.sendProjectStructureOps = false
@request.post = sinon.stub()
@handler.updateProjectStructure @project_id, @user_id, {}, @callback
@@ -406,7 +406,7 @@ describe 'DocumentUpdaterHandler', ->
describe "with project history enabled", ->
beforeEach ->
- @settings.apis.project_history.enabled = true
+ @settings.apis.project_history.sendProjectStructureOps = true
@url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
@@ -478,14 +478,22 @@ describe 'DocumentUpdaterHandler', ->
.should.equal true
done()
- describe "when a doc has been deleted", ->
- it 'should do nothing', (done) ->
+ describe "when an entity has been deleted", ->
+ it 'should end the structure update to the document updater', (done) ->
@docId = new ObjectId()
@changes = oldDocs: [
{ path: '/foo', docLines: 'a\nb', doc: _id: @docId }
]
+ docUpdates = [
+ id: @docId.toString(),
+ pathname: '/foo',
+ newPathname: ''
+ ]
+
@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()
diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
index 4e1af79b46..85760f510d 100644
--- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee
@@ -376,58 +376,53 @@ describe "EditorController", ->
err.should.equal "timed out"
done()
-
describe "deleteEntity", ->
-
beforeEach ->
@LockManager.getLock.callsArgWith(1)
@LockManager.releaseLock.callsArgWith(1)
- @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(4)
+ @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5)
it "should call deleteEntityWithoutLock", (done)->
- @EditorController.deleteEntity @project_id, @entity_id, @type, @source, =>
- @EditorController.deleteEntityWithoutLock.calledWith(@project_id, @entity_id, @type, @source).should.equal true
+ @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, =>
+ @EditorController.deleteEntityWithoutLock
+ .calledWith(@project_id, @entity_id, @type, @source, @user_id)
+ .should.equal true
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
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
done()
it "should error if it can't cat the lock", (done)->
@LockManager.getLock = sinon.stub().callsArgWith(1, "timed out")
- @EditorController.deleteEntity @project_id, @entity_id, @type, @source, (err)=>
- expect(err).to.exist
- err.should.equal "timed out"
- done()
-
-
+ @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) =>
+ expect(error).to.exist
+ error.should.equal "timed out"
+ done()
describe 'deleteEntityWithoutLock', ->
- beforeEach ->
- @ProjectEntityHandler.deleteEntity = (project_id, entity_id, type, callback)-> callback()
+ beforeEach (done) ->
@entity_id = "entity_id_here"
@type = "doc"
@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)->
- mock = sinon.mock(@ProjectEntityHandler).expects("deleteEntity").withArgs(@project_id, @entity_id, @type).callsArg(3)
+ it 'should delete the folder using the project entity handler', ->
+ @ProjectEntityHandler.deleteEntity
+ .calledWith(@project_id, @entity_id, @type, @user_id)
+ .should.equal.true
- @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, ->
- mock.verify()
- done()
-
- 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()
+ it 'notify users an entity has been deleted', ->
+ @EditorRealTimeController.emitToRoom
+ .calledWith(@project_id, "removeEntity", @entity_id, @source)
+ .should.equal true
describe "getting a list of project paths", ->
diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
index e05846c5d5..38419e6b46 100644
--- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
+++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee
@@ -331,12 +331,12 @@ describe "EditorHttpController", ->
Project_id: @project_id
entity_id: @entity_id = "entity-id-123"
entity_type: @entity_type = "entity-type"
- @EditorController.deleteEntity = sinon.stub().callsArg(4)
+ @EditorController.deleteEntity = sinon.stub().callsArg(5)
@EditorHttpController.deleteEntity @req, @res
it "should call EditorController.deleteEntity", ->
@EditorController.deleteEntity
- .calledWith(@project_id, @entity_id, @entity_type, "editor")
+ .calledWith(@project_id, @entity_id, @entity_type, "editor", @userId)
.should.equal true
it "should send back a success response", ->
diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
index e03189526a..f0669a5902 100644
--- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
+++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
@@ -31,7 +31,7 @@ describe "HistoryController", ->
describe "for a project with project history", ->
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
it "should set the flag for project history to true", ->
@@ -57,93 +57,55 @@ describe "HistoryController", ->
on: (event, handler) -> @events[event] = handler
@request.returns @proxy
- describe "with project history enabled", ->
+ describe "for a project with the project history flag", ->
beforeEach ->
- @settings.apis.project_history.enabled = true
+ @req.useProjectHistory = true
+ @HistoryController.proxyToHistoryApi @req, @res, @next
- describe "for a project with the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = true
- @HistoryController.proxyToHistoryApi @req, @res, @next
+ it "should get the user id", ->
+ @AuthenticationController.getLoggedInUserId
+ .calledWith(@req)
+ .should.equal true
- it "should get the user id", ->
- @AuthenticationController.getLoggedInUserId
- .calledWith(@req)
- .should.equal true
+ it "should call the project history api", ->
+ @request
+ .calledWith({
+ 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", ->
- @request
- .calledWith({
- url: "#{@settings.apis.project_history.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
- it "should pipe the response to the client", ->
- @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", ->
+ describe "for a project without the project history flag", ->
beforeEach ->
- @settings.apis.project_history.enabled = false
+ @req.useProjectHistory = false
+ @HistoryController.proxyToHistoryApi @req, @res, @next
- describe "for a project with the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = true
- @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 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 "for a project without the project history flag", ->
- beforeEach ->
- @req.useProjectHistory = false
- @HistoryController.proxyToHistoryApi @req, @res, @next
-
- 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 an error", ->
beforeEach ->
@@ -152,68 +114,3 @@ describe "HistoryController", ->
it "should pass the error up the call chain", ->
@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
diff --git a/services/web/test/unit/coffee/History/HistoryManagerTests.coffee b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee
new file mode 100644
index 0000000000..9b5f80df04
--- /dev/null
+++ b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee
@@ -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
diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
index 470b538bd9..378e909984 100644
--- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
@@ -38,7 +38,7 @@ describe 'ProjectCreationHandler', ->
setRootDoc: sinon.stub().callsArg(2)
@ProjectDetailsHandler =
validateProjectName: sinon.stub().yields()
- @HistoryController =
+ @HistoryManager =
initializeProject: sinon.stub().callsArg(0)
@user =
@@ -53,7 +53,7 @@ describe 'ProjectCreationHandler', ->
'../../models/User': User:@User
'../../models/Project':{Project:@ProjectModel}
'../../models/Folder':{Folder:@FolderModel}
- '../History/HistoryController': @HistoryController
+ '../History/HistoryManager': @HistoryManager
'./ProjectEntityHandler':@ProjectEntityHandler
"./ProjectDetailsHandler":@ProjectDetailsHandler
"settings-sharelatex": @Settings = {}
@@ -66,7 +66,7 @@ describe 'ProjectCreationHandler', ->
describe 'Creating a Blank project', ->
beforeEach ->
@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)
describe "successfully", ->
@@ -83,7 +83,7 @@ describe 'ProjectCreationHandler', ->
it "should initialize the project overleaf if history id not provided", (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)->
@handler.createBlankProject ownerId, projectName, (err, project)=>
diff --git a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
index b489014e7e..b7afb79f83 100644
--- a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
@@ -64,7 +64,7 @@ describe 'ProjectDuplicator', ->
@projectOptionsHandler =
setCompiler : sinon.stub()
@entityHandler =
- addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
+ addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
setRootDoc: sinon.stub()
addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
@@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
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)=>
@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
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)=>
@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
done()
@@ -139,13 +139,13 @@ describe 'ProjectDuplicator', ->
it 'should copy all the docs', (done)->
@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
@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)
.should.equal true
- @entityHandler.addDocWithProject
+ @entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
.should.equal true
- @entityHandler.addDocWithProject
+ @entityHandler.addDoc
.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
.should.equal true
done()
diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
index f62690e226..a4ac6a7f02 100644
--- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
@@ -157,13 +157,13 @@ describe 'ProjectEntityHandler', ->
@ProjectGetter.getProject.callsArgWith(2, null, @project)
@tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1)
@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"
@projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path)
describe "deleting from Mongo", ->
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", ->
@projectLocator.findElement.called.should.equal true
@@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', ->
it "should clean up the entity from the rest of the system", ->
@ProjectEntityHandler._cleanUpEntity
- .calledWith(@project, @entity, @type)
+ .calledWith(@project, @entity, @type, @path.fileSystem, userId)
.should.equal true
describe "_cleanUpEntity", ->
@@ -193,7 +193,9 @@ describe 'ProjectEntityHandler', ->
describe "a file", ->
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", ->
@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", ->
@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", ->
beforeEach (done) ->
- @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2)
- @ProjectEntityHandler._cleanUpEntity @project, @entity = {_id: @entity_id}, 'doc', done
+ @path = "/file/system/path.tex"
+ @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
+ @entity = {_id: @entity_id}
+ @ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done
it "should clean up the doc", ->
@ProjectEntityHandler._cleanUpDoc
- .calledWith(@project, @entity)
+ .calledWith(@project, @entity, @path, userId)
.should.equal true
describe "a folder", ->
beforeEach (done) ->
@folder =
folders: [
- fileRefs: [ @file1 = {_id: "file-id-1" } ]
- docs: [ @doc1 = { _id: "doc-id-1" } ]
+ name: "subfolder"
+ fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ]
+ docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ]
folders: []
]
- fileRefs: [ @file2 = { _id: "file-id-2" } ]
- docs: [ @doc2 = { _id: "doc-id-2" } ]
+ fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ]
+ docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ]
- @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2)
- @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(2)
- @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", done
+ @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4)
+ @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4)
+ path = "/folder"
+ @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, userId, done
it "should clean up all sub files", ->
- @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1).should.equal true
- @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2).should.equal true
+ @ProjectEntityHandler._cleanUpFile
+ .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", ->
- @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1).should.equal true
- @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2).should.equal true
+ @ProjectEntityHandler._cleanUpDoc
+ .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', ->
beforeEach ->
@@ -496,6 +516,51 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, userId, {newDocs})
.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", ->
beforeEach ->
@name = "doc-name"
@@ -584,6 +649,12 @@ describe 'ProjectEntityHandler', ->
@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', ->
beforeEach ->
@projectLocator
@@ -1116,6 +1187,7 @@ describe 'ProjectEntityHandler', ->
@doc =
_id: ObjectId()
name: "test.tex"
+ @path = "/path/to/doc"
@ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1)
@ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2)
@documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2)
@@ -1125,7 +1197,7 @@ describe 'ProjectEntityHandler', ->
describe "when the doc is the root doc", ->
beforeEach ->
@project.rootDoc_id = @doc._id
- @ProjectEntityHandler._cleanUpDoc @project, @doc, @callback
+ @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc
@@ -1146,13 +1218,19 @@ describe 'ProjectEntityHandler', ->
.calledWith(project_id, @doc._id.toString())
.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", ->
@callback.called.should.equal true
describe "when the doc is not the root doc", ->
beforeEach ->
@project.rootDoc_id = ObjectId()
- @ProjectEntityHandler._cleanUpDoc @project, @doc, @callback
+ @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback
it "should not unset the root doc", ->
@ProjectEntityHandler.unsetRootDoc.called.should.equal false
diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee
index dedd3ea7c8..3984d6341f 100644
--- a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee
+++ b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee
@@ -8,7 +8,7 @@ describe 'TpdsUpdateHandler', ->
beforeEach ->
@requestQueuer = {}
@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()
@editorController = {}
@project_id = "dsjajilknaksdn"
@@ -107,11 +107,13 @@ describe 'TpdsUpdateHandler', ->
it 'should call deleteEntity in the collaberation manager', (done)->
path = "/delete/this"
update = {}
- @updateMerger.deleteUpdate = sinon.stub().callsArg(3)
+ @updateMerger.deleteUpdate = sinon.stub().callsArg(4)
@handler.deleteUpdate @user_id, @project.name, path, @source, =>
@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()
it 'should mark the project as deleted by external source if path is a single slash', (done)->
diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
index e20419765f..cb2aa059ea 100644
--- a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
+++ b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee
@@ -145,13 +145,13 @@ describe 'UpdateMerger :', ->
it 'should get the element id', ->
@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
it 'should delete the entity in the editor controller with the correct type', (done)->
@entity.lines = []
- mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source).callsArg(4)
- @updateMerger.deleteUpdate @project_id, @path, @source, ->
+ mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source, @user_id).callsArg(5)
+ @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, ->
mock.verify()
done()
diff --git a/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
new file mode 100644
index 0000000000..9ad7ba9a2e
--- /dev/null
+++ b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
@@ -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 }
+ })