mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-09 20:48:56 +00:00
Merge branch 'master' into as-update-v2-copy
This commit is contained in:
commit
fb23b30c45
68 changed files with 1365 additions and 556 deletions
2
services/web/.gitignore
vendored
2
services/web/.gitignore
vendored
|
@ -39,6 +39,7 @@ data/*
|
|||
app.js
|
||||
app/js/*
|
||||
test/unit/js/*
|
||||
test/unit_frontend/js/*
|
||||
test/smoke/js/*
|
||||
test/acceptance/js/*
|
||||
cookies.txt
|
||||
|
@ -69,6 +70,7 @@ Gemfile.lock
|
|||
|
||||
public/stylesheets/ol-style.*.css
|
||||
public/stylesheets/style.*.css
|
||||
public/js/libs/require*.js
|
||||
|
||||
|
||||
*.swp
|
||||
|
|
17
services/web/Jenkinsfile
vendored
17
services/web/Jenkinsfile
vendored
|
@ -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}",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 = [])->
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -62,6 +62,9 @@ module.exports = SubscriptionUpdater =
|
|||
invited_emails: email
|
||||
}, callback
|
||||
|
||||
refreshSubscription: (user_id, callback=(err)->) ->
|
||||
SubscriptionUpdater._setUsersMinimumFeatures user_id, callback
|
||||
|
||||
deleteSubscription: (subscription_id, callback = (error) ->) ->
|
||||
SubscriptionLocator.getSubscription subscription_id, (err, subscription) ->
|
||||
return callback(err) if err?
|
||||
|
@ -106,17 +109,29 @@ module.exports = SubscriptionUpdater =
|
|||
SubscriptionLocator.getUsersSubscription user_id, cb
|
||||
groupSubscription: (cb)->
|
||||
SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb
|
||||
v1PlanCode: (cb) ->
|
||||
Modules = require '../../infrastructure/Modules'
|
||||
Modules.hooks.fire 'getV1PlanCode', user_id, (err, results) ->
|
||||
cb(err, results?[0] || null)
|
||||
async.series jobs, (err, results)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures"
|
||||
logger.err err:err, user_id:user_id,
|
||||
"error getting subscription or group for _setUsersMinimumFeatures"
|
||||
return callback(err)
|
||||
{subscription, groupSubscription} = results
|
||||
if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
|
||||
logger.log user_id:user_id, "using users subscription plan code for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
|
||||
else if groupSubscription? and groupSubscription.planCode?
|
||||
{subscription, groupSubscription, v1PlanCode} = results
|
||||
# Group Subscription
|
||||
if groupSubscription? and groupSubscription.planCode?
|
||||
logger.log user_id:user_id, "using group which user is memor of for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback
|
||||
# Personal Subscription
|
||||
else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode
|
||||
logger.log user_id:user_id, "using users subscription plan code for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
|
||||
# V1 Subscription
|
||||
else if v1PlanCode?
|
||||
logger.log user_id: user_id, "using the V1 plan for features"
|
||||
UserFeaturesUpdater.updateFeatures user_id, v1PlanCode, callback
|
||||
# Default
|
||||
else
|
||||
logger.log user_id:user_id, "using default features for user with no subscription or group"
|
||||
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)->
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ getFileContent = (filePath)->
|
|||
logger.log filePath:filePath, "file does not exist for hashing"
|
||||
return ""
|
||||
|
||||
logger.log "Generating file hashes..."
|
||||
pathList = [
|
||||
"#{jsPath}libs/require.js"
|
||||
"#{jsPath}ide.js"
|
||||
|
@ -46,23 +45,27 @@ pathList = [
|
|||
"/stylesheets/ol-style.css"
|
||||
]
|
||||
|
||||
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("."))
|
||||
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("."))
|
||||
|
||||
hashPath = splitPath.join("/")
|
||||
hashedFiles[path] = hashPath
|
||||
hashPath = splitPath.join("/")
|
||||
hashedFiles[path] = hashPath
|
||||
|
||||
fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
|
||||
fs.writeFileSync(fsHashPath, content)
|
||||
fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
|
||||
fs.writeFileSync(fsHashPath, content)
|
||||
|
||||
|
||||
logger.log "Finished hashing static content"
|
||||
logger.log "Finished hashing static content"
|
||||
|
||||
cdnAvailable = Settings.cdn?.web?.host?
|
||||
darkCdnAvailable = Settings.cdn?.web?.darkHost?
|
||||
|
@ -121,7 +124,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
res.locals.buildJsPath = (jsFile, opts = {})->
|
||||
path = Path.join(jsPath, jsFile)
|
||||
|
||||
if opts.hashedPath
|
||||
if opts.hashedPath && hashedFiles[path]?
|
||||
path = hashedFiles[path]
|
||||
|
||||
if !opts.qs?
|
||||
|
@ -141,7 +144,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
|
||||
res.locals.buildCssPath = (cssFile, opts)->
|
||||
path = Path.join("/stylesheets/", cssFile)
|
||||
if opts?.hashedPath
|
||||
if opts?.hashedPath && hashedFiles[path]?
|
||||
hashedPath = hashedFiles[path]
|
||||
return Url.resolve(staticFilesBase, hashedPath)
|
||||
return Url.resolve(staticFilesBase, path)
|
||||
|
@ -294,10 +297,14 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
webRouter.use (req, res, next) ->
|
||||
isOl = (Settings.brandPrefix == 'ol-')
|
||||
res.locals.uiConfig =
|
||||
defaultResizerSizeOpen : if isOl then 2 else 24
|
||||
defaultResizerSizeClosed : if isOl then 2 else 24
|
||||
eastResizerCursor : if isOl then "ew-resize" else null
|
||||
westResizerCursor : if isOl then "ew-resize" else null
|
||||
chatResizerSizeOpen : if isOl then 2 else 12
|
||||
chatResizerSizeClosed : 0
|
||||
defaultResizerSizeOpen : if isOl then 2 else 24
|
||||
defaultResizerSizeClosed : if isOl then 2 else 24
|
||||
eastResizerCursor : if isOl then "ew-resize" else null
|
||||
westResizerCursor : if isOl then "ew-resize" else null
|
||||
chatResizerSizeOpen : if isOl then 2 else 12
|
||||
chatResizerSizeClosed : 0
|
||||
chatMessageBorderSaturation: if isOl then "85%" else "70%"
|
||||
chatMessageBorderLightness : if isOl then "40%" else "70%"
|
||||
chatMessageBgSaturation : if isOl then "85%" else "60%"
|
||||
chatMessageBgLightness : if isOl then "40%" else "97%"
|
||||
next()
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -105,7 +105,7 @@ block requirejs
|
|||
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/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());
|
||||
|
|
|
@ -35,12 +35,9 @@ aside.chat(
|
|||
span(ng-if="message.user.first_name") {{ message.user.first_name }}
|
||||
span(ng-if="!message.user.first_name") {{ message.user.email }}
|
||||
.message(
|
||||
ng-style="{\
|
||||
'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)',\
|
||||
'background-color': 'hsl({{ hue(message.user) }}, 60%, 97%)'\
|
||||
}"
|
||||
ng-style="getMessageStyle(message.user);"
|
||||
)
|
||||
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}")
|
||||
.arrow(ng-style="getArrowStyle(message.user)")
|
||||
.message-content
|
||||
p(
|
||||
mathjax,
|
||||
|
|
|
@ -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 <strong>{{history.diff.doc.name}}</strong>
|
||||
.toolbar-right
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.toolbar-right(ng-if="!history.isV2")
|
||||
a.btn.btn-danger.btn-sm(
|
||||
href,
|
||||
ng-click="openRestoreDiffModal()"
|
||||
|
|
|
@ -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 }}'"
|
||||
|
|
|
@ -210,7 +210,7 @@ block content
|
|||
h3 #{translate("group_plan_enquiry")}
|
||||
.modal-body
|
||||
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
|
||||
span(ng-show="sent == false")
|
||||
span(ng-show="sent == false && error == false")
|
||||
.form-group
|
||||
label#title9(for='Field9')
|
||||
| Name
|
||||
|
@ -228,11 +228,13 @@ block content
|
|||
.form-group
|
||||
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
.form-group
|
||||
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';")
|
||||
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
|
||||
.form-group.text-center
|
||||
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
|
||||
span(ng-show="sent")
|
||||
span(ng-show="sent == true && error == false")
|
||||
p Request Sent, Thank you.
|
||||
span(ng-show="error")
|
||||
p Error sending request.
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
|
|
5
services/web/bin/compile_frontend
Executable file
5
services/web/bin/compile_frontend
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -e;
|
||||
COFFEE=node_modules/.bin/coffee
|
||||
echo Compiling public/coffee;
|
||||
$COFFEE -o public/js -c public/coffee;
|
5
services/web/bin/compile_frontend_tests
Executable file
5
services/web/bin/compile_frontend_tests
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -e;
|
||||
COFFEE=node_modules/.bin/coffee
|
||||
echo Compiling test/unit_frontend/coffee;
|
||||
$COFFEE -o test/unit_frontend/js -c test/unit_frontend/coffee;
|
5
services/web/bin/frontend_test
Executable file
5
services/web/bin/frontend_test
Executable file
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -e;
|
||||
MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
|
||||
$MOCHA "$@" test/unit_frontend/js
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -3,9 +3,22 @@ define [
|
|||
"ide/colors/ColorManager"
|
||||
], (App, ColorManager) ->
|
||||
App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) ->
|
||||
$scope.hue = (user) ->
|
||||
hslColorConfigs =
|
||||
borderSaturation: window.uiConfig?.chatMessageBorderSaturation or "70%"
|
||||
borderLightness : window.uiConfig?.chatMessageBorderLightness or "70%"
|
||||
bgSaturation : window.uiConfig?.chatMessageBgSaturation or "60%"
|
||||
bgLightness : window.uiConfig?.chatMessageBgLightness or "97%"
|
||||
|
||||
hue = (user) ->
|
||||
if !user?
|
||||
return 0
|
||||
else
|
||||
return ColorManager.getHueForUserId(user.id)
|
||||
|
||||
$scope.getMessageStyle = (user) ->
|
||||
"border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
|
||||
"background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })"
|
||||
|
||||
$scope.getArrowStyle = (user) ->
|
||||
"border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
|
||||
]
|
|
@ -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?
|
||||
|
|
280
services/web/public/coffee/ide/history/HistoryV2Manager.coffee
Normal file
280
services/web/public/coffee/ide/history/HistoryV2Manager.coffee
Normal file
|
@ -0,0 +1,280 @@
|
|||
define [
|
||||
"moment"
|
||||
"ide/colors/ColorManager"
|
||||
"ide/history/controllers/HistoryListController"
|
||||
"ide/history/controllers/HistoryDiffController"
|
||||
"ide/history/directives/infiniteScroll"
|
||||
], (moment, ColorManager) ->
|
||||
class HistoryManager
|
||||
constructor: (@ide, @$scope) ->
|
||||
@reset()
|
||||
|
||||
@$scope.toggleHistory = () =>
|
||||
if @$scope.ui.view == "history"
|
||||
@hide()
|
||||
else
|
||||
@show()
|
||||
|
||||
@$scope.$watch "history.selection.updates", (updates) =>
|
||||
if updates? and updates.length > 0
|
||||
@_selectDocFromUpdates()
|
||||
@reloadDiff()
|
||||
|
||||
@$scope.$on "entity:selected", (event, entity) =>
|
||||
if (@$scope.ui.view == "history") and (entity.type == "doc")
|
||||
@$scope.history.selection.pathname = _ide.fileTreeManager.getEntityPath(entity)
|
||||
@reloadDiff()
|
||||
|
||||
show: () ->
|
||||
@$scope.ui.view = "history"
|
||||
@reset()
|
||||
|
||||
hide: () ->
|
||||
@$scope.ui.view = "editor"
|
||||
# Make sure we run the 'open' logic for whatever is currently selected
|
||||
@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
|
||||
|
||||
reset: () ->
|
||||
@$scope.history = {
|
||||
isV2: true
|
||||
updates: []
|
||||
nextBeforeTimestamp: null
|
||||
atEnd: false
|
||||
selection: {
|
||||
updates: []
|
||||
pathname: null
|
||||
range: {
|
||||
fromV: null
|
||||
toV: null
|
||||
}
|
||||
}
|
||||
diff: null
|
||||
}
|
||||
|
||||
MAX_RECENT_UPDATES_TO_SELECT: 2
|
||||
autoSelectRecentUpdates: () ->
|
||||
return if @$scope.history.updates.length == 0
|
||||
|
||||
@$scope.history.updates[0].selectedTo = true
|
||||
|
||||
indexOfLastUpdateNotByMe = 0
|
||||
for update, i in @$scope.history.updates
|
||||
if @_updateContainsUserId(update, @$scope.user.id) or i > @MAX_RECENT_UPDATES_TO_SELECT
|
||||
break
|
||||
indexOfLastUpdateNotByMe = i
|
||||
|
||||
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
||||
|
||||
BATCH_SIZE: 10
|
||||
fetchNextBatchOfUpdates: () ->
|
||||
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
||||
if @$scope.history.nextBeforeTimestamp?
|
||||
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
||||
@$scope.history.loading = true
|
||||
@ide.$http
|
||||
.get(url)
|
||||
.then (response) =>
|
||||
{ data } = response
|
||||
@_loadUpdates(data.updates)
|
||||
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
|
||||
if !data.nextBeforeTimestamp?
|
||||
@$scope.history.atEnd = true
|
||||
@$scope.history.loading = false
|
||||
|
||||
reloadDiff: () ->
|
||||
diff = @$scope.history.diff
|
||||
{updates} = @$scope.history.selection
|
||||
{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
|
||||
|
||||
if !pathname?
|
||||
@$scope.history.diff = null
|
||||
return
|
||||
|
||||
return if diff? and
|
||||
diff.pathname == pathname and
|
||||
diff.fromV == fromV and
|
||||
diff.toV == toV
|
||||
|
||||
@$scope.history.diff = diff = {
|
||||
fromV: fromV
|
||||
toV: toV
|
||||
pathname: pathname
|
||||
error: false
|
||||
}
|
||||
|
||||
diff.loading = true
|
||||
url = "/project/#{@$scope.project_id}/diff"
|
||||
query = ["pathname=#{encodeURIComponent(pathname)}"]
|
||||
if diff.fromV? and diff.toV?
|
||||
query.push "from=#{diff.fromV}", "to=#{diff.toV}"
|
||||
url += "?" + query.join("&")
|
||||
|
||||
@ide.$http
|
||||
.get(url)
|
||||
.then (response) =>
|
||||
{ data } = response
|
||||
diff.loading = false
|
||||
{text, highlights} = @_parseDiff(data)
|
||||
diff.text = text
|
||||
diff.highlights = highlights
|
||||
.catch () ->
|
||||
diff.loading = false
|
||||
diff.error = true
|
||||
|
||||
_parseDiff: (diff) ->
|
||||
row = 0
|
||||
column = 0
|
||||
highlights = []
|
||||
text = ""
|
||||
for entry, i in diff.diff or []
|
||||
content = entry.u or entry.i or entry.d
|
||||
content ||= ""
|
||||
text += content
|
||||
lines = content.split("\n")
|
||||
startRow = row
|
||||
startColumn = column
|
||||
if lines.length > 1
|
||||
endRow = startRow + lines.length - 1
|
||||
endColumn = lines[lines.length - 1].length
|
||||
else
|
||||
endRow = startRow
|
||||
endColumn = startColumn + lines[0].length
|
||||
row = endRow
|
||||
column = endColumn
|
||||
|
||||
range = {
|
||||
start:
|
||||
row: startRow
|
||||
column: startColumn
|
||||
end:
|
||||
row: endRow
|
||||
column: endColumn
|
||||
}
|
||||
|
||||
if entry.i? or entry.d?
|
||||
if entry.meta.user?
|
||||
name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
|
||||
else
|
||||
name = "Anonymous"
|
||||
if entry.meta.user?.id == @$scope.user.id
|
||||
name = "you"
|
||||
date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
|
||||
if entry.i?
|
||||
highlights.push {
|
||||
label: "Added by #{name} on #{date}"
|
||||
highlight: range
|
||||
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
|
||||
}
|
||||
else if entry.d?
|
||||
highlights.push {
|
||||
label: "Deleted by #{name} on #{date}"
|
||||
strikeThrough: range
|
||||
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
|
||||
}
|
||||
|
||||
return {text, highlights}
|
||||
|
||||
_loadUpdates: (updates = []) ->
|
||||
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
|
||||
|
||||
for update in updates or []
|
||||
for user in update.meta.users or []
|
||||
if user?
|
||||
user.hue = ColorManager.getHueForUserId(user.id)
|
||||
|
||||
if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
|
||||
update.meta.first_in_day = true
|
||||
|
||||
update.selectedFrom = false
|
||||
update.selectedTo = false
|
||||
update.inSelection = false
|
||||
|
||||
previousUpdate = update
|
||||
|
||||
firstLoad = @$scope.history.updates.length == 0
|
||||
|
||||
@$scope.history.updates =
|
||||
@$scope.history.updates.concat(updates)
|
||||
|
||||
@autoSelectRecentUpdates() if firstLoad
|
||||
|
||||
_perDocSummaryOfUpdates: (updates) ->
|
||||
# Track current_pathname -> original_pathname
|
||||
original_pathnames = {}
|
||||
|
||||
# Map of original pathname -> doc summary
|
||||
docs_summary = {}
|
||||
|
||||
updatePathnameWithUpdateVersions = (pathname, update) ->
|
||||
# docs_summary is indexed by the original pathname the doc
|
||||
# had at the start, so we have to look this up from the current
|
||||
# pathname via original_pathname first
|
||||
if !original_pathnames[pathname]?
|
||||
original_pathnames[pathname] = pathname
|
||||
original_pathname = original_pathnames[pathname]
|
||||
doc_summary = docs_summary[original_pathname] ?= {
|
||||
fromV: update.fromV, toV: update.toV,
|
||||
}
|
||||
doc_summary.fromV = Math.min(
|
||||
doc_summary.fromV,
|
||||
update.fromV
|
||||
)
|
||||
doc_summary.toV = Math.max(
|
||||
doc_summary.toV,
|
||||
update.toV
|
||||
)
|
||||
|
||||
# Put updates in ascending chronological order
|
||||
updates = updates.slice().reverse()
|
||||
for update in updates
|
||||
for pathname in update.pathnames or []
|
||||
updatePathnameWithUpdateVersions(pathname, update)
|
||||
for project_op in update.project_ops or []
|
||||
if project_op.rename?
|
||||
rename = project_op.rename
|
||||
updatePathnameWithUpdateVersions(rename.pathname, update)
|
||||
original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
|
||||
delete original_pathnames[rename.pathname]
|
||||
if project_op.add?
|
||||
add = project_op.add
|
||||
updatePathnameWithUpdateVersions(add.pathname, update)
|
||||
|
||||
return docs_summary
|
||||
|
||||
_calculateDiffDataFromSelection: () ->
|
||||
fromV = toV = pathname = null
|
||||
|
||||
selected_pathname = @$scope.history.selection.pathname
|
||||
|
||||
for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
|
||||
if pathname == selected_pathname
|
||||
{fromV, toV} = doc
|
||||
return {fromV, toV, pathname}
|
||||
|
||||
return {}
|
||||
|
||||
# Set the track changes selected doc to one of the docs in the range
|
||||
# of currently selected updates. If we already have a selected doc
|
||||
# then prefer this one if present.
|
||||
_selectDocFromUpdates: () ->
|
||||
affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
|
||||
|
||||
selected_pathname = @$scope.history.selection.pathname
|
||||
if selected_pathname? and affected_docs[selected_pathname]
|
||||
# Selected doc is already open
|
||||
else
|
||||
# Set to first possible candidate
|
||||
for pathname, doc of affected_docs
|
||||
selected_pathname = pathname
|
||||
break
|
||||
|
||||
@$scope.history.selection.pathname = selected_pathname
|
||||
if selected_pathname?
|
||||
entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
|
||||
if entity?
|
||||
@ide.fileTreeManager.selectEntity(entity)
|
||||
|
||||
_updateContainsUserId: (update, user_id) ->
|
||||
for user in update.meta.users
|
||||
return true if user?.id == user_id
|
||||
return false
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -9,7 +9,6 @@ define [
|
|||
"libs/angular-cookie"
|
||||
"libs/passfield"
|
||||
"libs/sixpack"
|
||||
"libs/groove"
|
||||
"libs/angular-sixpack"
|
||||
"libs/ng-tags-input-3.0.0"
|
||||
], () ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -74,27 +74,34 @@ define [
|
|||
$modalInstance.close()
|
||||
|
||||
|
||||
App.controller 'UniverstiesContactController', ($scope, $modal) ->
|
||||
App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
|
||||
|
||||
$scope.form = {}
|
||||
$scope.sent = false
|
||||
$scope.sending = false
|
||||
$scope.error = false
|
||||
$scope.contactUs = ->
|
||||
if !$scope.form.email?
|
||||
console.log "email not set"
|
||||
return
|
||||
$scope.sending = true
|
||||
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
|
||||
params =
|
||||
data =
|
||||
_csrf : window.csrfToken
|
||||
name: $scope.form.name || $scope.form.email
|
||||
email: $scope.form.email
|
||||
labels: "#{$scope.form.source} accounts"
|
||||
message: "Please contact me with more details"
|
||||
subject: $scope.form.subject + " - [#{ticketNumber}]"
|
||||
about : "#{$scope.form.position || ''} #{$scope.form.university || ''}"
|
||||
subject: "#{$scope.form.name} - General Enquiry - #{$scope.form.position} - #{$scope.form.university}"
|
||||
inbox: "accounts"
|
||||
|
||||
Groove.createTicket params, (err, json)->
|
||||
$scope.sent = true
|
||||
request = $http.post "/support", data
|
||||
|
||||
request.catch ()->
|
||||
$scope.error = true
|
||||
$scope.$apply()
|
||||
|
||||
|
||||
request.then (response)->
|
||||
$scope.sent = true
|
||||
$scope.error = (response.status != 200)
|
||||
$scope.$apply()
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
!function(window) {
|
||||
|
||||
window.Groove = {
|
||||
|
||||
init: function(options) {
|
||||
this._options = options;
|
||||
if (typeof grooveOnReady != 'undefined') {grooveOnReady();}
|
||||
},
|
||||
|
||||
createTicket: function(params, callback) {
|
||||
var postData = serialize({
|
||||
"ticket[enduser_name]": params["name"],
|
||||
"ticket[enduser_email]": params["email"],
|
||||
"ticket[title]": params["subject"],
|
||||
"ticket[enduser_about]": params["about"],
|
||||
"ticket[label_string]": params["labels"],
|
||||
"ticket[comments_attributes][0][body]": params["message"]
|
||||
});
|
||||
|
||||
sendRequest(this._options.widget_ticket_url, function(req) {
|
||||
if (callback) {callback(req);}
|
||||
}, postData);
|
||||
}
|
||||
};
|
||||
|
||||
// http://www.quirksmode.org/js/xmlhttp.html
|
||||
function sendRequest(url, callback, postData) {
|
||||
var req = createXMLHTTPObject();
|
||||
if (!req) return;
|
||||
var method = (postData) ? "POST" : "GET";
|
||||
req.open(method, url, true);
|
||||
if (postData){
|
||||
try {
|
||||
req.setRequestHeader('Content-type','application/x-www-form-urlencoded');
|
||||
}
|
||||
catch(e) {
|
||||
req.contentType = 'application/x-www-form-urlencoded';
|
||||
};
|
||||
};
|
||||
req.onreadystatechange = function () {
|
||||
if (req.readyState != 4) return;
|
||||
callback(req);
|
||||
}
|
||||
if (req.readyState == 4) return;
|
||||
req.send(postData);
|
||||
}
|
||||
|
||||
var XMLHttpFactories = [
|
||||
function () {return new XDomainRequest()},
|
||||
function () {return new XMLHttpRequest()},
|
||||
function () {return new ActiveXObject("Msxml2.XMLHTTP")},
|
||||
function () {return new ActiveXObject("Msxml3.XMLHTTP")},
|
||||
function () {return new ActiveXObject("Microsoft.XMLHTTP")}
|
||||
];
|
||||
|
||||
function createXMLHTTPObject() {
|
||||
var xmlhttp = false;
|
||||
for (var i = 0; i < XMLHttpFactories.length; i++) {
|
||||
try {
|
||||
xmlhttp = XMLHttpFactories[i]();
|
||||
}
|
||||
catch (e) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return xmlhttp;
|
||||
}
|
||||
|
||||
function serialize(obj) {
|
||||
var str = [];
|
||||
for(var p in obj) {
|
||||
if (obj[p]) {
|
||||
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
|
||||
}
|
||||
}
|
||||
return str.join("&");
|
||||
}
|
||||
|
||||
if (typeof grooveOnLoad != 'undefined') {grooveOnLoad();}
|
||||
}(window);
|
||||
|
||||
Groove.init({"widget_ticket_url":"https://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.json"});
|
||||
|
|
@ -31,13 +31,18 @@
|
|||
right: 0;
|
||||
bottom: @new-message-height;
|
||||
overflow-x: hidden;
|
||||
background-color: @chat-bg;
|
||||
|
||||
li.message {
|
||||
margin: @line-height-computed / 2;
|
||||
.date {
|
||||
font-size: 12px;
|
||||
color: @gray-light;
|
||||
border-bottom: 1px solid @gray-lightest;
|
||||
color: @chat-message-date-color;
|
||||
margin-bottom: @line-height-computed / 2;
|
||||
text-align: right;
|
||||
}
|
||||
.date when (@is-overleaf = false) {
|
||||
border-bottom: 1px solid @gray-lightest;
|
||||
text-align: center;
|
||||
}
|
||||
.avatar {
|
||||
|
@ -56,20 +61,22 @@
|
|||
|
||||
.name {
|
||||
font-size: 12px;
|
||||
color: @gray-light;
|
||||
color: @chat-message-name-color;
|
||||
margin-bottom: 4px;
|
||||
min-height: 16px;
|
||||
}
|
||||
.message {
|
||||
border-left: 3px solid transparent;
|
||||
font-size: 14px;
|
||||
box-shadow: -1px 2px 3px #ddd;
|
||||
border-raduis: @border-radius-base;
|
||||
box-shadow: @chat-message-box-shadow;
|
||||
border-radius: @chat-message-border-radius;
|
||||
position: relative;
|
||||
|
||||
.message-content {
|
||||
padding: @line-height-computed / 2;
|
||||
padding: @chat-message-padding;
|
||||
overflow-x: auto;
|
||||
color: @chat-message-color;
|
||||
font-weight: @chat-message-weight;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
@ -124,7 +131,7 @@
|
|||
.full-size;
|
||||
top: auto;
|
||||
height: @new-message-height;
|
||||
background-color: @gray-lightest;
|
||||
background-color: @chat-new-message-bg;
|
||||
padding: @line-height-computed / 4;
|
||||
border-top: 1px solid @editor-border-color;
|
||||
textarea {
|
||||
|
@ -134,9 +141,10 @@
|
|||
border: 1px solid @editor-border-color;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: @gray-dark;
|
||||
color: @chat-new-message-textarea-color;
|
||||
font-size: 14px;
|
||||
padding: @line-height-computed / 4;
|
||||
background-color: @chat-new-message-textarea-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +153,7 @@
|
|||
word-break: break-all;
|
||||
}
|
||||
|
||||
.editor-dark {
|
||||
.editor-dark when (@is-overleaf = false) {
|
||||
.chat {
|
||||
.new-message {
|
||||
background-color: lighten(@editor-dark-background-color, 10%);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
@ -143,7 +143,7 @@
|
|||
.toolbar-label {
|
||||
display: none;
|
||||
margin: 0 4px;
|
||||
font-size: 12px;
|
||||
font-size: @toolbar-font-size;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -20,14 +20,9 @@
|
|||
//== Typography
|
||||
//
|
||||
//## Font, line-height, and color for body text, headings, and more.
|
||||
|
||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
|
||||
//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700);
|
||||
//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i);
|
||||
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
|
||||
|
||||
@font-family-sans-serif: "Open Sans", sans-serif;
|
||||
@font-family-serif: "Merriweather", serif;
|
||||
|
||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
||||
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
@font-family-base: @font-family-sans-serif;
|
||||
|
@ -550,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;
|
||||
|
@ -898,25 +893,28 @@
|
|||
@toolbar-btn-active-color : white;
|
||||
@toolbar-btn-active-bg-color : @link-color;
|
||||
@toolbar-btn-active-shadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
|
||||
@toolbar-font-size : 12px;
|
||||
@toolbar-alt-bg-color : #fafafa;
|
||||
@toolbar-icon-btn-color : @gray-light;
|
||||
@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;
|
||||
|
@ -925,6 +923,28 @@
|
|||
@editor-toggler-hover-bg-color : #DDD;
|
||||
@synctex-controls-z-index : 3;
|
||||
@synctex-controls-padding : 0 2px;
|
||||
|
||||
// Chat
|
||||
@chat-bg : transparent;
|
||||
@chat-message-color : @text-color;
|
||||
@chat-message-date-color : @gray-light;
|
||||
@chat-message-name-color : @gray-light;
|
||||
@chat-message-box-shadow : -1px 2px 3px #ddd;
|
||||
@chat-message-border-radius : 0;
|
||||
@chat-message-padding : @line-height-computed / 2;
|
||||
@chat-message-weight : normal;
|
||||
@chat-new-message-bg : @gray-lightest;
|
||||
@chat-new-message-textarea-bg : #FFF;
|
||||
@chat-new-message-textarea-color : @gray-dark;
|
||||
|
||||
// PDF
|
||||
@pdf-top-offset : @toolbar-tall-height;
|
||||
@pdf-bg : transparent;
|
||||
@pdfjs-bg : @gray-lighter;
|
||||
@pdf-page-shadow-color : #000;
|
||||
@log-line-no-color : @gray;
|
||||
@log-hints-color : @gray-dark;
|
||||
|
||||
// Tags
|
||||
@tag-border-radius : 0.25em;
|
||||
@tag-bg-color : @label-default-bg;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
@import "./_common-variables.less";
|
||||
|
||||
@is-overleaf: true;
|
||||
|
||||
@font-family-sans-serif: "Lato", sans-serif;
|
||||
|
||||
@header-height: 68px;
|
||||
@footer-height: 50px;
|
||||
|
||||
|
@ -64,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;
|
||||
|
@ -177,19 +202,22 @@
|
|||
@toolbar-icon-btn-hover-shadow : none;
|
||||
@toolbar-border-bottom : 1px solid @toolbar-border-color;
|
||||
@toolbar-icon-btn-hover-boxshadow : none;
|
||||
@toolbar-font-size : 13px;
|
||||
|
||||
// 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;
|
||||
|
@ -197,6 +225,31 @@
|
|||
@editor-toggler-hover-bg-color : @ol-green;
|
||||
@synctex-controls-z-index : 6;
|
||||
@synctex-controls-padding : 0;
|
||||
@editor-border-color : @ol-blue-gray-5;
|
||||
|
||||
// Chat
|
||||
@chat-bg : @ol-blue-gray-5;
|
||||
@chat-message-color : #FFF;
|
||||
@chat-message-name-color : #FFF;
|
||||
@chat-message-date-color : @ol-blue-gray-2;
|
||||
@chat-message-box-shadow : none;
|
||||
@chat-message-padding : 5px 10px;
|
||||
@chat-message-border-radius : @border-radius-large;
|
||||
@chat-message-weight : bold;
|
||||
@chat-new-message-bg : @ol-blue-gray-4;
|
||||
@chat-new-message-textarea-bg : @ol-blue-gray-1;
|
||||
@chat-new-message-textarea-color : @ol-blue-gray-6;
|
||||
|
||||
// PDF
|
||||
@pdf-top-offset : @toolbar-small-height;
|
||||
@pdf-bg : @ol-blue-gray-1;
|
||||
@pdfjs-bg : transparent;
|
||||
@pdf-page-shadow-color : rgba(0, 0, 0, 0.5);
|
||||
@log-line-no-color : #FFF;
|
||||
@log-hints-color : @ol-blue-gray-4;
|
||||
|
||||
//== Colors
|
||||
//
|
||||
//## Gray and brand colors for use across Bootstrap.
|
||||
@gray-darker: #252525;
|
||||
@gray-dark: #505050;
|
||||
|
@ -216,9 +269,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);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700&subset=latin-ext);
|
||||
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
|
||||
|
||||
// Core variables and mixins
|
||||
@import "core/ol-variables.less";
|
||||
@import "app/ol-style-guide.less";
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
|
||||
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
|
||||
|
||||
// Core variables and mixins
|
||||
@import "core/variables.less";
|
||||
@import "_style_includes.less";
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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", ->
|
||||
|
||||
|
|
|
@ -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", ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)=>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -57,6 +57,7 @@ describe "SubscriptionUpdater", ->
|
|||
|
||||
@ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1)
|
||||
@ReferalAllocator.cock = true
|
||||
@Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}}
|
||||
@SubscriptionUpdater = SandboxedModule.require modulePath, requires:
|
||||
'../../models/Subscription': Subscription:@SubscriptionModel
|
||||
'./UserFeaturesUpdater': @UserFeaturesUpdater
|
||||
|
@ -65,6 +66,7 @@ describe "SubscriptionUpdater", ->
|
|||
"logger-sharelatex": log:->
|
||||
'settings-sharelatex': @Settings
|
||||
"../Referal/ReferalAllocator" : @ReferalAllocator
|
||||
'../../infrastructure/Modules': @Modules
|
||||
|
||||
|
||||
describe "syncSubscription", ->
|
||||
|
@ -204,10 +206,22 @@ describe "SubscriptionUpdater", ->
|
|||
assert.equal args[1], @groupSubscription.planCode
|
||||
done()
|
||||
|
||||
it "should call updateFeatures with the overleaf subscription if set", (done)->
|
||||
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
|
||||
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null)
|
||||
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, ['ol_pro'])
|
||||
|
||||
@SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=>
|
||||
args = @UserFeaturesUpdater.updateFeatures.args[0]
|
||||
assert.equal args[0], @adminUser._id
|
||||
assert.equal args[1], 'ol_pro'
|
||||
done()
|
||||
|
||||
it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)->
|
||||
@subscription.planCode = @Settings.defaultPlanCode
|
||||
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
|
||||
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription)
|
||||
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
|
||||
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
|
||||
args = @UserFeaturesUpdater.updateFeatures.args[0]
|
||||
assert.equal args[0], @adminUser._id
|
||||
|
@ -218,6 +232,7 @@ describe "SubscriptionUpdater", ->
|
|||
it "should call updateFeatures with default if there are no subscriptions for user", (done)->
|
||||
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null)
|
||||
@SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null)
|
||||
@Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null)
|
||||
@SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=>
|
||||
args = @UserFeaturesUpdater.updateFeatures.args[0]
|
||||
assert.equal args[0], @adminUser._id
|
||||
|
@ -263,3 +278,13 @@ describe "SubscriptionUpdater", ->
|
|||
@SubscriptionUpdater._setUsersMinimumFeatures
|
||||
.calledWith(user_id)
|
||||
.should.equal true
|
||||
|
||||
describe 'refreshSubscription', ->
|
||||
beforeEach ->
|
||||
@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub()
|
||||
.callsArgWith(1, null)
|
||||
|
||||
it 'should call to _setUsersMinimumFeatures', ->
|
||||
@SubscriptionUpdater.refreshSubscription(@adminUser._id, ()->)
|
||||
@SubscriptionUpdater._setUsersMinimumFeatures.callCount.should.equal 1
|
||||
@SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true
|
||||
|
|
|
@ -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)->
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 }
|
||||
})
|
Loading…
Add table
Reference in a new issue