Merge branch 'master' into sk-fix-references-full-index

This commit is contained in:
Shane Kilkelly 2018-05-14 13:45:12 +01:00
commit 06c0b45ef7
25 changed files with 323 additions and 79 deletions

View file

@ -43,7 +43,6 @@ Unit tests can be run in the `test_unit` container defined in `docker-compose.te
The makefile contains a short cut to run these: The makefile contains a short cut to run these:
``` ```
make install # Only needs running once, or when npm packages are updated
make unit_test make unit_test
``` ```

View file

@ -47,6 +47,13 @@ UnsupportedExportRecordsError = (message) ->
return error return error
UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype
V1HistoryNotSyncedError = (message) ->
error = new Error(message)
error.name = "V1HistoryNotSyncedError"
error.__proto__ = V1HistoryNotSyncedError.prototype
return error
V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype
ProjectHistoryDisabledError = (message) -> ProjectHistoryDisabledError = (message) ->
error = new Error(message) error = new Error(message)
error.name = "ProjectHistoryDisabledError " error.name = "ProjectHistoryDisabledError "
@ -62,4 +69,5 @@ module.exports = Errors =
UnsupportedFileTypeError: UnsupportedFileTypeError UnsupportedFileTypeError: UnsupportedFileTypeError
UnsupportedBrandError: UnsupportedBrandError UnsupportedBrandError: UnsupportedBrandError
UnsupportedExportRecordsError: UnsupportedExportRecordsError UnsupportedExportRecordsError: UnsupportedExportRecordsError
V1HistoryNotSyncedError: V1HistoryNotSyncedError
ProjectHistoryDisabledError: ProjectHistoryDisabledError ProjectHistoryDisabledError: ProjectHistoryDisabledError

View file

@ -299,6 +299,8 @@ module.exports = ProjectController =
autoPairDelimiters: user.ace.autoPairDelimiters autoPairDelimiters: user.ace.autoPairDelimiters
pdfViewer : user.ace.pdfViewer pdfViewer : user.ace.pdfViewer
syntaxValidation: user.ace.syntaxValidation syntaxValidation: user.ace.syntaxValidation
fontFamily: user.ace.fontFamily
lineHeight: user.ace.lineHeight
} }
trackChangesState: project.track_changes trackChangesState: project.track_changes
privilegeLevel: privilegeLevel privilegeLevel: privilegeLevel
@ -311,6 +313,7 @@ module.exports = ProjectController =
maxDocLength: Settings.max_doc_length maxDocLength: Settings.max_doc_length
useV2History: !!project.overleaf?.history?.display useV2History: !!project.overleaf?.history?.display
showRichText: req.query?.rt == 'true' showRichText: req.query?.rt == 'true'
showPublishModal: req.query?.pm == 'true'
timer.done() timer.done()
_buildProjectList: (allProjects, v1Projects = [])-> _buildProjectList: (allProjects, v1Projects = [])->

View file

@ -16,38 +16,42 @@ AnalyticsManger = require("../Analytics/AnalyticsManager")
module.exports = ProjectCreationHandler = module.exports = ProjectCreationHandler =
createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)-> createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)->
metrics.inc("project-creation") metrics.inc("project-creation")
if arguments.length == 3 if arguments.length == 3
callback = projectHistoryId callback = attributes
projectHistoryId = null attributes = null
ProjectDetailsHandler.validateProjectName projectName, (error) -> ProjectDetailsHandler.validateProjectName projectName, (error) ->
return callback(error) if error? return callback(error) if error?
logger.log owner_id:owner_id, projectName:projectName, "creating blank project" logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
if projectHistoryId? if attributes?
ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, (error, project) -> ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) ->
return callback(error) if error? return callback(error) if error?
AnalyticsManger.recordEvent( AnalyticsManger.recordEvent(
owner_id, 'project-imported', { projectId: project._id, projectHistoryId: projectHistoryId } owner_id, 'project-imported', { projectId: project._id, attributes: attributes }
) )
callback(error, project) callback(error, project)
else else
HistoryManager.initializeProject (error, history) -> HistoryManager.initializeProject (error, history) ->
return callback(error) if error? return callback(error) if error?
ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, (error, project) -> attributes = overleaf: history: id: history?.overleaf_id
ProjectCreationHandler._createBlankProject owner_id, projectName, attributes, (error, project) ->
return callback(error) if error? return callback(error) if error?
AnalyticsManger.recordEvent( AnalyticsManger.recordEvent(
owner_id, 'project-created', { projectId: project._id } owner_id, 'project-created', { projectId: project._id }
) )
callback(error, project) callback(error, project)
_createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)-> _createBlankProject : (owner_id, projectName, attributes, callback = (error, project) ->)->
rootFolder = new Folder {'name':'rootFolder'} rootFolder = new Folder {'name':'rootFolder'}
project = new Project
owner_ref : new ObjectId(owner_id) attributes.owner_ref = new ObjectId(owner_id)
name : projectName attributes.name = projectName
project.overleaf.history.id = projectHistoryId project = new Project attributes
Object.assign(project, attributes)
if Settings.apis?.project_history?.displayHistoryForNewProjects if Settings.apis?.project_history?.displayHistoryForNewProjects
project.overleaf.history.display = true project.overleaf.history.display = true
if Settings.currentImageName? if Settings.currentImageName?

View file

@ -405,14 +405,41 @@ module.exports = ProjectEntityUpdateHandler = self =
DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback
_cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) -> _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
self._updateProjectStructureWithDeletedEntity project, entity, entityType, path, userId, (error) ->
return callback(error) if error?
if(entityType.indexOf("file") != -1)
self._cleanUpFile project, entity, path, userId, callback
else if (entityType.indexOf("doc") != -1)
self._cleanUpDoc project, entity, path, userId, callback
else if (entityType.indexOf("folder") != -1)
self._cleanUpFolder project, entity, path, userId, callback
else
callback()
# Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity
# methods both need to recursively iterate over the entities in folder.
# These are currently using separate implementations of the recursion. In
# future, these could be simplified using a common project entity iterator.
_updateProjectStructureWithDeletedEntity: (project, entity, entityType, entityPath, userId, callback = (error) ->) ->
# compute the changes to the project structure
if(entityType.indexOf("file") != -1) if(entityType.indexOf("file") != -1)
self._cleanUpFile project, entity, path, userId, callback changes = oldFiles: [ {file: entity, path: entityPath} ]
else if (entityType.indexOf("doc") != -1) else if (entityType.indexOf("doc") != -1)
self._cleanUpDoc project, entity, path, userId, callback changes = oldDocs: [ {doc: entity, path: entityPath} ]
else if (entityType.indexOf("folder") != -1) else if (entityType.indexOf("folder") != -1)
self._cleanUpFolder project, entity, path, userId, callback changes = {oldDocs: [], oldFiles: []}
else _recurseFolder = (folder, folderPath) ->
callback() for doc in folder.docs
changes.oldDocs.push {doc, path: path.join(folderPath, doc.name)}
for file in folder.fileRefs
changes.oldFiles.push {file, path: path.join(folderPath, file.name)}
for childFolder in folder.folders
_recurseFolder(childFolder, path.join(folderPath, childFolder.name))
_recurseFolder entity, entityPath
# now send the project structure changes to the docupdater
project_id = project._id.toString()
projectHistoryId = project.overleaf?.history?.id
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
_cleanUpDoc: (project, doc, path, userId, callback = (error) ->) -> _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) ->
project_id = project._id.toString() project_id = project._id.toString()
@ -429,21 +456,10 @@ module.exports = ProjectEntityUpdateHandler = self =
return callback(error) if error? return callback(error) if error?
DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) -> DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error? return callback(error) if error?
DocstoreManager.deleteDoc project_id, doc_id, (error) -> DocstoreManager.deleteDoc project_id, doc_id, callback
return callback(error) if error?
changes = oldDocs: [ {doc, path} ]
projectHistoryId = project.overleaf?.history?.id
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
_cleanUpFile: (project, file, path, userId, callback = (error) ->) -> _cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, (error) -> ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, callback
return callback(error) if error?
project_id = project._id.toString()
projectHistoryId = project.overleaf?.history?.id
changes = oldFiles: [ {file, path} ]
# we are now keeping a copy of every file versio so we no longer delete
# the file from the filestore
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
_cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) -> _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) ->
jobs = [] jobs = []

View file

@ -81,6 +81,11 @@ module.exports = UserController =
user.ace.pdfViewer = req.body.pdfViewer user.ace.pdfViewer = req.body.pdfViewer
if req.body.syntaxValidation? if req.body.syntaxValidation?
user.ace.syntaxValidation = req.body.syntaxValidation user.ace.syntaxValidation = req.body.syntaxValidation
if req.body.fontFamily?
user.ace.fontFamily = req.body.fontFamily
if req.body.lineHeight?
user.ace.lineHeight = req.body.lineHeight
user.save (err)-> user.save (err)->
newEmail = req.body.email?.trim().toLowerCase() newEmail = req.body.email?.trim().toLowerCase()
if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed() if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed()

View file

@ -183,6 +183,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
# Don't include the query string parameters, otherwise Google # Don't include the query string parameters, otherwise Google
# treats ?nocdn=true as the canonical version # treats ?nocdn=true as the canonical version
res.locals.currentUrl = Url.parse(req.originalUrl).pathname res.locals.currentUrl = Url.parse(req.originalUrl).pathname
res.locals.capitalize = (string) ->
return "" if string.length == 0
return string.charAt(0).toUpperCase() + string.slice(1)
next() next()
webRouter.use (req, res, next)-> webRouter.use (req, res, next)->
@ -321,5 +324,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
chatMessageBorderLightness : if isOl then "40%" else "70%" chatMessageBorderLightness : if isOl then "40%" else "70%"
chatMessageBgSaturation : if isOl then "85%" else "60%" chatMessageBgSaturation : if isOl then "85%" else "60%"
chatMessageBgLightness : if isOl then "40%" else "97%" chatMessageBgLightness : if isOl then "40%" else "97%"
defaultFontFamily : if isOl then 'lucida' else 'monaco'
defaultLineHeight : if isOl then 'normal' else 'compact'
renderAnnouncements : !isOl renderAnnouncements : !isOl
next() next()

View file

@ -14,9 +14,9 @@ module.exports = Features =
return Settings.enableGithubSync return Settings.enableGithubSync
when 'v1-return-message' when 'v1-return-message'
return Settings.accountMerge? and Settings.overleaf? return Settings.accountMerge? and Settings.overleaf?
when 'publish-modal'
return Settings.showPublishModal
when 'custom-togglers' when 'custom-togglers'
return Settings.overleaf? return Settings.overleaf?
when 'templates'
return !Settings.overleaf?
else else
throw new Error("unknown feature: #{feature}") throw new Error("unknown feature: #{feature}")

View file

@ -3,20 +3,37 @@ Settings = require('settings-sharelatex')
RedisWrapper = require("./RedisWrapper") RedisWrapper = require("./RedisWrapper")
rclient = RedisWrapper.client("lock") rclient = RedisWrapper.client("lock")
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
os = require "os"
crypto = require "crypto"
HOST = os.hostname()
PID = process.pid
RND = crypto.randomBytes(4).toString('hex')
COUNT = 0
module.exports = LockManager = module.exports = LockManager =
LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock LOCK_TEST_INTERVAL: 50 # 50ms between each test of the lock
MAX_TEST_INTERVAL: 1000 # back off to 1s between each test of the lock
MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock MAX_LOCK_WAIT_TIME: 10000 # 10s maximum time to spend trying to get the lock
REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis REDIS_LOCK_EXPIRY: 30 # seconds. Time until lock auto expires in redis
SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log SLOW_EXECUTION_THRESHOLD: 5000 # 5s, if execution takes longer than this then log
# Use a signed lock value as described in
# http://redis.io/topics/distlock#correct-implementation-with-a-single-instance
# to prevent accidental unlocking by multiple processes
randomLock : () ->
time = Date.now()
return "locked:host=#{HOST}:pid=#{PID}:random=#{RND}:time=#{time}:count=#{COUNT++}"
unlockScript: 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'
runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) -> runWithLock: (namespace, id, runner = ( (releaseLock = (error) ->) -> ), callback = ( (error) -> )) ->
# This error is defined here so we get a useful stacktrace # This error is defined here so we get a useful stacktrace
slowExecutionError = new Error "slow execution during lock" slowExecutionError = new Error "slow execution during lock"
timer = new metrics.Timer("lock.#{namespace}") timer = new metrics.Timer("lock.#{namespace}")
key = "lock:web:#{namespace}:#{id}" key = "lock:web:#{namespace}:#{id}"
LockManager._getLock key, namespace, (error) -> LockManager._getLock key, namespace, (error, lockValue) ->
return callback(error) if error? return callback(error) if error?
# The lock can expire in redis but the process carry on. This setTimout call # The lock can expire in redis but the process carry on. This setTimout call
@ -27,7 +44,7 @@ module.exports = LockManager =
exceededLockTimeout = setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY * 1000 exceededLockTimeout = setTimeout countIfExceededLockTimeout, LockManager.REDIS_LOCK_EXPIRY * 1000
runner (error1, values...) -> runner (error1, values...) ->
LockManager._releaseLock key, (error2) -> LockManager._releaseLock key, lockValue, (error2) ->
clearTimeout exceededLockTimeout clearTimeout exceededLockTimeout
timeTaken = new Date - timer.start timeTaken = new Date - timer.start
@ -39,19 +56,21 @@ module.exports = LockManager =
return callback(error) if error? return callback(error) if error?
callback null, values... callback null, values...
_tryLock : (key, namespace, callback = (err, isFree)->)-> _tryLock : (key, namespace, callback = (err, isFree, lockValue)->)->
rclient.set key, "locked", "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)-> lockValue = LockManager.randomLock()
rclient.set key, lockValue, "EX", LockManager.REDIS_LOCK_EXPIRY, "NX", (err, gotLock)->
return callback(err) if err? return callback(err) if err?
if gotLock == "OK" if gotLock == "OK"
metrics.inc "lock.#{namespace}.try.success" metrics.inc "lock.#{namespace}.try.success"
callback err, true callback err, true, lockValue
else else
metrics.inc "lock.#{namespace}.try.failed" metrics.inc "lock.#{namespace}.try.failed"
logger.log key: key, redis_response: gotLock, "lock is locked" logger.log key: key, redis_response: gotLock, "lock is locked"
callback err, false callback err, false
_getLock: (key, namespace, callback = (error) ->) -> _getLock: (key, namespace, callback = (error, lockValue) ->) ->
startTime = Date.now() startTime = Date.now()
testInterval = LockManager.LOCK_TEST_INTERVAL
attempts = 0 attempts = 0
do attempt = () -> do attempt = () ->
if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME if Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME
@ -59,13 +78,23 @@ module.exports = LockManager =
return callback(new Error("Timeout")) return callback(new Error("Timeout"))
attempts += 1 attempts += 1
LockManager._tryLock key, namespace, (error, gotLock) -> LockManager._tryLock key, namespace, (error, gotLock, lockValue) ->
return callback(error) if error? return callback(error) if error?
if gotLock if gotLock
metrics.gauge "lock.#{namespace}.get.success.tries", attempts metrics.gauge "lock.#{namespace}.get.success.tries", attempts
callback(null) callback(null, lockValue)
else else
setTimeout attempt, LockManager.LOCK_TEST_INTERVAL setTimeout attempt, testInterval
# back off when the lock is taken to avoid overloading
testInterval = Math.min(testInterval * 2, LockManager.MAX_TEST_INTERVAL)
_releaseLock: (key, callback)-> _releaseLock: (key, lockValue, callback)->
rclient.del key, callback rclient.eval LockManager.unlockScript, 1, key, lockValue, (err, result) ->
if err?
return callback(err)
else if result? and result isnt 1 # successful unlock should release exactly one key
logger.error {key:key, lockValue:lockValue, redis_err:err, redis_result:result}, "unlocking error"
metrics.inc "unlock-error"
return callback(new Error("tried to release timed out lock"))
else
callback(null,result)

View file

@ -20,14 +20,16 @@ UserSchema = new Schema
loginCount : {type : Number, default: 0} loginCount : {type : Number, default: 0}
holdingAccount : {type : Boolean, default: false} holdingAccount : {type : Boolean, default: false}
ace : { ace : {
mode : {type : String, default: 'none'} mode : {type : String, default: 'none'}
theme : {type : String, default: 'textmate'} theme : {type : String, default: 'textmate'}
fontSize : {type : Number, default:'12'} fontSize : {type : Number, default:'12'}
autoComplete: {type : Boolean, default: true} autoComplete : {type : Boolean, default: true}
autoPairDelimiters: {type : Boolean, default: true} autoPairDelimiters : {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"} spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"} pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean} syntaxValidation : {type : Boolean}
fontFamily : {type : String}
lineHeight : {type : String}
} }
features : { features : {
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators } collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }

View file

@ -154,7 +154,10 @@ block requirejs
}, },
"ace/ext-language_tools": { "ace/ext-language_tools": {
"deps": ["ace/ace"] "deps": ["ace/ace"]
} },
"ace/keybinding-vim": {
"deps": ["ace/ace"]
},
}, },
"config":{ "config":{
"moment":{ "moment":{

View file

@ -58,6 +58,7 @@ div.full-size(
read-only="!permissions.write", read-only="!permissions.write",
file-name="editor.open_doc_name", file-name="editor.open_doc_name",
on-ctrl-enter="recompileViaKey", on-ctrl-enter="recompileViaKey",
on-save="recompileViaKey",
on-ctrl-j="toggleReviewPanel", on-ctrl-j="toggleReviewPanel",
on-ctrl-shift-c="addNewCommentFromKbdShortcut", on-ctrl-shift-c="addNewCommentFromKbdShortcut",
on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut", on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut",
@ -68,6 +69,8 @@ div.full-size(
track-changes= "editor.trackChanges", track-changes= "editor.trackChanges",
doc-id="editor.open_doc_id" doc-id="editor.open_doc_id"
renderer-data="reviewPanel.rendererData" renderer-data="reviewPanel.rendererData"
font-family="settings.fontFamily || ui.defaultFontFamily"
line-height="settings.lineHeight || ui.defaultLineHeight"
) )
!= moduleIncludes('editor:body', locals) != moduleIncludes('editor:body', locals)

View file

@ -150,6 +150,26 @@ aside#left-menu.full-size(
each size in ['10','11','12','13','14','16','20','24'] each size in ['10','11','12','13','14','16','20','24']
option(value=size) #{size}px option(value=size) #{size}px
.form-controls
label(for="fontFamily") #{translate("font_family")}
select(
name="fontFamily"
ng-model="settings.fontFamily"
)
option(value="", disabled) #{translate("default")}
each fontFamily in ['monaco', 'lucida']
option(value=fontFamily) #{capitalize(fontFamily)}
.form-controls
label(for="lineHeight") #{translate("line_height")}
select(
name="lineHeight"
ng-model="settings.lineHeight"
)
option(value="", disabled) #{translate("default")}
each lineHeight in ['compact', 'normal', 'wide']
option(value=lineHeight) #{translate(lineHeight)}
.form-controls .form-controls
label(for="pdfViewer") #{translate("pdf_viewer")} label(for="pdfViewer") #{translate("pdf_viewer")}
select( select(

View file

@ -32,14 +32,16 @@
ng-click="downloadSelectedProjects()" ng-click="downloadSelectedProjects()"
) )
i.fa.fa-cloud-download i.fa.fa-cloud-download
- var archiveButtonString = settings.overleaf ? translate("archive") : translate("delete")
- var archiveButtonIcon = settings.overleaf ? "fa-inbox" : "fa-trash-o"
a.btn.btn-default( a.btn.btn-default(
href, href,
tooltip=translate('delete'), tooltip=`{{ isArchiveableProjectSelected ? '${archiveButtonString}' : '${translate("leave")}' }}`,
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()" ng-click="openArchiveProjectsModal()"
) )
i.fa.fa-trash-o i.fa(ng-class=`isArchiveableProjectSelected ? '${archiveButtonIcon}' : 'fa-sign-out'`)
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
a.btn.btn-default.dropdown-toggle( a.btn.btn-default.dropdown-toggle(

View file

@ -41,7 +41,7 @@
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')") li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
a(href) #{translate("shared_with_you")} a(href) #{translate("shared_with_you")}
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
a(href) #{translate("deleted_projects")} a(href) #{settings.overleaf ? translate("archived_projects") : translate("deleted_projects")}
if isShowingV1Projects if isShowingV1Projects
li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')") li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')")
a(href) #{translate("v1_projects")} a(href) #{translate("v1_projects")}

View file

@ -80,6 +80,8 @@ define [
miniReviewPanelVisible: false miniReviewPanelVisible: false
chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen
chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed
defaultFontFamily: window.uiConfig.defaultFontFamily
defaultLineHeight: window.uiConfig.defaultLineHeight
} }
$scope.user = window.user $scope.user = window.user

View file

@ -3,6 +3,7 @@ define [
"ace/ace" "ace/ace"
"ace/ext-searchbox" "ace/ext-searchbox"
"ace/ext-modelist" "ace/ext-modelist"
"ace/keybinding-vim"
"ide/editor/directives/aceEditor/undo/UndoManager" "ide/editor/directives/aceEditor/undo/UndoManager"
"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager" "ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
"ide/editor/directives/aceEditor/spell-check/SpellCheckManager" "ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
@ -14,9 +15,10 @@ define [
"ide/graphics/services/graphics" "ide/graphics/services/graphics"
"ide/preamble/services/preamble" "ide/preamble/services/preamble"
"ide/files/services/files" "ide/files/services/files"
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) -> ], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
EditSession = ace.require('ace/edit_session').EditSession EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist') ModeList = ace.require('ace/ext/modelist')
Vim = ace.require('ace/keyboard/vim').Vim
# set the path for ace workers if using a CDN (from editor.pug) # set the path for ace workers if using a CDN (from editor.pug)
if window.aceWorkerPath != "" if window.aceWorkerPath != ""
@ -60,6 +62,7 @@ define [
onCtrlJ: "=" # Toggle the review panel onCtrlJ: "=" # Toggle the review panel
onCtrlShiftC: "=" # Add a new comment onCtrlShiftC: "=" # Add a new comment
onCtrlShiftA: "=" # Toggle track-changes on/off onCtrlShiftA: "=" # Toggle track-changes on/off
onSave: "=" # Cmd/Ctrl-S or :w in Vim
syntaxValidation: "=" syntaxValidation: "="
reviewPanel: "=" reviewPanel: "="
eventsBridge: "=" eventsBridge: "="
@ -67,6 +70,8 @@ define [
trackChangesEnabled: "=" trackChangesEnabled: "="
docId: "=" docId: "="
rendererData: "=" rendererData: "="
lineHeight: "="
fontFamily: "="
} }
link: (scope, element, attrs) -> link: (scope, element, attrs) ->
# Don't freak out if we're already in an apply callback # Don't freak out if we're already in an apply callback
@ -106,16 +111,26 @@ define [
metadataManager = new MetadataManager(scope, editor, element, metadata) metadataManager = new MetadataManager(scope, editor, element, metadata)
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files) autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
# Prevert Ctrl|Cmd-S from triggering save dialog scope.$watch "onSave", (callback) ->
editor.commands.addCommand if callback?
name: "save", Vim.defineEx 'write', 'w', callback
bindKey: win: "Ctrl-S", mac: "Command-S" editor.commands.addCommand
exec: () -> name: "save",
readOnly: true bindKey: win: "Ctrl-S", mac: "Command-S"
exec: callback
readOnly: true
# Not technically 'save', but Ctrl-. recompiles in OL v1
# so maintain compatibility
editor.commands.addCommand
name: "recompile_v1",
bindKey: win: "Ctrl-.", mac: "Ctrl-."
exec: callback
readOnly: true
editor.commands.removeCommand "transposeletters" editor.commands.removeCommand "transposeletters"
editor.commands.removeCommand "showSettingsMenu" editor.commands.removeCommand "showSettingsMenu"
editor.commands.removeCommand "foldall" editor.commands.removeCommand "foldall"
# For European keyboards, the / is above 7 so needs Shift pressing. # For European keyboards, the / is above 7 so needs Shift pressing.
# This comes through as Command-Shift-/ on OS X, which is mapped to # This comes through as Command-Shift-/ on OS X, which is mapped to
# toggleBlockComment. # toggleBlockComment.
@ -266,6 +281,29 @@ define [
"font-size": value + "px" "font-size": value + "px"
}) })
scope.$watch "fontFamily", (value) ->
if value?
switch value
when 'monaco'
editor.setOption('fontFamily', '"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace')
when 'lucida'
editor.setOption('fontFamily', '"Lucida Console", monospace')
else
editor.setOption('fontFamily', null)
scope.$watch "lineHeight", (value) ->
if value?
switch value
when 'compact'
editor.container.style.lineHeight = 1.33
when 'normal'
editor.container.style.lineHeight = 1.6
when 'wide'
editor.container.style.lineHeight = 2
else
editor.container.style.lineHeight = 1.6
editor.renderer.updateFontSize()
scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) -> scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
if old_sharejs_doc? if old_sharejs_doc?
detachFromAce(old_sharejs_doc) detachFromAce(old_sharejs_doc)

View file

@ -8,6 +8,12 @@ define [
if $scope.settings.pdfViewer not in ["pdfjs", "native"] if $scope.settings.pdfViewer not in ["pdfjs", "native"]
$scope.settings.pdfViewer = "pdfjs" $scope.settings.pdfViewer = "pdfjs"
if $scope.settings.fontFamily? and $scope.settings.fontFamily not in ["monaco", "lucida"]
delete $scope.settings.fontFamily
if $scope.settings.lineHeight? and $scope.settings.lineHeight not in ["compact", "normal", "wide"]
delete $scope.settings.lineHeight
$scope.fontSizeAsStr = (newVal) -> $scope.fontSizeAsStr = (newVal) ->
if newVal? if newVal?
$scope.settings.fontSize = newVal $scope.settings.fontSize = newVal
@ -41,6 +47,14 @@ define [
if syntaxValidation != oldSyntaxValidation if syntaxValidation != oldSyntaxValidation
settings.saveSettings({syntaxValidation: syntaxValidation}) settings.saveSettings({syntaxValidation: syntaxValidation})
$scope.$watch "settings.fontFamily", (fontFamily, oldFontFamily) =>
if fontFamily != oldFontFamily
settings.saveSettings({fontFamily: fontFamily})
$scope.$watch "settings.lineHeight", (lineHeight, oldLineHeight) =>
if lineHeight != oldLineHeight
settings.saveSettings({lineHeight: lineHeight})
$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) => $scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
return if @ignoreUpdates return if @ignoreUpdates
if oldLanguage? and language != oldLanguage if oldLanguage? and language != oldLanguage

View file

@ -8,6 +8,7 @@ define [
$scope.notifications = window.data.notifications $scope.notifications = window.data.notifications
$scope.allSelected = false $scope.allSelected = false
$scope.selectedProjects = [] $scope.selectedProjects = []
$scope.isArchiveableProjectSelected = false
$scope.filter = "all" $scope.filter = "all"
$scope.predicate = "lastUpdated" $scope.predicate = "lastUpdated"
$scope.nUntagged = 0 $scope.nUntagged = 0
@ -85,6 +86,8 @@ define [
$scope.updateSelectedProjects = () -> $scope.updateSelectedProjects = () ->
$scope.selectedProjects = $scope.projects.filter (project) -> project.selected $scope.selectedProjects = $scope.projects.filter (project) -> project.selected
$scope.isArchiveableProjectSelected = $scope.selectedProjects.some (project) ->
window.user_id == project.owner._id
$scope.getSelectedProjects = () -> $scope.getSelectedProjects = () ->
$scope.selectedProjects $scope.selectedProjects

View file

@ -0,0 +1,71 @@
ace.define("ace/theme/overleaf",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
"use strict";
exports.isDark = false;
exports.cssClass = "ace-overleaf";
exports.cssText = ".ace-overleaf .ace_gutter {\
background: #f0f0f0;\
color: #333;\
}\
.ace-overleaf .ace_print-margin {\
width: 1px;\
background: #e8e8e8;\
}\
.ace-overleaf {\
background-color: #FFFFFF;\
color: black;\
}\
.ace-overleaf .ace_cursor {\
color: black;\
}\
.ace-overleaf .ace_marker-layer .ace_selection {\
background: rgb(181, 213, 255);\
}\
.ace-overleaf.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px white;\
}\
.ace-overleaf .ace_marker-layer .ace_step {\
background: rgb(252, 255, 0);\
}\
.ace-overleaf .ace_marker-layer .ace_bracket {\
border: 1px solid #5A5CAD;\
}\
.ace-overleaf .ace_marker-layer .ace_active-line {\
background: rgba(0, 0, 0, 0.07);\
}\
.ace-overleaf .ace_gutter-active-line {\
background-color: #dcdcdc;\
}\
.ace-overleaf .ace_marker-layer .ace_selected-word {\
background: rgb(250, 250, 255);\
border: 1px solid rgb(200, 200, 250);\
}\
.ace-overleaf .ace_fold {\
background-color: #6B72E6;\
}\
.ace-overleaf .ace_comment {\
color: #0080FF;\
font-style: italic;\
}\
.ace-overleaf .ace_storage,\
.ace-overleaf .ace_keyword {\
color: #3F7F7F;\
}\
.ace-overleaf .ace_variable,\
.ace-overleaf .ace_string {\
color: #5A5CAD;\
}\
";
exports.$id = "ace/theme/overleaf";
var dom = require("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
});
(function() {
ace.require(["ace/theme/overleaf"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View file

@ -22,6 +22,7 @@ describe "ProjectStructureMongoLock", ->
before (done) -> before (done) ->
# We want to instantly fail if the lock is taken # We want to instantly fail if the lock is taken
LockManager.MAX_LOCK_WAIT_TIME = 1 LockManager.MAX_LOCK_WAIT_TIME = 1
@lockValue = "lock-value"
userDetails = userDetails =
holdingAccount:false, holdingAccount:false,
email: 'test@example.com' email: 'test@example.com'
@ -33,11 +34,13 @@ describe "ProjectStructureMongoLock", ->
@locked_project = project @locked_project = project
namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE namespace = ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE
@lock_key = "lock:web:#{namespace}:#{project._id}" @lock_key = "lock:web:#{namespace}:#{project._id}"
LockManager._getLock @lock_key, namespace, done LockManager._getLock @lock_key, namespace, (err, lockValue) =>
@lockValue = lockValue
done()
return return
after (done) -> after (done) ->
LockManager._releaseLock @lock_key, done LockManager._releaseLock @lock_key, @lockValue, done
describe 'interacting with the locked project', -> describe 'interacting with the locked project', ->
LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder'] LOCKING_UPDATE_METHODS = ['addDoc', 'addFile', 'mkdirp', 'moveEntity', 'renameEntity', 'addFolder']

View file

@ -98,7 +98,11 @@ describe 'ProjectCreationHandler', ->
it "should set the overleaf id if overleaf id provided", (done)-> it "should set the overleaf id if overleaf id provided", (done)->
overleaf_id = 2345 overleaf_id = 2345
@handler.createBlankProject ownerId, projectName, overleaf_id, (err, project)-> attributes =
overleaf:
history:
id: overleaf_id
@handler.createBlankProject ownerId, projectName, attributes, (err, project)->
project.overleaf.history.id.should.equal overleaf_id project.overleaf.history.id.should.equal overleaf_id
done() done()

View file

@ -872,6 +872,12 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(@project, @entity, @path, userId) .calledWith(@project, @entity, @path, userId)
.should.equal true .should.equal true
it "should should send the update to the doc updater", ->
oldDocs = [ doc: @entity, path: @path ]
@DocumentUpdaterHandler.updateProjectStructure
.calledWith(project_id, projectHistoryId, userId, {oldDocs})
.should.equal true
describe "a folder", -> describe "a folder", ->
beforeEach (done) -> beforeEach (done) ->
@folder = @folder =
@ -905,6 +911,13 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(@project, @doc2, "/folder/doc-name-2", userId) .calledWith(@project, @doc2, "/folder/doc-name-2", userId)
.should.equal true .should.equal true
it "should should send one update to the doc updater for all docs and files", ->
oldFiles = [ {file: @file2, path: "/folder/file-name-2"}, {file: @file1, path: "/folder/subfolder/file-name-1"} ]
oldDocs = [ {doc: @doc2, path: "/folder/doc-name-2"}, { doc: @doc1, path: "/folder/subfolder/doc-name-1"} ]
@DocumentUpdaterHandler.updateProjectStructure
.calledWith(project_id, projectHistoryId, userId, {oldFiles, oldDocs})
.should.equal true
describe "_cleanUpDoc", -> describe "_cleanUpDoc", ->
beforeEach -> beforeEach ->
@doc = @doc =
@ -941,12 +954,6 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWith(project_id, @doc._id.toString()) .calledWith(project_id, @doc._id.toString())
.should.equal true .should.equal true
it "should should send the update to the doc updater", ->
oldDocs = [ doc: @doc, path: @path ]
@DocumentUpdaterHandler.updateProjectStructure
.calledWith(project_id, projectHistoryId, userId, {oldDocs})
.should.equal true
it "should call the callback", -> it "should call the callback", ->
@callback.called.should.equal true @callback.called.should.equal true

View file

@ -3,23 +3,25 @@ assert = require('assert')
path = require('path') path = require('path')
modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js' modulePath = path.join __dirname, '../../../../../app/js/infrastructure/LockManager.js'
lockKey = "lock:web:{#{5678}}" lockKey = "lock:web:{#{5678}}"
lockValue = "123456"
SandboxedModule = require('sandboxed-module') SandboxedModule = require('sandboxed-module')
describe 'LockManager - releasing the lock', ()-> describe 'LockManager - releasing the lock', ()->
deleteStub = sinon.stub().callsArgWith(1) deleteStub = sinon.stub().callsArgWith(4)
mocks = mocks =
"logger-sharelatex": log:-> "logger-sharelatex": log:->
"./RedisWrapper": "./RedisWrapper":
client: ()-> client: ()->
auth:-> auth:->
del:deleteStub eval:deleteStub
LockManager = SandboxedModule.require(modulePath, requires: mocks)
LockManager = SandboxedModule.require(modulePath, requires: mocks)
LockManager.unlockScript = "this is the unlock script"
it 'should put a all data into memory', (done)-> it 'should put a all data into memory', (done)->
LockManager._releaseLock lockKey, -> LockManager._releaseLock lockKey, lockValue, ->
deleteStub.calledWith(lockKey).should.equal true deleteStub.calledWith(LockManager.unlockScript, 1, lockKey, lockValue).should.equal true
done() done()

View file

@ -22,10 +22,11 @@ describe 'LockManager - trying the lock', ->
describe "when the lock is not set", -> describe "when the lock is not set", ->
beforeEach -> beforeEach ->
@set.callsArgWith(5, null, "OK") @set.callsArgWith(5, null, "OK")
@LockManager.randomLock = sinon.stub().returns("random-lock-value")
@LockManager._tryLock @key, @namespace, @callback @LockManager._tryLock @key, @namespace, @callback
it "should set the lock key with an expiry if it is not set", -> it "should set the lock key with an expiry if it is not set", ->
@set.calledWith(@key, "locked", "EX", 30, "NX") @set.calledWith(@key, "random-lock-value", "EX", 30, "NX")
.should.equal true .should.equal true
it "should return the callback with true", -> it "should return the callback with true", ->