mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into ns-no-duplicate-packages
This commit is contained in:
commit
cf4d6c1165
80 changed files with 1533 additions and 629 deletions
6
services/web/.gitignore
vendored
6
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
|
||||
|
@ -67,6 +68,11 @@ public/minjs/
|
|||
|
||||
Gemfile.lock
|
||||
|
||||
public/stylesheets/ol-style.*.css
|
||||
public/stylesheets/style.*.css
|
||||
public/js/libs/require*.js
|
||||
|
||||
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
|
|
|
@ -205,12 +205,12 @@ module.exports = (grunt) ->
|
|||
modules: [
|
||||
{
|
||||
name: "main",
|
||||
exclude: ["libs"]
|
||||
exclude: ["libraries"]
|
||||
}, {
|
||||
name: "ide",
|
||||
exclude: ["libs", "pdfjs-dist/build/pdf"]
|
||||
}, {
|
||||
name: "libs"
|
||||
exclude: ["pdfjs-dist/build/pdf", "libraries"]
|
||||
},{
|
||||
name: "libraries"
|
||||
},{
|
||||
name: "ace/mode-latex"
|
||||
},{
|
||||
|
|
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
|
||||
|
|
|
@ -26,8 +26,16 @@ InvalidNameError = (message) ->
|
|||
return error
|
||||
InvalidNameError.prototype.__proto__ = Error.prototype
|
||||
|
||||
UnsupportedFileTypeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "UnsupportedFileTypeError"
|
||||
error.__proto__ = UnsupportedFileTypeError.prototype
|
||||
return error
|
||||
UnsupportedFileTypeError.prototype.__proto___ = Error.prototype
|
||||
|
||||
module.exports = Errors =
|
||||
NotFoundError: NotFoundError
|
||||
ServiceNotConfiguredError: ServiceNotConfiguredError
|
||||
TooManyRequestsError: TooManyRequestsError
|
||||
InvalidNameError: InvalidNameError
|
||||
UnsupportedFileTypeError: UnsupportedFileTypeError
|
||||
|
|
|
@ -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)->
|
||||
|
@ -253,7 +253,7 @@ module.exports = ProjectController =
|
|||
# Extract data from user's ObjectId
|
||||
timestamp = parseInt(user_id.toString().substring(0, 8), 16)
|
||||
|
||||
rolloutPercentage = 40 # Percentage of users to roll out to
|
||||
rolloutPercentage = 60 # Percentage of users to roll out to
|
||||
if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
|
||||
# Don't show if user is not part of roll out
|
||||
return cb(null, { enabled: false, showOnboarding: false })
|
||||
|
@ -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()
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ Modules = require "./Modules"
|
|||
Url = require "url"
|
||||
PackageVersions = require "./PackageVersions"
|
||||
htmlEncoder = new require("node-html-encoder").Encoder("numerical")
|
||||
fingerprints = {}
|
||||
hashedFiles = {}
|
||||
Path = require 'path'
|
||||
Features = require "./Features"
|
||||
|
||||
|
@ -30,43 +30,42 @@ getFileContent = (filePath)->
|
|||
filePath = Path.join __dirname, "../../../", "public#{filePath}"
|
||||
exists = fs.existsSync filePath
|
||||
if exists
|
||||
content = fs.readFileSync filePath
|
||||
content = fs.readFileSync filePath, "UTF-8"
|
||||
return content
|
||||
else
|
||||
logger.log filePath:filePath, "file does not exist for fingerprints"
|
||||
logger.log filePath:filePath, "file does not exist for hashing"
|
||||
return ""
|
||||
|
||||
logger.log "Generating file fingerprints..."
|
||||
pathList = [
|
||||
["#{jsPath}libs/#{fineuploader}.js"]
|
||||
["#{jsPath}libs/require.js"]
|
||||
["#{jsPath}ide.js"]
|
||||
["#{jsPath}main.js"]
|
||||
["#{jsPath}libs.js"]
|
||||
["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"]
|
||||
["#{jsPath}libs/#{pdfjs}/pdf.js"]
|
||||
["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
|
||||
["#{jsPath}libs/#{pdfjs}/compatibility.js"]
|
||||
["/stylesheets/style.css"]
|
||||
["/stylesheets/ol-style.css"]
|
||||
"#{jsPath}libs/require.js"
|
||||
"#{jsPath}ide.js"
|
||||
"#{jsPath}main.js"
|
||||
"#{jsPath}libraries.js"
|
||||
"/stylesheets/style.css"
|
||||
"/stylesheets/ol-style.css"
|
||||
]
|
||||
|
||||
for paths in pathList
|
||||
contentList = _.map(paths, getFileContent)
|
||||
content = contentList.join("")
|
||||
hash = crypto.createHash("md5").update(content).digest("hex")
|
||||
_.each paths, (filePath)->
|
||||
logger.log "#{filePath}: #{hash}"
|
||||
fingerprints[filePath] = hash
|
||||
if !Settings.useMinifiedJs
|
||||
logger.log "not using minified JS, not hashing static files"
|
||||
else
|
||||
logger.log "Generating file hashes..."
|
||||
for path in pathList
|
||||
content = getFileContent(path)
|
||||
hash = crypto.createHash("md5").update(content).digest("hex")
|
||||
|
||||
splitPath = path.split("/")
|
||||
filenameSplit = splitPath.pop().split(".")
|
||||
filenameSplit.splice(filenameSplit.length-1, 0, hash)
|
||||
splitPath.push(filenameSplit.join("."))
|
||||
|
||||
getFingerprint = (path) ->
|
||||
if fingerprints[path]?
|
||||
return fingerprints[path]
|
||||
else
|
||||
logger.err "No fingerprint for file: #{path}"
|
||||
return ""
|
||||
hashPath = splitPath.join("/")
|
||||
hashedFiles[path] = hashPath
|
||||
|
||||
logger.log "Finished generating file fingerprints"
|
||||
fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
|
||||
fs.writeFileSync(fsHashPath, content)
|
||||
|
||||
|
||||
logger.log "Finished hashing static content"
|
||||
|
||||
cdnAvailable = Settings.cdn?.web?.host?
|
||||
darkCdnAvailable = Settings.cdn?.web?.darkHost?
|
||||
|
@ -120,29 +119,35 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
|
||||
res.locals.lib = PackageVersions.lib
|
||||
|
||||
|
||||
|
||||
res.locals.buildJsPath = (jsFile, opts = {})->
|
||||
path = Path.join(jsPath, jsFile)
|
||||
|
||||
doFingerPrint = opts.fingerprint != false
|
||||
if opts.hashedPath && hashedFiles[path]?
|
||||
path = hashedFiles[path]
|
||||
|
||||
if !opts.qs?
|
||||
opts.qs = {}
|
||||
|
||||
if !opts.qs?.fingerprint? and doFingerPrint
|
||||
opts.qs.fingerprint = getFingerprint(path)
|
||||
|
||||
if opts.cdn != false
|
||||
path = Url.resolve(staticFilesBase, path)
|
||||
|
||||
qs = querystring.stringify(opts.qs)
|
||||
|
||||
if opts.removeExtension == true
|
||||
path = path.slice(0,-3)
|
||||
|
||||
if qs? and qs.length > 0
|
||||
path = path + "?" + qs
|
||||
return path
|
||||
|
||||
res.locals.buildCssPath = (cssFile)->
|
||||
res.locals.buildCssPath = (cssFile, opts)->
|
||||
path = Path.join("/stylesheets/", cssFile)
|
||||
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
|
||||
if opts?.hashedPath && hashedFiles[path]?
|
||||
hashedPath = hashedFiles[path]
|
||||
return Url.resolve(staticFilesBase, hashedPath)
|
||||
return Url.resolve(staticFilesBase, path)
|
||||
|
||||
res.locals.buildImgPath = (imgFile)->
|
||||
path = Path.join("/img/", imgFile)
|
||||
|
@ -227,10 +232,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
|
|||
return req.query?[field]
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.fingerprint = getFingerprint
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.formatPrice = SubscriptionFormatters.formatPrice
|
||||
next()
|
||||
|
@ -296,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) ->
|
||||
|
|
|
@ -23,7 +23,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png")
|
||||
link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529")
|
||||
|
||||
link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css"))
|
||||
link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css", {hashedPath:true}))
|
||||
|
||||
block _headLinks
|
||||
|
||||
|
@ -57,7 +57,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
script(type="text/javascript").
|
||||
window.csrfToken = "#{csrfToken}";
|
||||
|
||||
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
|
||||
script(src=buildJsPath("libs/jquery-1.11.1.min.js"))
|
||||
script(type="text/javascript").
|
||||
var noCdnKey = "nocdn=true"
|
||||
var cdnBlocked = typeof jQuery === 'undefined'
|
||||
|
@ -68,7 +68,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
|
||||
block scripts
|
||||
|
||||
script(src=buildJsPath("libs/angular-1.6.4.min.js", {fingerprint:false}))
|
||||
script(src=buildJsPath("libs/angular-1.6.4.min.js"))
|
||||
|
||||
script.
|
||||
window.sharelatex = {
|
||||
|
@ -95,7 +95,11 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
cdnDomain : '!{settings.templates.cdnDomain}',
|
||||
indexName : '!{settings.templates.indexName}'
|
||||
}
|
||||
|
||||
|
||||
- if (settings.overleaf && settings.overleaf.useOLFreeTrial)
|
||||
script.
|
||||
window.redirectToOLFreeTrialUrl = '!{settings.overleaf.host}/users/trial'
|
||||
|
||||
body
|
||||
if(settings.recaptcha)
|
||||
script(src="https://www.google.com/recaptcha/api.js?render=explicit")
|
||||
|
@ -142,9 +146,10 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
window.requirejs = {
|
||||
"paths" : {
|
||||
"moment": "libs/#{lib('moment')}",
|
||||
"fineuploader": "libs/#{lib('fineuploader')}"
|
||||
"fineuploader": "libs/#{lib('fineuploader')}",
|
||||
"main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
|
||||
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
|
||||
},
|
||||
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||
"config":{
|
||||
"moment":{
|
||||
"noGlobal": true
|
||||
|
@ -152,9 +157,9 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
}
|
||||
};
|
||||
script(
|
||||
data-main=buildJsPath('main.js', {fingerprint:false}),
|
||||
data-main=buildJsPath('main.js', {hashedPath:false}),
|
||||
baseurl=fullJsPath,
|
||||
src=buildJsPath('libs/require.js')
|
||||
src=buildJsPath('libs/require.js', {hashedPath:true})
|
||||
)
|
||||
|
||||
include contact-us-modal
|
||||
|
|
|
@ -98,14 +98,14 @@ block requirejs
|
|||
script(type="text/javascript" src='/socket.io/socket.io.js')
|
||||
|
||||
//- don't use cdn for workers
|
||||
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false})
|
||||
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
|
||||
- var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false,fingerprint:false})
|
||||
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false})
|
||||
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false})
|
||||
- var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false})
|
||||
|
||||
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
|
||||
//- and doesn't prematurely end the script tag.
|
||||
script#data(type="application/json").
|
||||
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')}
|
||||
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History}).replace(/\//g, '\\/')}
|
||||
|
||||
script(type="text/javascript").
|
||||
window.data = JSON.parse($("#data").text());
|
||||
|
@ -126,14 +126,15 @@ block requirejs
|
|||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||
window.requirejs = {
|
||||
"paths" : {
|
||||
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
|
||||
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, qs:{config:'TeX-AMS_HTML'}})}",
|
||||
"moment": "libs/#{lib('moment')}",
|
||||
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
|
||||
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
|
||||
"ace": "#{lib('ace')}",
|
||||
"fineuploader": "libs/#{lib('fineuploader')}"
|
||||
"fineuploader": "libs/#{lib('fineuploader')}",
|
||||
"ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
|
||||
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
|
||||
},
|
||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||
"waitSeconds": 0,
|
||||
"shim": {
|
||||
"pdfjs-dist/build/pdf": {
|
||||
|
@ -155,14 +156,13 @@ block requirejs
|
|||
}
|
||||
}
|
||||
};
|
||||
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
|
||||
window.aceWorkerPath = "#{aceWorkerPath}";
|
||||
window.pdfCMapsPath = "#{pdfCMapsPath}"
|
||||
window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
|
||||
|
||||
script(
|
||||
data-main=buildJsPath("ide.js", {fingerprint:false}),
|
||||
data-main=buildJsPath("ide.js", {hashedPath:false}),
|
||||
baseurl=fullJsPath,
|
||||
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
|
||||
src=buildJsPath('libs/require.js')
|
||||
data-ace-base=buildJsPath(lib('ace')),
|
||||
src=buildJsPath('libs/require.js', {hashedPath:true})
|
||||
)
|
||||
|
|
|
@ -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()"
|
||||
|
|
|
@ -63,6 +63,14 @@ block content
|
|||
aside.project-list-sidebar.col-md-2.col-xs-3
|
||||
include ./list/side-bar
|
||||
|
||||
if isShowingV1Projects && settings.overleaf && settings.overleaf.host
|
||||
.project-list-sidebar-v2-pane.col-md-2.col-xs-3
|
||||
span Welcome to the Overleaf v2 alpha! #[a(href="https://www.overleaf.com/help/v2") Find out more].
|
||||
span To tag or rename your v1 projects, please go back to Overleaf v1.
|
||||
a.project-list-sidebar-v1-link(
|
||||
href=settings.overleaf.host + "/dash?prefer-v1-dash=1"
|
||||
) Go back to v1
|
||||
|
||||
.project-list-main.col-md-10.col-xs-9
|
||||
include ./list/notifications
|
||||
include ./list/project-list
|
||||
|
|
|
@ -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 }}'"
|
||||
|
|
|
@ -317,38 +317,56 @@ script(type="text/ng-template", id="userProfileModalTemplate")
|
|||
script(type="text/ng-template", id="v1ImportModalTemplate")
|
||||
.modal-header
|
||||
button.close(ng-click="dismiss()") ×
|
||||
h3 #{translate("import_project_to_v2")}
|
||||
h3 Move Project to Overleaf v2
|
||||
|
||||
.modal-body.v1-import-wrapper
|
||||
.v1-import-step-1(ng-show="step === 1")
|
||||
img.v1-import-img(
|
||||
src="/img/v1-import/v2-editor.png"
|
||||
alt="The new V2 Editor."
|
||||
)
|
||||
h2.v1-import-title Try importing your project to V2!
|
||||
p Some exciting copy about the new features:
|
||||
ul
|
||||
li Some stuff
|
||||
li Some more stuff
|
||||
li Yet more stuff
|
||||
.v1-import-row
|
||||
.v1-import-col
|
||||
img.v1-import-img(
|
||||
src="/img/v1-import/v2-editor.png"
|
||||
alt="The new V2 Editor."
|
||||
)
|
||||
.v1-import-col
|
||||
h2.v1-import-title Try the Overleaf v2 Editor
|
||||
p The Overleaf v2 editor has many great new features including:
|
||||
ul
|
||||
li Faster real-time collaboration
|
||||
li See your coauthors’ cursors
|
||||
li Chat with math support
|
||||
li Tracked changes and commenting
|
||||
li Improved LaTeX autocomplete
|
||||
li Two-way Dropbox sync
|
||||
p.v1-import-cta Would you like to move #[strong {{project.name}}] into Overleaf v2?
|
||||
|
||||
.v1-import-step-2(ng-show="step === 2")
|
||||
div.v1-import-warning(aria-label="Warning symbol.")
|
||||
i.fa.fa-exclamation-triangle
|
||||
h2.v1-import-title #[strong Warning:] Overleaf V2 is in beta
|
||||
p Once importing your project you will lose access to the some of the features of Overleaf V1. This includes the git bridge, journal integrations, WYSIWYG and linked files. We’re working on bringing these features to V2!
|
||||
p Once you have imported a project to V2 you #[strong cannot go back to V1].
|
||||
p Are you sure you want to import to V2?
|
||||
.v1-import-row
|
||||
.v1-import-warning.v1-import-col(aria-label="Warning symbol.")
|
||||
i.fa.fa-exclamation-triangle
|
||||
.v1-import-col
|
||||
h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental
|
||||
p We are still working hard to bring some Overleaf v1 features to the v2 editor. If you move this project to v2 now, you will:
|
||||
ul
|
||||
li Lose access your project via git
|
||||
li Not be able to use the Journals and Services menu to submit directly to our partners
|
||||
li Not be able to use the Rich Text (WYSIWYG) mode
|
||||
li Not be able to use linked files (to URLs or to files in other Overleaf projects)
|
||||
li Not be able to use some bibliography integrations (Zotero, CiteULike)
|
||||
li Lose access to your labelled versions and not be able to create new labelled versions
|
||||
.v1-import-cta
|
||||
p
|
||||
strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2.
|
||||
p Are you sure you want to move #[strong {{project.name}}] into Overleaf v2?
|
||||
|
||||
.modal-footer.v1-import-footer
|
||||
div(ng-show="step === 1")
|
||||
if settings.overleaf && settings.overleaf.host
|
||||
a.btn.btn-primary.v1-import-btn(
|
||||
ng-href=settings.overleaf.host + "/{{project.id}}"
|
||||
) #{translate("open_in_v1")}
|
||||
) No thanks, open in v1
|
||||
button.btn.btn-primary.v1-import-btn(
|
||||
ng-click="moveToConfirmation()"
|
||||
) #{translate("import_to_v2")}
|
||||
) Yes, move project to v2
|
||||
div(ng-show="step === 2")
|
||||
form(
|
||||
async-form="v1Import",
|
||||
|
@ -363,9 +381,9 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
|
|||
a.btn.btn-primary.v1-import-btn(
|
||||
ng-href=settings.overleaf.host + "/{{project.id}}"
|
||||
ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}"
|
||||
) #{translate("never_mind_open_in_v1")}
|
||||
) No thanks, open in v1
|
||||
input.btn.btn-primary.v1-import-btn(
|
||||
type="submit",
|
||||
value=translate('yes_im_sure')
|
||||
value="Yes, move project to v2"
|
||||
ng-disabled="v1ImportForm.inflight || v1ImportForm.success"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
script(type="text/ng-template", id="v1ProjectTooltipTemplate")
|
||||
span This project is from Overleaf version 1 and has not been imported to the beta yet
|
||||
span This project is from Overleaf v1 and has not been imported to v2 yet.
|
||||
|
||||
|
||||
script(type="text/ng-template", id="v1TagTooltipTemplate")
|
||||
span This folder is from Overleaf version 1 and has not been imported to the beta yet
|
||||
span This folder/tag is from Overleaf v1. Please go back to v1 to manage.
|
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",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
define [
|
||||
"libs"
|
||||
"libraries"
|
||||
"modules/recursionHelper"
|
||||
"modules/errorCatcher"
|
||||
"modules/localStorage"
|
||||
|
|
|
@ -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 })"
|
||||
]
|
|
@ -33,7 +33,7 @@ define [
|
|||
if !ace.config._moduleUrl?
|
||||
ace.config._moduleUrl = ace.config.moduleUrl
|
||||
ace.config.moduleUrl = (args...) ->
|
||||
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
|
||||
url = ace.config._moduleUrl(args...)
|
||||
return url
|
||||
|
||||
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) ->
|
||||
|
|
|
@ -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,5 +1,5 @@
|
|||
define [
|
||||
"libs"
|
||||
"libraries"
|
||||
], () ->
|
||||
angular.module('underscore', []).factory '_', ->
|
||||
return window._
|
||||
|
|
|
@ -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"});
|
||||
|
1
services/web/public/stylesheets/_ol_style_includes.less
Normal file
1
services/web/public/stylesheets/_ol_style_includes.less
Normal file
|
@ -0,0 +1 @@
|
|||
@import "app/sidebar-v2-dash-pane.less";
|
|
@ -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;
|
||||
|
|
|
@ -2,8 +2,34 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.v1-import-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.v1-import-col {
|
||||
flex-basis: 50%;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.v1-import-col ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.v1-import-img {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.v1-import-cta {
|
||||
margin-top: 20px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v1-import-warning {
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
}
|
||||
|
||||
.project-list-sidebar when (@is-overleaf) {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
@v2-dash-pane-link-height: 130px;
|
||||
|
||||
.project-list-sidebar {
|
||||
height: calc(~"100% -" @v2-dash-pane-link-height);
|
||||
}
|
||||
|
||||
.project-list-sidebar-v2-pane {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: @v2-dash-pane-link-height;
|
||||
background-color: @v2-dash-pane-bg;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.project-list-sidebar-v2-pane a {
|
||||
color: @v2-dash-pane-link-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.project-list-sidebar-v2-pane a:hover {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -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;
|
||||
|
@ -110,6 +135,10 @@
|
|||
@sidebar-active-bg : @ol-blue-gray-6;
|
||||
@sidebar-hover-bg : @ol-blue-gray-4;
|
||||
@sidebar-hover-text-decoration : none;
|
||||
@v2-dash-pane-bg : @ol-blue-gray-4;
|
||||
@v2-dash-pane-link-color : #FFF;
|
||||
@v2-dash-pane-btn-bg : @ol-blue-gray-5;
|
||||
@v2-dash-pane-btn-hover-bg : @ol-blue-gray-6;
|
||||
|
||||
@folders-menu-margin : 0 -(@grid-gutter-width / 2);
|
||||
@folders-menu-line-height : @structured-list-line-height;
|
||||
|
@ -173,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;
|
||||
|
@ -193,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;
|
||||
|
@ -212,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,4 +1,8 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700&subset=latin-ext);
|
||||
@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
|
||||
|
||||
// Core variables and mixins
|
||||
@import "core/ol-variables.less";
|
||||
@import "app/ol-style-guide.less";
|
||||
@import "_style_includes.less";
|
||||
@import "_style_includes.less";
|
||||
@import "_ol_style_includes.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…
Reference in a new issue