Merge branch 'master' into sk-fix-chat-link-color-in-v2

This commit is contained in:
Shane Kilkelly
2018-05-01 15:23:42 +01:00
125 changed files with 2542 additions and 5769 deletions
+3 -1
View File
@@ -49,6 +49,8 @@ TpdsWorker.js
BackgroundJobsWorker.js
UserAndProjectPopulator.coffee
public/es/modules
public/js/*.js
public/js/*.map
public/js/libs/sharejs.js
@@ -80,7 +82,7 @@ public/js/libs/require*.js
app/views/external
/modules/
modules
docker-shared.yml
config/*.coffee
-4
View File
@@ -1,8 +1,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- npm install -g grunt-cli
@@ -16,4 +13,3 @@ script:
services:
- redis-server
- mongodb
+2 -2
View File
@@ -180,7 +180,7 @@ clean_css:
rm -f public/stylesheets/*.css*
clean_ci:
docker-compose down
docker-compose down -v
test: test_unit test_frontend test_acceptance
@@ -221,7 +221,7 @@ test_acceptance_module: $(MODULE_MAKEFILES)
fi
test_clean:
docker-compose ${DOCKER_COMPOSE_FLAGS} down
docker-compose ${DOCKER_COMPOSE_FLAGS} down -v
ci:
MOCHA_ARGS="--reporter tap" \
+10 -3
View File
@@ -1,13 +1,16 @@
MODULE_NAME := $(notdir $(shell pwd))
MODULE_DIR := modules/$(MODULE_NAME)
COFFEE := ../../node_modules/.bin/coffee
APP_COFFEE_FILES := $(shell find app/coffee -name '*.coffee') \
APP_COFFEE_FILES := $(shell [ -e app/coffee ] && find app/coffee -name '*.coffee') \
$(shell [ -e test/unit/coffee ] && find test/unit/coffee -name '*.coffee') \
$(shell [ -e test/acceptance/coffee ] && find test/acceptance/coffee -name '*.coffee')
APP_JS_FILES := $(subst coffee,js,$(APP_COFFEE_FILES))
IDE_COFFEE_FILES := $(shell [ -e public/coffee/ide ] && find public/coffee/ide -name '*.coffee')
IDE_JS_FILES := $(subst public/coffee/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_COFFEE_FILES))
IDE_JS_FILES := $(subst coffee,js,$(IDE_JS_FILES))
IDE_TEST_COFFEE_FILES := $(shell [ -e test/unit_frontend/coffee ] && find test/unit_frontend/coffee -name '*.coffee')
IDE_TEST_JS_FILES := $(subst test/unit_frontend/coffee/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_COFFEE_FILES))
IDE_TEST_JS_FILES := $(subst coffee,js,$(IDE_TEST_JS_FILES))
MAIN_COFFEE_FILES := $(shell [ -e public/coffee/main ] && find public/coffee/main -name '*.coffee')
MAIN_JS_FILES := $(subst public/coffee/main,../../public/js/main/$(MODULE_NAME),$(MAIN_COFFEE_FILES))
MAIN_JS_FILES := $(subst coffee,js,$(MAIN_JS_FILES))
@@ -26,6 +29,10 @@ test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
@mkdir -p $(dir $@)
$(COFFEE) --compile --print $< > $@
../../test/unit_frontend/js/ide/$(MODULE_NAME)/%.js: test/unit_frontend/coffee/ide/%.coffee
@mkdir -p $(dir $@)
$(COFFEE) --compile --print $< > $@
../../public/js/ide/$(MODULE_NAME)/%.js: public/coffee/ide/%.coffee
@mkdir -p $(dir $@)
$(COFFEE) --compile --print $< > $@
@@ -37,11 +44,11 @@ test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
index.js: index.coffee
$(COFFEE) --compile --print $< > $@
compile: $(APP_JS_FILES) $(IDE_JS_FILES) $(MAIN_JS_FILES) index.js
compile: $(APP_JS_FILES) $(IDE_JS_FILES) $(MAIN_JS_FILES) $(IDE_TEST_JS_FILES) index.js
@echo > /dev/null
compile_full:
$(COFFEE) -o app/js -c app/coffee
if [ -e app/coffee ]; then $(COFFEE) -o app/js -c app/coffee; fi
if [ -e test/unit/coffee ]; then $(COFFEE) -o test/unit/js -c test/unit/coffee; fi
if [ -e test/acceptance/coffee ]; then $(COFFEE) -o test/acceptance/js -c test/acceptance/coffee; fi
if [ -e public/coffee/ide/ ]; then $(COFFEE) -o ../../public/js/ide/$(MODULE_NAME) -c public/coffee/ide/; fi
@@ -17,17 +17,17 @@ makeFaultTolerantRequest = (userId, options, callback) ->
if settings.overleaf?
options.qs = Object.assign({}, options.qs, { fromV2: 1 })
makeRequest(options, callback)
makeRequest options, (err) ->
if err?
logger.err { err: err }, 'Request to analytics failed'
callback() # Do not wait for all the attempts
makeRequest = (opts, callback)->
if settings.apis?.analytics?.url?
urlPath = opts.url
opts.url = "#{settings.apis.analytics.url}#{urlPath}"
request opts, (err) ->
if err?
logger.err { err: err }, 'Request to analytics failed'
callback() # Do not wait for all the attempts
request opts, callback
else
callback(new Errors.ServiceNotConfiguredError('Analytics service not configured'))
@@ -18,15 +18,16 @@ module.exports = CompileManager =
timer.done()
_callback(args...)
@_checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)->
if !canCompile
return callback null, "autocompile-backoff", []
logger.log project_id: project_id, user_id: user_id, "compiling project"
CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) ->
return callback(error) if error?
if recentlyCompiled
logger.warn {project_id, user_id}, "project was recently compiled so not continuing"
return callback null, "too-recently-compiled", []
logger.log project_id: project_id, user_id: user_id, "compiling project"
CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) ->
return callback(error) if error?
if recentlyCompiled
logger.warn {project_id, user_id}, "project was recently compiled so not continuing"
return callback null, "too-recently-compiled", []
CompileManager._checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)->
if !canCompile
return callback null, "autocompile-backoff", []
CompileManager._ensureRootDocumentIsSet project_id, (error) ->
return callback(error) if error?
@@ -121,15 +121,15 @@ module.exports = DocumentUpdaterHandler =
method: "DELETE"
}, project_id, "delete-thread", callback
resyncProjectHistory: (project_id, docs, files, callback) ->
resyncProjectHistory: (project_id, projectHistoryId, docs, files, callback) ->
logger.info {project_id, docs, files}, "resyncing project history in doc updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/history/resync"
json: { docs, files }
json: { docs, files, projectHistoryId }
method: "POST"
}, project_id, "resync-project-history", callback
updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
updateProjectStructure: (project_id, projectHistoryId, userId, changes, callback = (error) ->)->
return callback() if !settings.apis.project_history?.sendProjectStructureOps
Project.findOne {_id: project_id}, {version:true}, (err, currentProject) ->
@@ -144,7 +144,13 @@ module.exports = DocumentUpdaterHandler =
logger.log {project_id}, "updating project structure in doc updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}"
json: { docUpdates, fileUpdates, userId, version: currentProject.version }
json: {
docUpdates,
fileUpdates,
userId,
version: currentProject.version
projectHistoryId
}
method: "POST"
}, project_id, "update-project-structure", callback
@@ -174,6 +180,23 @@ module.exports = DocumentUpdaterHandler =
oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString()
newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString()
# Send deletes before adds (and renames) to keep a 1:1 mapping between
# paths and ids
#
# When a file is replaced, we first delete the old file and then add the
# new file. If the 'add' operation is sent to project history before the
# 'delete' then we would have two files with the same path at that point
# in time.
for id, oldEntity of oldEntitiesHash
newEntity = newEntitiesHash[id]
if !newEntity?
# entity deleted
updates.push
id: id
pathname: oldEntity.path
newPathname: ''
for id, newEntity of newEntitiesHash
oldEntity = oldEntitiesHash[id]
@@ -191,16 +214,6 @@ 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"
@@ -1,3 +1,5 @@
ProjectGetter = require "../Project/ProjectGetter"
ProjectLocator = require "../Project/ProjectLocator"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
logger = require("logger-sharelatex")
@@ -8,21 +10,31 @@ module.exports =
doc_id = req.params.doc_id
plain = req?.query?.plain == 'true'
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
ProjectEntityHandler.getDoc project_id, doc_id, {pathname: true}, (error, lines, rev, version, ranges, pathname) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
if plain
res.type "text/plain"
res.send lines.join('\n')
else
res.type "json"
res.send JSON.stringify {
lines: lines
version: version
ranges: ranges
pathname: pathname
}
ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) ->
return next(error) if error?
return res.sendStatus(404) if !project?
ProjectLocator.findElement {project: project, element_id: doc_id, type: 'doc'}, (error, doc, path) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding doc contents for getDocument"
return next(error)
if plain
res.type "text/plain"
res.send lines.join('\n')
else
projectHistoryId = project?.overleaf?.history?.id
res.type "json"
res.send JSON.stringify {
lines: lines
version: version
ranges: ranges
pathname: path.fileSystem
projectHistoryId: projectHistoryId
}
setDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
@@ -41,11 +41,13 @@ module.exports = EditorController =
callback err, doc
upsertFile: (project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id, callback = (err, file) ->) ->
ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, file, didAddFile) ->
ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile) ->
return callback(err) if err?
if didAddFile
EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, file, source, linkedFileData
callback null, file
if not didAddFile # replacement, so remove the existing file from the client
EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source
# now add the new file on the client
EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, newFile, source, linkedFileData
callback null, newFile
upsertDocWithPath: (project_id, elementPath, docLines, source, user_id, callback) ->
ProjectEntityUpdateHandler.upsertDocWithPath project_id, elementPath, docLines, source, user_id, (err, doc, didAddNewDoc, newFolders, lastFolder) ->
@@ -57,12 +59,14 @@ module.exports = EditorController =
callback()
upsertFileWithPath: (project_id, elementPath, fsPath, linkedFileData, source, user_id, callback) ->
ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, linkedFileData, user_id, (err, file, didAddFile, newFolders, lastFolder) ->
ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile, newFolders, lastFolder) ->
return callback(err) if err?
EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) ->
return callback(err) if err?
if didAddFile
EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, file, source, linkedFileData
if not didAddFile # replacement, so remove the existing file from the client
EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source
# now add the new file on the client
EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, newFile, source, linkedFileData
callback()
addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)->
@@ -57,26 +57,9 @@ module.exports = EditorHttpController =
privilegeLevel
)
restoreDoc: (req, res, next) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
name = req.body.name
if !name?
return res.sendStatus 400 # Malformed request
logger.log project_id: project_id, doc_id: doc_id, "restoring doc"
ProjectEntityUpdateHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) =>
return next(error) if error?
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc)
res.json {
doc_id: doc._id
}
_nameIsAcceptableLength: (name)->
return name? and name.length < 150 and name.length != 0
addDoc: (req, res, next) ->
project_id = req.params.Project_id
name = req.body.name
@@ -14,8 +14,6 @@ module.exports =
webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc
webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder
webRouter.post '/project/:Project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, EditorHttpController.restoreDoc
# Called by the real-time API to load up the current project state.
# This is a post request because it's more than just a getting of data. We take actions
# whenever a user joins a project, like updating the deleted status.
@@ -6,6 +6,7 @@ Errors = require "../Errors/Errors"
HistoryManager = require "./HistoryManager"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
RestoreManager = require "./RestoreManager"
module.exports = HistoryController =
selectHistoryApi: (req, res, next = (error) ->) ->
@@ -71,3 +72,29 @@ module.exports = HistoryController =
return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError
return next(error) if error?
res.sendStatus 204
restoreFileFromV2: (req, res, next) ->
{project_id} = req.params
{version, pathname} = req.body
user_id = AuthenticationController.getLoggedInUserId req
logger.log {project_id, version, pathname}, "restoring file from v2"
RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) ->
return next(error) if error?
res.json {
type: entity.type,
id: entity._id
}
restoreDocFromDeletedDoc: (req, res, next) ->
{project_id, doc_id} = req.params
{name} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
if !name?
return res.sendStatus 400 # Malformed request
logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc"
RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) =>
return next(error) if error?
res.json {
doc_id: doc._id
}
@@ -0,0 +1,60 @@
Settings = require 'settings-sharelatex'
Path = require 'path'
FileWriter = require '../../infrastructure/FileWriter'
FileSystemImportManager = require '../Uploads/FileSystemImportManager'
ProjectEntityHandler = require '../Project/ProjectEntityHandler'
ProjectLocator = require '../Project/ProjectLocator'
EditorController = require '../Editor/EditorController'
Errors = require '../Errors/Errors'
moment = require 'moment'
module.exports = RestoreManager =
restoreDocFromDeletedDoc: (user_id, project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
# This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system.
# It looks up the deleted doc's contents, and then creates a new doc with the same content.
# We don't actually remove the deleted doc entry, just create a new one from its lines.
ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
return callback(error) if error?
addDocWithName = (name, callback) ->
EditorController.addDoc project_id, null, name, lines, 'restore', user_id, callback
RestoreManager._addEntityWithUniqueName addDocWithName, name, callback
restoreFileFromV2: (user_id, project_id, version, pathname, callback = (error, entity) ->) ->
RestoreManager._writeFileVersionToDisk project_id, version, pathname, (error, fsPath) ->
return callback(error) if error?
basename = Path.basename(pathname)
dirname = Path.dirname(pathname)
if dirname == '.' # no directory
dirname = ''
RestoreManager._findOrCreateFolder project_id, dirname, (error, parent_folder_id) ->
return callback(error) if error?
addEntityWithName = (name, callback) ->
FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, name, fsPath, false, callback
RestoreManager._addEntityWithUniqueName addEntityWithName, basename, callback
_findOrCreateFolder: (project_id, dirname, callback = (error, folder_id) ->) ->
EditorController.mkdirp project_id, dirname, (error, newFolders, lastFolder) ->
return callback(error) if error?
return callback(null, lastFolder?._id)
_addEntityWithUniqueName: (addEntityWithName, basename, callback = (error) ->) ->
addEntityWithName basename, (error, entity) ->
if error?
if error instanceof Errors.InvalidNameError
# likely a duplicate name, so try with a prefix
date = moment(new Date()).format('Do MMM YY H:mm:ss')
# Move extension to the end so the file type is preserved
extension = Path.extname(basename)
basename = Path.basename(basename, extension)
basename = "#{basename} (Restored on #{date})"
if extension != ''
basename = "#{basename}#{extension}"
addEntityWithName basename, callback
else
callback(error)
else
callback(null, entity)
_writeFileVersionToDisk: (project_id, version, pathname, callback = (error, fsPath) ->) ->
url = "#{Settings.apis.project_history.url}/project/#{project_id}/version/#{version}/#{encodeURIComponent(pathname)}"
FileWriter.writeUrlToDisk project_id, url, callback
@@ -157,7 +157,7 @@ module.exports = ProjectController =
hasSubscription: (cb)->
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
user: (cb) ->
User.findById user_id, "featureSwitches overleaf awareOfV2", cb
User.findById user_id, "featureSwitches overleaf awareOfV2 features", cb
}, (err, results)->
if err?
logger.err err:err, "error getting data for project list page"
@@ -172,6 +172,7 @@ module.exports = ProjectController =
user = results.user
warnings = ProjectController._buildWarningsList results.v1Projects
ProjectController._injectProjectOwners projects, (error, projects) ->
return next(error) if error?
viewModel = {
@@ -193,6 +194,14 @@ module.exports = ProjectController =
else
viewModel.showUserDetailsArea = false
paidUser = user.features?.github # use a heuristic for paid account
freeUserProportion = 0.10
sampleFreeUser = parseInt(user._id.toString().slice(-2), 16) < freeUserProportion * 255
showFrontWidget = paidUser or sampleFreeUser
logger.log {paidUser, sampleFreeUser, showFrontWidget}, 'deciding whether to show front widget'
if showFrontWidget
viewModel.frontChatWidgetRoomId = Settings.overleaf?.front_chat_widget_room_id
res.render 'project/list', viewModel
timer.done()
@@ -301,6 +310,7 @@ module.exports = ProjectController =
themes: THEME_LIST
maxDocLength: Settings.max_doc_length
useV2History: !!project.overleaf?.history?.display
showRichText: req.query?.rt == 'true'
timer.done()
_buildProjectList: (allProjects, v1Projects = [])->
@@ -35,10 +35,17 @@ module.exports = ProjectEditorHandler =
compileGroup:"standard"
templates: false
references: false
referencesSearch: false
mendeley: false
trackChanges: false
trackChangesVisible: ProjectEditorHandler.trackChangesAvailable
})
# Originally these two feature flags were both signalled by the now-deprecated `references` flag.
# For older users, the presence of the `references` feature flag should still turn on these features.
result.features.referencesSearch = result.features.referencesSearch or result.features.references
result.features.mendeley = result.features.mendeley or result.features.references
return result
buildOwnerAndMembersViews: (members) ->
@@ -6,7 +6,6 @@ DocstoreManager = require "../Docstore/DocstoreManager"
DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
Errors = require '../Errors/Errors'
Project = require('../../models/Project').Project
ProjectLocator = require('./ProjectLocator')
ProjectGetter = require "./ProjectGetter"
TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
@@ -105,14 +104,7 @@ module.exports = ProjectEntityHandler = self =
callback = options
options = {}
if options["pathname"]
delete options["pathname"]
ProjectLocator.findElement {project_id: project_id, element_id: doc_id, type: 'doc'}, (error, doc, path) =>
return callback(error) if error?
DocstoreManager.getDoc project_id, doc_id, options, (error, lines, rev, version, ranges) =>
callback(error, lines, rev, version, ranges, path.fileSystem)
else
DocstoreManager.getDoc project_id, doc_id, options, callback
DocstoreManager.getDoc project_id, doc_id, options, callback
_getAllFolders: (project_id, callback) ->
logger.log project_id:project_id, "getting all folders for project"
@@ -31,7 +31,7 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
LOCK_NAMESPACE: LOCK_NAMESPACE
addDoc: wrapWithLock (project_id, folder_id, doc, callback = (err, result) ->) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add doc"
return callback(err)
@@ -40,7 +40,7 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
self._putElement project, folder_id, doc, "doc", callback
addFile: wrapWithLock (project_id, folder_id, fileRef, callback = (error, result, project) ->)->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add file"
return callback(err)
@@ -48,28 +48,28 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
self._confirmFolder project, folder_id, (folder_id)->
self._putElement project, folder_id, fileRef, "file", callback
replaceFile: wrapWithLock (project_id, file_id, linkedFileData, callback) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder: true, name:true}, (err, project) ->
replaceFileWithNew: wrapWithLock (project_id, file_id, newFileRef, callback) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) ->
return callback(err) if err?
ProjectLocator.findElement {project:project, element_id: file_id, type: 'file'}, (err, fileRef, path)=>
return callback(err) if err?
conditions = _id:project._id
inc = {}
inc["#{path.mongo}.rev"] = 1
# currently we do not need to increment the project version number for changes that are replacements
# but when we make switch to having immutable files the replace operation will add a new file, and
# this will require a version increase. We will start incrementing the project version now as it does
# no harm and will help to test it.
inc['version'] = 1
set = {}
set["#{path.mongo}.created"] = new Date()
set["#{path.mongo}.linkedFileData"] = linkedFileData
update =
"$inc": inc
"$set": set
Project.update conditions, update, {}, (err) ->
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project_id, fileRef, (err) ->
return callback(err) if err?
callback null, fileRef, project, path
conditions = _id:project._id
inc = {}
# increment the project structure version as we are adding a new file here
inc['version'] = 1
set = {}
set["#{path.mongo}._id"] = newFileRef._id
set["#{path.mongo}.created"] = new Date()
set["#{path.mongo}.linkedFileData"] = newFileRef.linkedFileData
set["#{path.mongo}.rev"] = 1
update =
"$inc": inc
"$set": set
Project.update conditions, update, {}, (err) ->
return callback(err) if err?
callback null, fileRef, project, path
mkdirp: wrapWithLock (project_id, path, callback) ->
folders = path.split('/')
@@ -110,7 +110,7 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
callback null, folders, lastFolder
moveEntity: wrapWithLock (project_id, entity_id, destFolderId, entityType, callback = (error) ->) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) ->
return callback(err) if err?
ProjectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)->
return callback(err) if err?
@@ -127,10 +127,10 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
startPath = entityPath.fileSystem
endPath = result.path.fileSystem
changes = {oldDocs, newDocs, oldFiles, newFiles}
callback null, project.name, startPath, endPath, entity.rev, changes, callback
callback null, project, startPath, endPath, entity.rev, changes, callback
deleteEntity: wrapWithLock (project_id, entity_id, entityType, callback) ->
ProjectGetter.getProjectWithoutLock project_id, {name:true, rootFolder:true}, (error, project) ->
ProjectGetter.getProjectWithoutLock project_id, {name:true, rootFolder:true, overleaf:true}, (error, project) ->
return callback(error) if error?
ProjectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path) ->
return callback(error) if error?
@@ -139,7 +139,7 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
callback null, entity, path, project
renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, callback) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (error, project)=>
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (error, project)=>
return callback(error) if error?
ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) =>
return callback(error) if error?
@@ -161,10 +161,10 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
return callback(error) if error?
startPath = entPath.fileSystem
changes = {oldDocs, newDocs, oldFiles, newFiles}
callback null, project.name, startPath, endPath, entity.rev, changes, callback
callback null, project, startPath, endPath, entity.rev, changes, callback
addFolder: wrapWithLock (project_id, parentFolder_id, folderName, callback) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true}, (err, project) ->
ProjectGetter.getProjectWithoutLock project_id, {rootFolder:true, name:true, overleaf:true}, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add folder"
return callback(err)
@@ -300,3 +300,29 @@ module.exports = ProjectEntityMongoUpdateHandler = self =
if isNestedFolder
return callback(new Errors.InvalidNameError("destination folder is a child folder of me"))
callback()
_insertDeletedDocReference: (project_id, doc, callback = (error) ->) ->
Project.update {
_id: project_id
}, {
$push: {
deletedDocs: {
_id: doc._id
name: doc.name
}
}
}, {}, callback
_insertDeletedFileReference: (project_id, fileRef, callback = (error) ->) ->
Project.update {
_id: project_id
}, {
$push: {
deletedFiles: {
_id: fileRef._id
name: fileRef.name
linkedFileData: fileRef.linkedFileData
deletedAt: new Date()
}
}
}, {}, callback
@@ -24,17 +24,31 @@ wrapWithLock = (methodWithoutLock) ->
# This lock is used to make sure that the project structure updates are made
# sequentially. In particular the updates must be made in mongo and sent to
# the doc-updater in the same order.
methodWithLock = (project_id, args..., callback) ->
LockManager.runWithLock LOCK_NAMESPACE, project_id,
(cb) -> methodWithoutLock project_id, args..., cb
callback
methodWithLock.withoutLock = methodWithoutLock
methodWithLock
if typeof methodWithoutLock is 'function'
methodWithLock = (project_id, args..., callback) ->
LockManager.runWithLock LOCK_NAMESPACE, project_id,
(cb) -> methodWithoutLock project_id, args..., cb
callback
methodWithLock.withoutLock = methodWithoutLock
methodWithLock
else
# handle case with separate setup and locked stages
wrapWithSetup = methodWithoutLock.beforeLock # a function to set things up before the lock
mainTask = methodWithoutLock.withLock # function to execute inside the lock
methodWithLock = wrapWithSetup (project_id, args..., callback) ->
LockManager.runWithLock(LOCK_NAMESPACE, project_id, (cb) ->
mainTask(project_id, args..., cb)
callback)
methodWithLock.withoutLock = wrapWithSetup mainTask
methodWithLock.beforeLock = methodWithoutLock.beforeLock
methodWithLock.mainTask = methodWithoutLock.withLock
methodWithLock
module.exports = ProjectEntityUpdateHandler = self =
# this doesn't need any locking because it's only called by ProjectDuplicator
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
projectHistoryId = project.overleaf?.history?.id
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
return callback(err) if err?
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=>
@@ -59,7 +73,7 @@ module.exports = ProjectEntityUpdateHandler = self =
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
@@ -108,52 +122,87 @@ module.exports = ProjectEntityUpdateHandler = self =
logger.log project_id: project_id, "removing root doc"
Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
if not SafePath.isCleanFilename name
return callback new Errors.InvalidNameError("invalid element name")
# getDoc will return the deleted doc's lines, but we don't actually remove
# the deleted doc, just create a new one from its lines.
ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
return callback(error) if error?
self.addDoc project_id, null, name, lines, callback
addDoc: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
self.addDocWithoutUpdatingHistory.withoutLock project_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path, project) ->
return callback(error) if error?
projectHistoryId = project.overleaf?.history?.id
newDocs = [
doc: doc
path: path
docLines: docLines.join('\n')
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newDocs}, (error) ->
return callback(error) if error?
callback null, doc, folder_id
addFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id) ->)->
self.addFileWithoutUpdatingHistory.withoutLock project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, folder_id, path, fileStoreUrl) ->
return callback(error) if error?
newFiles = [
file: fileRef
path: path
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
_uploadFile: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, fileStoreUrl) ->)->
if not SafePath.isCleanFilename fileName
return callback new Errors.InvalidNameError("invalid element name")
fileRef = new File(
name: fileName
linkedFileData: linkedFileData
)
FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (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)
callback(null, fileRef, fileStoreUrl)
replaceFile: wrapWithLock (project_id, file_id, fsPath, linkedFileData, userId, callback)->
FileStoreHandler.uploadFileFromDisk project_id, file_id, fsPath, (err, fileStoreUrl)->
return callback(err) if err?
ProjectEntityMongoUpdateHandler.replaceFile project_id, file_id, linkedFileData, (err, fileRef, project, path) ->
_addFileAndSendToTpds: (project_id, folder_id, fileName, fileRef, callback = (error) ->)->
ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) ->
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, result, project)
addFile: wrapWithLock
beforeLock: (next) ->
(project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) ->
ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) ->
return callback(error) if error?
next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileName, fileRef, (err, result, project) ->
return callback(err) if err?
projectHistoryId = project.overleaf?.history?.id
newFiles = [
file: fileRef
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) ->
return callback(error) if error?
callback(null, fileRef, folder_id)
replaceFile: wrapWithLock
beforeLock: (next) ->
(project_id, file_id, fsPath, linkedFileData, userId, callback)->
# create a new file
fileRef = new File(
name: "dummy-upload-filename"
linkedFileData: linkedFileData
)
FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
return callback(err) if err?
next project_id, file_id, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback
withLock: (project_id, file_id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback)->
ProjectEntityMongoUpdateHandler.replaceFileWithNew project_id, file_id, newFileRef, (err, oldFileRef, project, path) ->
return callback(err) if err?
oldFiles = [
file: oldFileRef
path: path.fileSystem
]
newFiles = [
file: newFileRef
path: path.fileSystem
url: fileStoreUrl
]
TpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, (err) ->
projectHistoryId = project.overleaf?.history?.id
TpdsUpdateSender.addFile {project_id:project._id, file_id:newFileRef._id, path:path.fileSystem, rev:newFileRef.rev+1, project_name:project.name}, (err) ->
return callback(err) if err?
DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, callback
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {oldFiles, newFiles}, callback
addDocWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
# This method should never be called directly, except when importing a project
@@ -178,31 +227,21 @@ module.exports = ProjectEntityUpdateHandler = self =
rev: 0
}, (err) ->
return callback(err) if err?
callback(null, doc, folder_id, result?.path?.fileSystem)
callback(null, doc, folder_id, result?.path?.fileSystem, project)
addFileWithoutUpdatingHistory: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)->
addFileWithoutUpdatingHistory: wrapWithLock
# 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.
if not SafePath.isCleanFilename fileName
return callback new Errors.InvalidNameError("invalid element name")
fileRef = new File(
name: fileName
linkedFileData: linkedFileData
)
FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (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)
ProjectEntityMongoUpdateHandler.addFile project_id, folder_id, fileRef, (err, result, project) ->
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)
beforeLock: (next) ->
(project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback) ->
ProjectEntityUpdateHandler._uploadFile project_id, folder_id, fileName, fsPath, linkedFileData, userId, (error, fileRef, fileStoreUrl) ->
return callback(error) if error?
next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)->
ProjectEntityUpdateHandler._addFileAndSendToTpds project_id, folder_id, fileName, fileRef, (err, result, project) ->
return callback(err) if err?
callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
upsertDoc: wrapWithLock (project_id, folder_id, docName, docLines, source, userId, callback = (err, doc, folder_id, isNewDoc)->)->
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
@@ -224,23 +263,36 @@ module.exports = ProjectEntityUpdateHandler = self =
return callback(err) if err?
callback null, doc, !existingDoc?
upsertFile: wrapWithLock (project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback = (err, file, isNewFile)->)->
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
return callback(error) if error?
return callback(new Error("Couldn't find folder")) if !folder?
existingFile = null
for fileRef in folder.fileRefs
if fileRef.name == fileName
existingFile = fileRef
break
if existingFile?
self.replaceFile.withoutLock project_id, existingFile._id, fsPath, linkedFileData, userId, (err) ->
upsertFile: wrapWithLock
beforeLock: (next) ->
(project_id, folder_id, fileName, fsPath, linkedFileData, userId, callback)->
# create a new file
fileRef = new File(
name: fileName
linkedFileData: linkedFileData
)
FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
return callback(err) if err?
callback null, existingFile, !existingFile?
else
self.addFile.withoutLock project_id, folder_id, fileName, fsPath, linkedFileData, userId, (err, file) ->
return callback(err) if err?
callback null, file, !existingFile?
next(project_id, folder_id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback)
withLock: (project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, callback = (err, file, isNewFile, existingFile)->)->
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
return callback(error) if error?
return callback(new Error("Couldn't find folder")) if !folder?
existingFile = null
for fileRef in folder.fileRefs
if fileRef.name == fileName
existingFile = fileRef
break
if existingFile?
# this calls directly into the replaceFile main task (without the beforeLock part)
self.replaceFile.mainTask project_id, existingFile._id, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) ->
return callback(err) if err?
callback null, newFileRef, !existingFile?, existingFile
else
# this calls directly into the addFile main task (without the beforeLock part)
self.addFile.mainTask project_id, folder_id, fileName, fsPath, linkedFileData, userId, newFileRef, fileStoreUrl, (err) ->
return callback(err) if err?
callback null, newFileRef, !existingFile?, existingFile
upsertDocWithPath: wrapWithLock (project_id, elementPath, docLines, source, userId, callback) ->
docName = path.basename(elementPath)
@@ -251,14 +303,26 @@ module.exports = ProjectEntityUpdateHandler = self =
return callback(err) if err?
callback null, doc, isNewDoc, newFolders, folder
upsertFileWithPath: wrapWithLock (project_id, elementPath, fsPath, linkedFileData, userId, callback) ->
fileName = path.basename(elementPath)
folderPath = path.dirname(elementPath)
self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
return callback(err) if err?
self.upsertFile.withoutLock project_id, folder._id, fileName, fsPath, linkedFileData, userId, (err, file, isNewFile) ->
upsertFileWithPath: wrapWithLock
beforeLock: (next) ->
(project_id, elementPath, fsPath, linkedFileData, userId, callback)->
fileName = path.basename(elementPath)
folderPath = path.dirname(elementPath)
# create a new file
fileRef = new File(
name: fileName
linkedFileData: linkedFileData
)
FileStoreHandler.uploadFileFromDisk project_id, fileRef._id, fsPath, (err, fileStoreUrl)->
return callback(err) if err?
next project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback
withLock: (project_id, folderPath, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, callback) ->
self.mkdirp.withoutLock project_id, folderPath, (err, newFolders, folder) ->
return callback(err) if err?
callback null, file, isNewFile, newFolders, folder
# this calls directly into the upsertFile main task (without the beforeLock part)
self.upsertFile.mainTask project_id, folder._id, fileName, fsPath, linkedFileData, userId, fileRef, fileStoreUrl, (err, newFile, isNewFile, existingFile) ->
return callback(err) if err?
callback null, newFile, isNewFile, existingFile, newFolders, folder
deleteEntity: wrapWithLock (project_id, entity_id, entityType, userId, callback = (error) ->)->
logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity"
@@ -294,10 +358,11 @@ module.exports = ProjectEntityUpdateHandler = self =
logger.err {err: "No entityType set", project_id, entity_id}
return callback("No entityType set")
entityType = entityType.toLowerCase()
ProjectEntityMongoUpdateHandler.moveEntity project_id, entity_id, destFolderId, entityType, (err, project_name, startPath, endPath, rev, changes) ->
ProjectEntityMongoUpdateHandler.moveEntity project_id, entity_id, destFolderId, entityType, (err, project, startPath, endPath, rev, changes) ->
return callback(err) if err?
TpdsUpdateSender.moveEntity { project_id, project_name, startPath, endPath, rev }
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
projectHistoryId = project.overleaf?.history?.id
TpdsUpdateSender.moveEntity { project_id, project_name: project.name, startPath, endPath, rev }
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
renameEntity: wrapWithLock (project_id, entity_id, entityType, newName, userId, callback)->
if not SafePath.isCleanFilename newName
@@ -308,10 +373,11 @@ module.exports = ProjectEntityUpdateHandler = self =
return callback("No entityType set")
entityType = entityType.toLowerCase()
ProjectEntityMongoUpdateHandler.renameEntity project_id, entity_id, entityType, newName, (err, project_name, startPath, endPath, rev, changes) ->
ProjectEntityMongoUpdateHandler.renameEntity project_id, entity_id, entityType, newName, (err, project, startPath, endPath, rev, changes) ->
return callback(err) if err?
TpdsUpdateSender.moveEntity({project_id, startPath, endPath, project_name, rev})
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
projectHistoryId = project.overleaf?.history?.id
TpdsUpdateSender.moveEntity { project_id, project_name: project.name, startPath, endPath, rev }
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
# This doesn't directly update project structure but we need to take the lock
# to prevent anything else being queued before the resync update
@@ -319,7 +385,8 @@ module.exports = ProjectEntityUpdateHandler = self =
ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) ->
return callback(error) if error?
if !project?.overleaf?.history?.id?
projectHistoryId = project?.overleaf?.history?.id
if !projectHistoryId?
error = new Errors.ProjectHistoryDisabledError("project history not enabled for #{project_id}")
return callback(error)
@@ -335,7 +402,8 @@ module.exports = ProjectEntityUpdateHandler = self =
path: file.path
url: FileStoreHandler._buildUrl(project_id, file.file._id)
DocumentUpdaterHandler.resyncProjectHistory project_id, docs, files, callback
DocumentUpdaterHandler.resyncProjectHistory project_id, projectHistoryId, docs, files, callback
_cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) ->
if(entityType.indexOf("file") != -1)
self._cleanUpFile project, entity, path, userId, callback
@@ -357,22 +425,25 @@ module.exports = ProjectEntityUpdateHandler = self =
unsetRootDocIfRequired (error) ->
return callback(error) if error?
self._insertDeletedDocReference project._id, doc, (error) ->
ProjectEntityMongoUpdateHandler._insertDeletedDocReference project._id, doc, (error) ->
return callback(error) if error?
DocumentUpdaterHandler.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error?
DocstoreManager.deleteDoc project_id, doc_id, (error) ->
return callback(error) if error?
changes = oldDocs: [ {doc, path} ]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
projectHistoryId = project.overleaf?.history?.id
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, changes, callback
_cleanUpFile: (project, file, path, userId, callback = (error) ->) ->
project_id = project._id.toString()
file_id = file._id.toString()
FileStoreHandler.deleteFile project_id, file_id, (error) ->
ProjectEntityMongoUpdateHandler._insertDeletedFileReference project._id, file, (error) ->
return callback(error) if error?
project_id = project._id.toString()
projectHistoryId = project.overleaf?.history?.id
changes = oldFiles: [ {file, path} ]
DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback
# 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) ->) ->
jobs = []
@@ -392,15 +463,3 @@ module.exports = ProjectEntityUpdateHandler = self =
jobs.push (callback) -> self._cleanUpFolder project, childFolder, folderPath, userId, callback
async.series jobs, callback
_insertDeletedDocReference: (project_id, doc, callback = (error) ->) ->
Project.update {
_id: project_id
}, {
$push: {
deletedDocs: {
_id: doc._id
name: doc.name
}
}
}, {}, callback
@@ -15,6 +15,7 @@ htmlEncoder = new require("node-html-encoder").Encoder("numerical")
hashedFiles = {}
Path = require 'path'
Features = require "./Features"
Modules = require "./Modules"
jsPath =
if Settings.useMinifiedJs
@@ -41,10 +42,9 @@ pathList = [
"#{jsPath}ide.js"
"#{jsPath}main.js"
"#{jsPath}libraries.js"
"#{jsPath}es/richText.js"
"/stylesheets/style.css"
"/stylesheets/ol-style.css"
]
].concat(Modules.moduleAssetFiles(jsPath))
if !Settings.useMinifiedJs
logger.log "not using minified JS, not hashing static files"
@@ -150,6 +150,8 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
res.locals.buildWebpackPath = (jsFile, opts = {}) ->
if Settings.webpack? and !Settings.useMinifiedJs
path = Path.join(jsPath, jsFile)
if opts.removeExtension == true
path = path.slice(0,-3)
return "#{Settings.webpack.url}/public#{path}"
else
return res.locals.buildJsPath(jsFile, opts)
@@ -309,11 +311,11 @@ 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
defaultResizerSizeOpen : if isOl then 7 else 24
defaultResizerSizeClosed : if isOl then 7 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
chatResizerSizeOpen : if isOl then 7 else 12
chatResizerSizeClosed : 0
chatMessageBorderSaturation: if isOl then "85%" else "70%"
chatMessageBorderLightness : if isOl then "40%" else "70%"
@@ -14,7 +14,9 @@ module.exports = Features =
return Settings.enableGithubSync
when 'v1-return-message'
return Settings.accountMerge? and Settings.overleaf?
when 'rich-text'
return Settings.showRichText
when 'publish-modal'
return Settings.showPublishModal
when 'custom-togglers'
return Settings.overleaf?
else
throw new Error("unknown feature: #{feature}")
@@ -3,21 +3,40 @@ logger = require 'logger-sharelatex'
uuid = require 'uuid'
_ = require 'underscore'
Settings = require 'settings-sharelatex'
request = require 'request'
module.exports =
module.exports = FileWriter =
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
callback = _.once(callback)
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
writeStream = fs.createWriteStream(fsPath)
stream.pipe(writeStream)
stream.pause()
fs.mkdir Settings.path.dumpFolder, (error) ->
stream.resume()
if error? and error.code != 'EEXIST'
# Ignore error about already existing
return callback(error)
stream.on 'error', (err)->
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream"
callback(err)
writeStream.on 'error', (err)->
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk"
callback(err)
writeStream.on "finish", ->
logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
callback null, fsPath
writeStream = fs.createWriteStream(fsPath)
stream.pipe(writeStream)
stream.on 'error', (err)->
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with incoming stream"
callback(err)
writeStream.on 'error', (err)->
logger.err {err, identifier, fsPath}, "[writeStreamToDisk] something went wrong with writing to disk"
callback(err)
writeStream.on "finish", ->
logger.log {identifier, fsPath}, "[writeStreamToDisk] write stream finished"
callback null, fsPath
writeUrlToDisk: (identifier, url, callback = (error, fsPath) ->) ->
callback = _.once(callback)
stream = request.get(url)
stream.on 'response', (response) ->
if 200 <= response.statusCode < 300
FileWriter.writeStreamToDisk identifier, stream, callback
else
err = new Error("bad response from url: #{response.statusCode}")
logger.err {err, identifier, url}, err.message
callback(err)
@@ -22,6 +22,8 @@ module.exports =
}
docs: (docs) ->
if !docs?.map?
return
docs.map (doc) ->
{
path: doc.path
@@ -29,6 +31,8 @@ module.exports =
}
files: (files) ->
if !files?.map?
return
files.map (file) ->
{
path: file.path
@@ -43,6 +43,13 @@ module.exports = Modules =
moduleIncludesAvailable: (view) ->
return (Modules.viewIncludes[view] or []).length > 0
moduleAssetFiles: (pathPrefix) ->
assetFiles = []
for module in @modules
for assetFile in module.assetFiles or []
assetFiles.push "#{pathPrefix}#{assetFile}"
return assetFiles
attachHooks: () ->
for module in @modules
if module.hooks?
@@ -12,6 +12,10 @@ ObjectId = Schema.ObjectId
DeletedDocSchema = new Schema
name: String
DeletedFileSchema = new Schema
name: String
deletedAt: {type: Date}
ProjectSchema = new Schema
name : {type:String, default:'new project'}
lastUpdated : {type:Date, default: () -> new Date()}
@@ -30,6 +34,7 @@ ProjectSchema = new Schema
description : {type:String, default:''}
archived : { type: Boolean }
deletedDocs : [DeletedDocSchema]
deletedFiles : [DeletedFileSchema]
imageName : { type: String }
track_changes : { type: Object }
tokens :
+31 -29
View File
@@ -20,40 +20,42 @@ UserSchema = new Schema
loginCount : {type : Number, default: 0}
holdingAccount : {type : Boolean, default: false}
ace : {
mode : {type : String, default: 'none'}
theme : {type : String, default: 'textmate'}
fontSize : {type : Number, default:'12'}
autoComplete: {type : Boolean, default: true}
autoPairDelimiters: {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean}
}
mode : {type : String, default: 'none'}
theme : {type : String, default: 'textmate'}
fontSize : {type : Number, default:'12'}
autoComplete: {type : Boolean, default: true}
autoPairDelimiters: {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean}
}
features : {
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
versioning: { type:Boolean, default: Settings.defaultFeatures.versioning }
dropbox: { type:Boolean, default: Settings.defaultFeatures.dropbox }
github: { type:Boolean, default: Settings.defaultFeatures.github }
compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout }
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
references: { type:Boolean, default: Settings.defaultFeatures.references }
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
}
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
versioning: { type:Boolean, default: Settings.defaultFeatures.versioning }
dropbox: { type:Boolean, default: Settings.defaultFeatures.dropbox }
github: { type:Boolean, default: Settings.defaultFeatures.github }
compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout }
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
references: { type:Boolean, default: Settings.defaultFeatures.references }
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
mendeley: { type:Boolean, default: Settings.defaultFeatures.mendeley }
referencesSearch: { type:Boolean, default: Settings.defaultFeatures.referencesSearch }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]
refered_user_count: { type:Number, default: 0 }
subscription:
recurlyToken : String
freeTrialExpiresAt: Date
freeTrialDowngraded: Boolean
freeTrialPlanCode: String
# This is poorly named. It does not directly correspond
# to whether the user has has a free trial, but rather
# whether they should be allowed one in the future.
# For example, a user signing up directly for a paid plan
# has this set to true, despite never having had a free trial
hadFreeTrial: {type: Boolean, default: false}
recurlyToken : String
freeTrialExpiresAt: Date
freeTrialDowngraded: Boolean
freeTrialPlanCode: String
# This is poorly named. It does not directly correspond
# to whether the user has has a free trial, but rather
# whether they should be allowed one in the future.
# For example, a user signing up directly for a paid plan
# has this set to true, despite never having had a free trial
hadFreeTrial: {type: Boolean, default: false}
refProviders: {
mendeley: Boolean # coerce the refProviders values to Booleans
zotero: Boolean
+7 -3
View File
@@ -201,8 +201,11 @@ module.exports = class Router
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.proxyToHistoryApiAndInjectUserDetails
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@@ -261,9 +264,10 @@ module.exports = class Router
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
webRouter.get "/beta/participate", AuthenticationController.requireLogin(), BetaProgramController.optInPage
webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn
webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut
# disable beta program while v2 is in beta
# webRouter.get "/beta/participate", AuthenticationController.requireLogin(), BetaProgramController.optInPage
# webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn
# webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut
webRouter.get "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.sudoModePrompt
webRouter.post "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.submitPassword
+2 -17
View File
@@ -4,7 +4,7 @@ html.full-height(itemscope, itemtype='http://schema.org/Product')
title Something went wrong
link(rel="icon", href="/favicon.ico")
if buildCssPath
link(rel='stylesheet', href=buildCssPath('/style.css'))
link(rel="stylesheet", href=buildCssPath("/" + settings.brandPrefix + "style.css"))
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
body.full-height
.content.full-height
@@ -18,19 +18,4 @@ html.full-height(itemscope, itemtype='http://schema.org/Product')
.error-details
p.error-status Something went wrong, sorry.
p.error-description Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
a.error-btn(href="/") Home
//- .content
//- .container
//- .row
//- .col-md-8.col-md-offset-2.text-center
//- .page-header
//- h2 Oh dear, something went wrong.
//- if buildImgPath
//- p
//- img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
//- p
//- | Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
//- p
//- a(href="/")
//- i.fa.fa-arrow-circle-o-left
//- | Take me home
a.error-btn(href="/") Home
+2 -18
View File
@@ -21,7 +21,7 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="icon", href="/" + settings.brandPrefix + "favicon.ico")
link(rel="icon", sizes="192x192", href="/" + settings.brandPrefix + "touch-icon-192x192.png")
link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png")
link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529")
link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color=settings.brandPrefix === 'ol-' ? "#4f9c45" : "#a93529")
link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css", {hashedPath:true}))
@@ -48,7 +48,7 @@ html(itemscope, itemtype='http://schema.org/Product')
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '#{gaToken}', 'sharelatex.com');
ga('create', '#{gaToken}', '#{settings.cookieDomain.replace(/^\./, "")}');
ga('send', 'pageview');
- else
script(type='text/javascript').
@@ -111,22 +111,6 @@ html(itemscope, itemtype='http://schema.org/Product')
data-badge="inline"
)
- if(typeof(suppressSystemMessages) == "undefined")
.system-messages(
ng-cloak
ng-controller="SystemMessagesController"
)
.system-message(
ng-repeat="message in messages"
ng-controller="SystemMessageController"
ng-hide="hidden"
)
a(href, ng-click="hide()").pull-right &times;
.system-message-content(ng-bind-html="htmlContent")
include translations/translation_message
- if(typeof(suppressNavbar) == "undefined")
include layout/navbar
+8 -7
View File
@@ -55,13 +55,18 @@ block content
include ./editor/header
include ./editor/share
!= moduleIncludes("publish:body", locals)
#ide-body(
main#ide-body(
ng-cloak,
role="main",
layout="main",
ng-hide="state.loading",
resize-on="layout:chat:resize",
minimum-restore-size-west="130"
custom-toggler-pane=hasFeature('custom-togglers') ? "'west'" : "false"
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? "'" + translate("tooltip_hide_filetree") + "'" : "false"
custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? "'" + translate("tooltip_show_filetree") + "'" : "false"
)
.ui-layout-west
include ./editor/file-tree
@@ -133,6 +138,8 @@ block requirejs
"fineuploader": "libs/#{lib('fineuploader')}",
"ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
"libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}",
!{moduleIncludes("editor:script", locals)}
!{moduleIncludes("publish:script", locals)}
},
"waitSeconds": 0,
"shim": {
@@ -159,12 +166,6 @@ block requirejs
window.pdfCMapsPath = "#{pdfCMapsPath}"
window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
if hasFeature('rich-text')
script(
src=buildWebpackPath('es/richText.js', {hashedPath:settings.useMinifiedJs})
type="text/javascript"
)
script(
data-main=buildJsPath("ide.js", {hashedPath:false}),
baseurl=fullJsPath,
@@ -35,7 +35,7 @@ div.binary-file.full-size(
p.no-preview(
ng-if="failedLoad || textPreview.error || isUnpreviewableFile()"
) #{translate("no_preview_available")} {{ failedLoad }} {{ textPreview.error }} {{ isUnpreviewableFile() }}
) #{translate("no_preview_available")}
div.binary-file-footer
div(ng-if="openFile.linkedFileData.provider == 'url'")
@@ -8,6 +8,9 @@ div.full-size(
initial-size-east="'50%'"
minimum-restore-size-east="300"
allow-overflow-on="'center'"
custom-toggler-pane=hasFeature('custom-togglers') ? "'east'" : "false"
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? "'" + translate("tooltip_hide_pdf") + "'" : "false"
custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? "'" + translate("tooltip_show_pdf") + "'" : "false"
)
.ui-layout-center(
ng-controller="ReviewPanelController",
@@ -30,14 +33,13 @@ div.full-size(
i.fa.fa-arrow-left
| &nbsp;&nbsp;#{translate("open_a_file_on_the_left")}
if hasFeature('rich-text')
.toolbar.toolbar-editor(ng-controller="EditorToolbarController")
button(ng-click="toggleRichText()") Rich Text
!= moduleIncludes('editor:toolbar', locals)
#editor(
ace-editor="editor",
ng-if="!editor.richText",
ng-show="!!editor.sharejs_doc && !editor.opening",
style=showRichText ? "top: 32px" : "",
theme="settings.theme",
keybindings="settings.mode",
font-size="settings.fontSize",
@@ -68,13 +70,7 @@ div.full-size(
renderer-data="reviewPanel.rendererData"
)
if hasFeature('rich-text')
#editor-rich-text(
cm-editor,
ng-if="editor.richText"
ng-show="!!editor.sharejs_doc && !editor.opening"
sharejs-doc="editor.sharejs_doc"
)
!= moduleIncludes('editor:body', locals)
include ./review-panel
@@ -100,7 +96,6 @@ div.full-size(
ng-click="syncToCode()"
)
i.synctex-control-icon
div.full-size(
ng-if="ui.pdfLayout == 'flat'"
ng-show="ui.view == 'pdf'"
@@ -107,6 +107,9 @@ header.toolbar.toolbar-header.toolbar-with-labels(
)
i.fa.fa-fw.fa-group
p.toolbar-label #{translate("share")}
!= moduleIncludes('publish:button', locals)
a.btn.btn-full-height(
href,
ng-click="toggleHistory();",
@@ -11,7 +11,7 @@ aside.file-tree.file-tree-history(ng-controller="FileTreeController", ng-class="
.entity
.entity-name.entity-name-history(
ng-click="history.selection.pathname = pathname",
ng-class="{ 'deleted': doc.deleted }"
ng-class="{ 'deleted': !!doc.deletedAtV }"
)
i.fa.fa-fw.fa-pencil
span {{ pathname }}
+34 -141
View File
@@ -1,80 +1,39 @@
div#history(ng-show="ui.view == 'history'")
span(ng-controller="HistoryPremiumPopup")
span
.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
div(ng-if="project.owner._id == user.id")
div(sixpack-switch="teaser-history")
.message(sixpack-default)
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
.message(ng-if="project.owner._id == user.id")
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
sixpack-convert="teaser-history"
) #{translate("start_free_trial")}
.message.message-wider(sixpack-when="focused")
header.message-header
h3 History
.message-body
h4.teaser-title See who changed what. Go back to previous versions.
img.teaser-img(
src="/img/teasers/history/teaser-history.png"
alt="History"
)
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
.row
.col-md-8.col-md-offset-2
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| Catch up with your collaborators changes
li
i.fa.fa-check &nbsp;
| See changes over any time period
li
i.fa.fa-check &nbsp;
| Revert your documents to previous versions
li
i.fa.fa-check &nbsp;
| Restore deleted files
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
sixpack-convert="teaser-history"
) Try it for free
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
) #{translate("start_free_trial")}
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
@@ -163,74 +122,8 @@ div#history(ng-show="ui.view == 'history'")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
.diff-panel.full-size(ng-controller="HistoryDiffController")
.diff(
ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary"
)
.toolbar.toolbar-alt
span.name
| <strong>{{history.diff.highlights.length}} </strong>
ng-pluralize(
count="history.diff.highlights.length",
when="{\
'one': 'change',\
'other': 'changes'\
}"
)
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right
a.btn.btn-danger.btn-sm(
href,
ng-if="!history.isV2"
ng-click="openRestoreDiffModal()"
) #{translate("restore_to_before_these_changes")}
.deleted-warning(
ng-show="history.selection.docs[history.selection.pathname].deleted"
) This file was deleted
.diff-editor.hide-ace-cursor(
ace-editor="history",
theme="settings.theme",
font-size="settings.fontSize",
text="history.diff.text",
highlights="history.diff.highlights",
read-only="true",
resize-on="layout:main:resize",
navigate-highlights="true"
)
.diff.diff-binary(ng-show="history.diff.binary")
.toolbar.toolbar-alt
span.name
strong {{history.diff.pathname}}
.alert.alert-info We're still working on showing image and binary changes, sorry. Stay tuned!
.diff-deleted.text-centered(
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
p
a.btn.btn-primary.btn-lg(
href,
ng-click="restoreDeletedDoc()",
ng-disabled="history.diff.restoreInProgress"
) #{translate("restore")}
.diff-deleted.text-centered(
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
p.text-serif #{translate("file_restored_back_to_editor")}
p
a.btn.btn-default(
href,
ng-click="backToEditorAfterRestore()",
) #{translate("file_restored_back_to_editor_btn")}
.loading-panel(ng-show="history.diff.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="history.diff.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}
include ./history/diffPanelV1
include ./history/diffPanelV2
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
.modal-header
@@ -0,0 +1,58 @@
.diff-panel.full-size(ng-if="!history.isV2", ng-controller="HistoryDiffController")
.diff(
ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary"
)
.toolbar.toolbar-alt
span.name
| <strong>{{history.diff.highlights.length}} </strong>
ng-pluralize(
count="history.diff.highlights.length",
when="{\
'one': 'change',\
'other': 'changes'\
}"
)
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"
) #{translate("restore_to_before_these_changes")}
.diff-editor.hide-ace-cursor(
ace-editor="history",
theme="settings.theme",
font-size="settings.fontSize",
text="history.diff.text",
highlights="history.diff.highlights",
read-only="true",
resize-on="layout:main:resize",
navigate-highlights="true"
)
.diff-deleted.text-centered(
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
p
a.btn.btn-primary.btn-lg(
href,
ng-click="restoreDeletedDoc()",
ng-disabled="history.diff.restoreInProgress"
) #{translate("restore")}
.diff-deleted.text-centered(
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
p.text-serif #{translate("file_restored_back_to_editor")}
p
a.btn.btn-default(
href,
ng-click="backToEditorAfterRestore()",
) #{translate("file_restored_back_to_editor_btn")}
.loading-panel(ng-show="history.diff.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="history.diff.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}
@@ -0,0 +1,50 @@
.diff-panel.full-size(ng-if="history.isV2", ng-controller="HistoryV2DiffController")
.diff(
ng-if="!!history.diff && !history.diff.loading && !history.diff.error",
ng-class="{ 'diff-binary': history.diff.binary }"
)
.toolbar.toolbar-alt
span.name(ng-if="history.diff.binary")
strong {{history.diff.pathname}}
span.name(ng-if="!history.diff.binary")
| <strong>{{history.diff.highlights.length}} </strong>
ng-pluralize(
count="history.diff.highlights.length",
when="{\
'one': 'change',\
'other': 'changes'\
}"
)
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right(ng-if="history.selection.docs[history.selection.pathname].deletedAtV")
button.btn.btn-danger.btn-sm(
ng-click="restoreDeletedFile()"
ng-show="!restoreState.error"
ng-disabled="restoreState.inflight"
)
i.fa.fa-fw.fa-step-backward
span(ng-show="!restoreState.inflight")
| Restore this deleted file
span(ng-show="restoreState.inflight")
| Restoring...
span.text-danger(ng-show="restoreState.error")
| Error restoring, sorry
.diff-editor.hide-ace-cursor(
ng-if="!history.diff.binary"
ace-editor="history",
theme="settings.theme",
font-size="settings.fontSize",
text="history.diff.text",
highlights="history.diff.highlights",
read-only="true",
resize-on="layout:main:resize",
navigate-highlights="true"
)
.alert.alert-info(ng-if="history.diff.binary")
| We're still working on showing image and binary changes, sorry. Stay tuned!
.loading-panel(ng-show="history.diff.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="history.diff.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}
@@ -402,7 +402,6 @@ div.full-size.pdf(ng-controller="PdfController")
a.btn.btn-success.row-spaced-small(
href
ng-class="buttonClass"
sixpack-convert="track_changes_feature_info"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
@@ -173,7 +173,6 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
a.btn.btn-success(
href
ng-class="buttonClass"
sixpack-convert="track_changes_feature_info"
ng-click="startFreeTrial('projectMembers')"
) #{translate("start_free_trial")}
+22 -2
View File
@@ -56,9 +56,28 @@ block content
href
ng-click="showAll();"
) Show all
.content.content-alt.project-list-page(ng-controller="ProjectPageController")
.project-list-content
main.content.content-alt.project-list-page(
ng-controller="ProjectPageController"
role="main"
)
- if(typeof(suppressSystemMessages) == "undefined")
.system-messages(
ng-cloak
ng-controller="SystemMessagesController"
)
.system-message(
ng-repeat="message in messages"
ng-controller="SystemMessageController"
ng-hide="hidden"
)
button(ng-click="hide()").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.system-message-content(ng-bind-html="htmlContent")
include ../translations/translation_message
.project-list-content(event-tracking=settings.overleaf ? "loads_v2_dash" : "", onboard=settings.overleaf ? "true" : "", event-tracking-trigger=settings.overleaf ? "load" : "", event-tracking-mb="true", event-segmentation="{location: 'dash', v2_onboard: true}")
.row.project-list-row(ng-cloak)
.project-list-container(ng-if="projects.length > 0")
.project-list-sidebar-wrapper.col-md-2.col-xs-3
@@ -77,6 +96,7 @@ block content
if userIsFromOLv1(user)
div(ng-show="visible")
| To tag or rename your v1 projects, please go back to Overleaf v1.
div(ng-show="visible")
a.project-list-sidebar-v1-link(
href=settings.overleaf.host + "/dash?prefer-v1-dash=1"
) Go back to v1
@@ -1,4 +1,4 @@
- if (settings.overleaf && settings.overleaf.front_chat_widget_room_id != null)
- if (frontChatWidgetRoomId)
script.
window.FCSP = '#{settings.overleaf.front_chat_widget_room_id}';
script(src="https://chat-assets.frontapp.com/v1/chat.bundle.js")
window.FCSP = '#{frontChatWidgetRoomId}';
script(src="https://chat-assets.frontapp.com/v1/chat.bundle.js")
+4 -1
View File
@@ -38,4 +38,7 @@
tooltip-append-to-body="true"
)
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}
if settings.overleaf
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
else
span.last-modified {{project.lastUpdated | formatDate}}
+11 -8
View File
@@ -342,17 +342,20 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
.v1-import-step-2(ng-show="step === 2")
.v1-import-row
.v1-import-warning.v1-import-col(aria-label="Warning symbol.")
i.fa.fa-exclamation-triangle
i.fa.fa-flask
.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:
p We are still working hard to bring some Overleaf v1 features to the v2 editor. In v2 there is:
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
li <strong>No Journals and Services</strong> menu to submit directly to our partners yet
li <strong>No Rich Text (WYSIWYG)</strong> mode yet
li <strong>No linked files</strong> (to URLs or to files in other Overleaf projects) yet
li <strong>No Zotero and CiteULike</strong> integrations yet
li <strong>No labelled versions</strong> yet
p.row-spaced-small
| If you currently use the <strong>Overleaf Git bridge</strong> with your v1 project, you can migrate your project to the Overleaf v2 GitHub integration.
|
a(href='https://www.overleaf.com/help/343-working-offline-in-overleaf-v2', target='_blank') Read More.
.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.
@@ -6,13 +6,13 @@ if (user.awareOfV2 && !settings.overleaf)
.col-xs-12
.alert.alert-info
.notification_inner
.notification_body
a.btn.btn-info.btn-sm.pull-right(href="/user/login_to_ol_v2") Try Overleaf v2
| ShareLaTeX is joining Overleaf and will become <em>Overleaf v2</em> in late 2018 (<a href="https://www.overleaf.com/help/342-overleaf-v2-faq">read more</a>).
.notification_body(event-tracking="sees_v2_banner" event-tracking-mb="true" event-segmentation="{location: 'welcome', v2_onboard: true}" event-tracking-trigger="load" event-tracking-send-once="true")
a.btn.btn-info.btn-sm.pull-right(event-tracking="go_to_v2" event-tracking-mb="true" event-segmentation="{location: 'welcome', v2_onboard: true}" event-tracking-trigger="click" href="/user/login_to_ol_v2") Try Overleaf v2
| ShareLaTeX is joining Overleaf and will become <em>Overleaf v2</em> in late 2018 (<a event-tracking="click_v2_read_more" event-tracking-mb="true" event-segmentation="{location: 'welcome', v2_onboard: true}" event-tracking-trigger="click" href="https://www.overleaf.com/help/342-overleaf-v2-faq">read more</a>).
<br/>
| Were beta testing Overleaf v2 now and you can try it out with your ShareLaTeX account.
.notification_close
button(ng-click="dismiss()").close.pull-right
button(ng-click="dismiss()" event-tracking="closes_v2_banner" event-tracking-mb="true" event-segmentation="{location: 'welcome', v2_onboard: true}" event-tracking-trigger="click").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
@@ -49,4 +49,4 @@ span(ng-controller="NotificationsController").userNotifications
span().notification_close
button(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
span.sr-only #{translate("close")}
@@ -61,6 +61,7 @@
li(
ng-repeat="tag in tags | orderBy:'name'",
ng-controller="TagDropdownItemController"
ng-if="!tag.isV1"
)
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
i.fa(
@@ -10,19 +10,16 @@
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openCreateProjectModal()"
) #{translate("blank_project")}
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openCreateProjectModal('example')"
) #{translate("example_project")}
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openUploadProjectModal()"
) #{translate("upload_project")}
!= moduleIncludes("newProjectMenu", locals)
@@ -31,7 +28,7 @@
li.dropdown-header #{translate("templates")}
each item in templates
li
a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)}
a.menu-indent(href=item.url) #{translate(item.name)}
.row-spaced(ng-if="projects.length > 0", ng-cloak)
ul.list-unstyled.folders-menu(
@@ -76,7 +73,7 @@
tooltip-template="'v1TagTooltipTemplate'"
tooltip-append-to-body="true"
)
span.dropdown.tag-menu(dropdown)
span.dropdown.tag-menu(dropdown)(ng-if="!tag.isV1")
a.dropdown-toggle(
href="#",
data-toggle="dropdown",
@@ -133,7 +130,7 @@
hr
p.small #{translate("on_free_sl")}
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
a(href="/user/subscription/plans").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
@@ -22,4 +22,4 @@
span.owner {{ownerName()}}
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
@@ -59,24 +59,29 @@ block content
.col-md-12.text-centered
small #{translate("no_members")}
hr
div(ng-if="users.length < groupSize", ng-cloak)
hr
p
.small #{translate("add_more_members")}
form.form
.row
.col-xs-6
input.form-control(
name="email",
type="text",
placeholder="jane@example.com, joe@example.com",
ng-model="inputs.emails",
on-enter="addMembers()"
)
.col-xs-4
button.btn.btn-primary(ng-click="addMembers()") #{translate("add")}
.col-xs-2
a(href="/subscription/group/export") Export CSV
p.small #{translate("add_more_members")}
form.form
.row
.col-xs-6
input.form-control(
name="email",
type="text",
placeholder="jane@example.com, joe@example.com",
ng-model="inputs.emails",
on-enter="addMembers()"
)
.col-xs-4
button.btn.btn-primary(ng-click="addMembers()") #{translate("add")}
.col-xs-2
a(href="/subscription/group/export") Export CSV
div(ng-if="users.length >= groupSize && users.length > 0", ng-cloak)
.row
.col-xs-2.col-xs-offset-10
a(href="/subscription/group/export") Export CSV
script(type="text/javascript").
window.users = !{JSON.stringify(users)};
@@ -84,5 +89,3 @@ block content
+42 -301
View File
@@ -1,8 +1,6 @@
extends ../layout
block scripts
script(src="https://js.recurly.com/v3/recurly.js")
script(type='text/javascript').
window.countryCode = '#{countryCode}'
window.plan_code = '#{plan_code}'
@@ -41,19 +39,20 @@ block content
span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})}
span(ng-if="discountMonths && discountRate") &nbsp; - {{discountMonths}} #{translate("month")}s {{discountRate}}% Off
div(ng-if="price")
strong {{price.currency.symbol}}{{price.next.total}}
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
div(ng-if="normalPrice")
span.small Normally {{price.currency.symbol}}{{normalPrice}}
span.small Normally {{plans[currencyCode]['symbol']}}{{normalPrice}}
.row
div()
.col-md-12()
form(
ng-if="planName"
name="simpleCCForm"
novalidate
)
div.payment-method-toggle
a.payment-method-toggle-switch(
href
@@ -75,10 +74,10 @@ block content
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
div(ng-if="paymentMethod.value === 'credit_card'")
div(ng-show="paymentMethod.value === 'credit_card'")
.row
.col-xs-6
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-external-error' : ''")
label(for="first-name") #{translate('first_name')}
input#first-name.form-control(
type="text"
@@ -90,7 +89,7 @@ block content
)
span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }}
.col-xs-6
.form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
.form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-external-error' : ''")
label(for="last-name") #{translate('last_name')}
input#last-name.form-control(
type="text"
@@ -100,47 +99,41 @@ block content
ng-model="data.last_name"
required
)
span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }}
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''")
.form-group(ng-class="validation.errorFields.number ? 'has-external-error' : ''")
label(for="card-no") #{translate("credit_card_number")}
input#card-no.form-control(
div#card-no(
type="text"
ng-model="data.number"
name="ccNumber"
ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;"
ng-blur="validateCardNumber();"
required
cc-format-card-number
data-recurly='number'
)
span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
.row
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''")
label #{translate("expiry")}
input.form-control(
type="text"
ng-model="data.mmYY"
name="expiry"
placeholder="MM / YY"
ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;"
ng-blur="updateExpiry(); validateExpiry()"
required
cc-format-expiry
.col-xs-3
.form-group.has-feedback(ng-class="validation.errorFields.month ? 'has-external-error' : ''")
label(for="month").capitalised #{translate("month")}
div(
type="number"
name="month"
data-recurly="month"
)
span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
.col-xs-3
.form-group.has-feedback(ng-class="validation.errorFields.year ? 'has-external-error' : ''")
label(for="year").capitalised #{translate("year")}
div(
type="number"
name="year"
data-recurly="year"
)
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''")
.form-group.has-feedback(ng-class="validation.errorFields.cvv ? 'has-external-error' : ''")
label #{translate("security_code")}
input.form-control(
type="text"
div(
type="number"
ng-model="data.cvv"
ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;"
ng-blur="validateCvv()"
data-recurly="cvv"
name="cvv"
required
cc-format-sec-code
)
.form-control-feedback
@@ -151,20 +144,21 @@ block content
tooltip-trigger="mouseenter"
tooltip-append-to-body="true"
) ?
span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
div
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-external-error' : ''")
label(for="country") #{translate('country')}
select#country.form-control(
data-recurly="country"
ng-model="data.country"
name="country"
ng-change="updateCountry()"
required
required,
ng-options="country.code as country.name for country in countries",
ng-selected="{{country.code == data.country}}"
)
+countries_options()
option(value='', disabled, selected) #{translate("country")}
option(value='-') --------------
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
if (showVatField)
@@ -189,8 +183,8 @@ block content
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
span Total:
strong {{price.currency.symbol}}{{price.next.total}}
span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
span ({{plans[currencyCode]['symbol']}}{{price.next.subtotal}} + {{plans[currencyCode]['symbol']}}{{price.next.tax}} tax)
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
hr.thin
@@ -198,12 +192,14 @@ block content
div.payment-submit
button.btn.btn-success.btn-block(
ng-click="submit()"
ng-disabled="processing || !isFormValid(simpleCCForm);"
ng-disabled="processing || !isFormValid(simpleCCForm);"
)
span(ng-show="processing")
i.fa.fa-spinner.fa-spin
| &nbsp;
| {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
span(ng-if="paymentMethod.value === 'credit_card'")
| {{ monthlyBilling ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_now")}'}}
span(ng-if="paymentMethod.value !== 'credit_card'") #{translate("upgrade_paypal_btn")}
.col-md-3.col-md-pull-4
@@ -258,258 +254,3 @@ block content
)
p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card.
p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card.
mixin countries_options()
option(value='', disabled, selected) #{translate("country")}
option(value='-') --------------
option(value='AF') Afghanistan
option(value='AL') Albania
option(value='DZ') Algeria
option(value='AS') American Samoa
option(value='AD') Andorra
option(value='AO') Angola
option(value='AI') Anguilla
option(value='AQ') Antarctica
option(value='AG') Antigua and Barbuda
option(value='AR') Argentina
option(value='AM') Armenia
option(value='AW') Aruba
option(value='AC') Ascension Island
option(value='AU') Australia
option(value='AT') Austria
option(value='AZ') Azerbaijan
option(value='BS') Bahamas
option(value='BH') Bahrain
option(value='BD') Bangladesh
option(value='BB') Barbados
option(value='BE') Belgium
option(value='BZ') Belize
option(value='BJ') Benin
option(value='BM') Bermuda
option(value='BT') Bhutan
option(value='BO') Bolivia
option(value='BA') Bosnia and Herzegovina
option(value='BW') Botswana
option(value='BV') Bouvet Island
option(value='BR') Brazil
option(value='BQ') British Antarctic Territory
option(value='IO') British Indian Ocean Territory
option(value='VG') British Virgin Islands
option(value='BN') Brunei
option(value='BG') Bulgaria
option(value='BF') Burkina Faso
option(value='BI') Burundi
option(value='KH') Cambodia
option(value='CM') Cameroon
option(value='CA') Canada
option(value='IC') Canary Islands
option(value='CT') Canton and Enderbury Islands
option(value='CV') Cape Verde
option(value='KY') Cayman Islands
option(value='CF') Central African Republic
option(value='EA') Ceuta and Melilla
option(value='TD') Chad
option(value='CL') Chile
option(value='CN') China
option(value='CX') Christmas Island
option(value='CP') Clipperton Island
option(value='CC') Cocos [Keeling] Islands
option(value='CO') Colombia
option(value='KM') Comoros
option(value='CD') Congo [DRC]
option(value='CK') Cook Islands
option(value='CR') Costa Rica
option(value='HR') Croatia
option(value='CU') Cuba
option(value='CY') Cyprus
option(value='CZ') Czech Republic
option(value='DK') Denmark
option(value='DG') Diego Garcia
option(value='DJ') Djibouti
option(value='DM') Dominica
option(value='DO') Dominican Republic
option(value='NQ') Dronning Maud Land
option(value='TL') East Timor
option(value='EC') Ecuador
option(value='EG') Egypt
option(value='SV') El Salvador
option(value='EE') Estonia
option(value='ET') Ethiopia
option(value='FK') Falkland Islands [Islas Malvinas]
option(value='FO') Faroe Islands
option(value='FJ') Fiji
option(value='FI') Finland
option(value='FR') France
option(value='GF') French Guiana
option(value='PF') French Polynesia
option(value='TF') French Southern Territories
option(value='FQ') French Southern and Antarctic Territories
option(value='GA') Gabon
option(value='GM') Gambia
option(value='GE') Georgia
option(value='DE') Germany
option(value='GH') Ghana
option(value='GI') Gibraltar
option(value='GR') Greece
option(value='GL') Greenland
option(value='GD') Grenada
option(value='GP') Guadeloupe
option(value='GU') Guam
option(value='GT') Guatemala
option(value='GG') Guernsey
option(value='GW') Guinea-Bissau
option(value='GY') Guyana
option(value='HT') Haiti
option(value='HM') Heard Island and McDonald Islands
option(value='HN') Honduras
option(value='HK') Hong Kong
option(value='HU') Hungary
option(value='IS') Iceland
option(value='IN') India
option(value='ID') Indonesia
option(value='IE') Ireland
option(value='IM') Isle of Man
option(value='IL') Israel
option(value='IT') Italy
option(value='JM') Jamaica
option(value='JP') Japan
option(value='JE') Jersey
option(value='JT') Johnston Island
option(value='JO') Jordan
option(value='KZ') Kazakhstan
option(value='KE') Kenya
option(value='KI') Kiribati
option(value='KW') Kuwait
option(value='KG') Kyrgyzstan
option(value='LA') Laos
option(value='LV') Latvia
option(value='LS') Lesotho
option(value='LY') Libya
option(value='LI') Liechtenstein
option(value='LT') Lithuania
option(value='LU') Luxembourg
option(value='MO') Macau
option(value='MK') Macedonia [FYROM]
option(value='MG') Madagascar
option(value='MW') Malawi
option(value='MY') Malaysia
option(value='MV') Maldives
option(value='ML') Mali
option(value='MT') Malta
option(value='MH') Marshall Islands
option(value='MQ') Martinique
option(value='MR') Mauritania
option(value='MU') Mauritius
option(value='YT') Mayotte
option(value='FX') Metropolitan France
option(value='MX') Mexico
option(value='FM') Micronesia
option(value='MI') Midway Islands
option(value='MD') Moldova
option(value='MC') Monaco
option(value='MN') Mongolia
option(value='ME') Montenegro
option(value='MS') Montserrat
option(value='MA') Morocco
option(value='MZ') Mozambique
option(value='NA') Namibia
option(value='NR') Nauru
option(value='NP') Nepal
option(value='NL') Netherlands
option(value='AN') Netherlands Antilles
option(value='NT') Neutral Zone
option(value='NC') New Caledonia
option(value='NZ') New Zealand
option(value='NI') Nicaragua
option(value='NE') Niger
option(value='NG') Nigeria
option(value='NU') Niue
option(value='NF') Norfolk Island
option(value='VD') North Vietnam
option(value='MP') Northern Mariana Islands
option(value='NO') Norway
option(value='OM') Oman
option(value='QO') Outlying Oceania
option(value='PC') Pacific Islands Trust Territory
option(value='PK') Pakistan
option(value='PW') Palau
option(value='PS') Palestinian Territories
option(value='PA') Panama
option(value='PZ') Panama Canal Zone
option(value='PY') Paraguay
option(value='YD') People&apos;s Democratic Republic of Yemen
option(value='PE') Peru
option(value='PH') Philippines
option(value='PN') Pitcairn Islands
option(value='PL') Poland
option(value='PT') Portugal
option(value='PR') Puerto Rico
option(value='QA') Qatar
option(value='RO') Romania
option(value='RU') Russia
option(value='RW') Rwanda
option(value='RE') R&eacute;union
option(value='BL') Saint Barth&eacute;lemy
option(value='SH') Saint Helena
option(value='KN') Saint Kitts and Nevis
option(value='LC') Saint Lucia
option(value='MF') Saint Martin
option(value='PM') Saint Pierre and Miquelon
option(value='VC') Saint Vincent and the Grenadines
option(value='WS') Samoa
option(value='SM') San Marino
option(value='SA') Saudi Arabia
option(value='SN') Senegal
option(value='RS') Serbia
option(value='CS') Serbia and Montenegro
option(value='SC') Seychelles
option(value='SL') Sierra Leone
option(value='SG') Singapore
option(value='SK') Slovakia
option(value='SI') Slovenia
option(value='SB') Solomon Islands
option(value='ZA') South Africa
option(value='GS') South Georgia and the South Sandwich Islands
option(value='KR') South Korea
option(value='ES') Spain
option(value='LK') Sri Lanka
option(value='SR') Suriname
option(value='SJ') Svalbard and Jan Mayen
option(value='SZ') Swaziland
option(value='SE') Sweden
option(value='CH') Switzerland
option(value='ST') S&atilde;o Tom&eacute; and Pr&iacute;ncipe
option(value='TW') Taiwan
option(value='TJ') Tajikistan
option(value='TZ') Tanzania
option(value='TH') Thailand
option(value='TG') Togo
option(value='TK') Tokelau
option(value='TO') Tonga
option(value='TT') Trinidad and Tobago
option(value='TA') Tristan da Cunha
option(value='TN') Tunisia
option(value='TR') Turkey
option(value='TM') Turkmenistan
option(value='TC') Turks and Caicos Islands
option(value='TV') Tuvalu
option(value='UM') U.S. Minor Outlying Islands
option(value='PU') U.S. Miscellaneous Pacific Islands
option(value='VI') U.S. Virgin Islands
option(value='UG') Uganda
option(value='UA') Ukraine
option(value='AE') United Arab Emirates
option(value='GB') United Kingdom
option(value='US') United States
option(value='UY') Uruguay
option(value='UZ') Uzbekistan
option(value='VU') Vanuatu
option(value='VA') Vatican City
option(value='VE') Venezuela
option(value='VN') Vietnam
option(value='WK') Wake Island
option(value='WF') Wallis and Futuna
option(value='EH') Western Sahara
option(value='YE') Yemen
option(value='ZM') Zambia
option(value='AX') &angst;land Islands
+6 -12
View File
@@ -76,7 +76,7 @@ block content
br
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
@@ -90,10 +90,8 @@ block content
| {{plans[currencyCode]['collaborator']['annual']}}
span.small /yr
ul.list-unstyled
li
strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})}
strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})}
strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})}
li
strong #{translate("collabs_per_proj", {collabcount:10})}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
@@ -144,7 +142,7 @@ block content
br
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
) #{translate("sign_up_now")}
.col-md-4
@@ -157,9 +155,7 @@ block content
span.small /mo
ul.list-unstyled
li
strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
strong #{translate("collabs_per_proj", {collabcount:6})}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
@@ -180,9 +176,7 @@ block content
span.small /yr
ul.list-unstyled
li
strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
strong #{translate("collabs_per_proj", {collabcount:6})}
li #{translate("full_doc_history")}
li #{translate("sync_to_dropbox")}
li #{translate("sync_to_github")}
+13 -11
View File
@@ -118,17 +118,19 @@ block content
| !{moduleIncludes("userSettings", locals)}
hr
h3
| #{translate("sharelatex_beta_program")}
if (user.betaProgram)
p.small
| #{translate("beta_program_already_participating")}
div
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
//- The beta program doesn't make much sense to include while v2 is going
//- but we may want to add it back in later
//- hr
//-
//- h3
//- | #{translate("sharelatex_beta_program")}
//-
//- if (user.betaProgram)
//- p.small
//- | #{translate("beta_program_already_participating")}
//-
//- div
//- a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
hr
@@ -154,6 +154,8 @@ module.exports = settings =
url: "http://#{process.env['ANALYTICS_HOST'] or 'localhost'}:3050"
linkedUrlProxy:
url: process.env['LINKED_URL_PROXY']
thirdpartyreferences:
url: "http://#{process.env['THIRD_PARTY_REFERENCES_HOST'] or 'localhost'}:3046"
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
+9 -3
View File
@@ -18,6 +18,7 @@ module.exports = function (config) {
'public/js/libs/angular-1.6.4.min.js',
'public/js/libs/angular-mocks.js',
'public/js/libs/jquery-1.11.1.min.js',
'public/js/libs/underscore-1.3.3.js',
// Set up requirejs
'test/unit_frontend/js/test-main.js',
// Include source & test files, but don't "include" them as requirejs
@@ -25,13 +26,18 @@ module.exports = function (config) {
{ pattern: 'public/js/**/*.js', included: false },
{ pattern: 'test/unit_frontend/js/**/*.js', included: false },
// Include ES test files
'test/unit_frontend/es/**/*.js'
'test/unit_frontend/es/**/*.js',
'modules/**/test/unit_frontend/es/**/*.js',
// Include CSS (there is some in js/libs dir)
'public/stylesheets/**/*.css',
'public/js/libs/**/*.css'
],
middleware: ['fake-img'],
preprocessors: {
// Run ES test files through webpack (which will then include source
// files in bundle)
'test/unit_frontend/es/**/*.js': ['webpack']
'test/unit_frontend/es/**/*.js': ['webpack'],
'modules/**/test/unit_frontend/es/**/*.js': ['webpack']
},
frameworks: ['requirejs', 'mocha', 'chai-sinon'],
// Configure webpack in the tests
@@ -72,4 +78,4 @@ function fakeImgMiddlewareFactory () {
}
next()
}
}
}
-6
View File
@@ -1,6 +0,0 @@
# Ignore all modules except for a whitelist
*
!dropbox
!github-sync
!public-registration
!.gitignore
View File
+3 -2
View File
@@ -5,10 +5,11 @@
],
"verbose": true,
"legacyWatch": true,
"exec": "make compile",
"exec": "make compile || exit 1",
"watch": [
"public/coffee/",
"public/stylesheets/"
"public/stylesheets/",
"modules/**/public/coffee/"
],
"ext": "coffee less"
}
+2 -1
View File
@@ -11,7 +11,8 @@
"watch": [
"app/coffee/",
"app.coffee",
"modules/*/app/coffee/"
"modules/*/app/coffee/",
"config/"
],
"ext": "coffee"
}
+126 -8
View File
@@ -533,6 +533,12 @@
"resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz",
"dev": true
},
"babel-helper-builder-react-jsx": {
"version": "6.26.0",
"from": "babel-helper-builder-react-jsx@>=6.24.1 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz",
"dev": true
},
"babel-helper-call-delegate": {
"version": "6.24.1",
"from": "babel-helper-call-delegate@>=6.24.1 <7.0.0",
@@ -629,6 +635,18 @@
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz",
"dev": true
},
"babel-plugin-syntax-flow": {
"version": "6.18.0",
"from": "babel-plugin-syntax-flow@>=6.18.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
"dev": true
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"from": "babel-plugin-syntax-jsx@>=6.3.13 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
"dev": true
},
"babel-plugin-syntax-trailing-function-commas": {
"version": "6.22.0",
"from": "babel-plugin-syntax-trailing-function-commas@>=6.22.0 <7.0.0",
@@ -779,6 +797,36 @@
"resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz",
"dev": true
},
"babel-plugin-transform-flow-strip-types": {
"version": "6.22.0",
"from": "babel-plugin-transform-flow-strip-types@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz",
"dev": true
},
"babel-plugin-transform-react-display-name": {
"version": "6.25.0",
"from": "babel-plugin-transform-react-display-name@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz",
"dev": true
},
"babel-plugin-transform-react-jsx": {
"version": "6.24.1",
"from": "babel-plugin-transform-react-jsx@>=6.24.1 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz",
"dev": true
},
"babel-plugin-transform-react-jsx-self": {
"version": "6.22.0",
"from": "babel-plugin-transform-react-jsx-self@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz",
"dev": true
},
"babel-plugin-transform-react-jsx-source": {
"version": "6.22.0",
"from": "babel-plugin-transform-react-jsx-source@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz",
"dev": true
},
"babel-plugin-transform-regenerator": {
"version": "6.26.0",
"from": "babel-plugin-transform-regenerator@>=6.22.0 <7.0.0",
@@ -805,6 +853,18 @@
}
}
},
"babel-preset-flow": {
"version": "6.23.0",
"from": "babel-preset-flow@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz",
"dev": true
},
"babel-preset-react": {
"version": "6.24.1",
"from": "babel-preset-react@>=6.16.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz",
"dev": true
},
"babel-register": {
"version": "6.26.0",
"from": "babel-register@>=6.26.0 <7.0.0",
@@ -2031,6 +2091,11 @@
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz",
"dev": true
},
"create-react-class": {
"version": "15.6.3",
"from": "create-react-class@>=15.6.0 <16.0.0",
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz"
},
"cross-spawn": {
"version": "5.1.0",
"from": "cross-spawn@>=5.0.1 <6.0.0",
@@ -3429,6 +3494,23 @@
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
"dev": true
},
"fbjs": {
"version": "0.8.16",
"from": "fbjs@>=0.8.9 <0.9.0",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
"dependencies": {
"core-js": {
"version": "1.2.7",
"from": "core-js@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz"
},
"promise": {
"version": "7.3.1",
"from": "promise@>=7.1.1 <8.0.0",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz"
}
}
},
"fd-slicer": {
"version": "1.0.1",
"from": "fd-slicer@>=1.0.1 <1.1.0",
@@ -3744,6 +3826,11 @@
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
"dev": true
},
"fuse.js": {
"version": "3.2.0",
"from": "fuse.js@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.2.0.tgz"
},
"gauge": {
"version": "2.7.4",
"from": "gauge@>=2.7.3 <2.8.0",
@@ -5307,8 +5394,7 @@
"is-stream": {
"version": "1.1.0",
"from": "is-stream@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz"
},
"is-symbol": {
"version": "1.0.1",
@@ -5355,6 +5441,11 @@
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"dev": true
},
"isomorphic-fetch": {
"version": "2.2.1",
"from": "isomorphic-fetch@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz"
},
"isstream": {
"version": "0.1.2",
"from": "isstream@>=0.1.2 <0.2.0",
@@ -5391,8 +5482,7 @@
"js-tokens": {
"version": "3.0.2",
"from": "js-tokens@>=3.0.2 <4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz"
},
"js-yaml": {
"version": "2.0.5",
@@ -6858,8 +6948,7 @@
"loose-envify": {
"version": "1.3.1",
"from": "loose-envify@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz"
},
"loud-rejection": {
"version": "1.6.0",
@@ -7620,6 +7709,11 @@
"from": "nocache@2.0.0",
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz"
},
"node-fetch": {
"version": "1.7.3",
"from": "node-fetch@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz"
},
"node-forge": {
"version": "0.2.24",
"from": "node-forge@0.2.24",
@@ -8663,6 +8757,11 @@
"resolved": "https://registry.npmjs.org/prompt/-/prompt-0.2.14.tgz",
"dev": true
},
"prop-types": {
"version": "15.6.1",
"from": "prop-types@>=15.5.10 <16.0.0",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz"
},
"proxy-addr": {
"version": "1.0.10",
"from": "proxy-addr@>=1.0.8 <1.1.0",
@@ -8967,6 +9066,16 @@
}
}
},
"react": {
"version": "15.6.2",
"from": "react@>=15.4.2 <16.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz"
},
"react-dom": {
"version": "15.6.2",
"from": "react-dom@>=15.4.2 <16.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz"
},
"read": {
"version": "1.0.7",
"from": "read@>=1.0.0 <1.1.0",
@@ -9676,8 +9785,7 @@
"setimmediate": {
"version": "1.0.5",
"from": "setimmediate@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"dev": true
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz"
},
"setprototypeof": {
"version": "1.0.3",
@@ -10945,6 +11053,11 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"dev": true
},
"ua-parser-js": {
"version": "0.7.17",
"from": "ua-parser-js@>=0.7.9 <0.8.0",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz"
},
"uglify-js": {
"version": "2.4.24",
"from": "uglify-js@>=2.4.0 <2.5.0",
@@ -12061,6 +12174,11 @@
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
"dev": true
},
"whatwg-fetch": {
"version": "2.0.4",
"from": "whatwg-fetch@>=0.10.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz"
},
"when": {
"version": "3.7.8",
"from": "when@>=3.7.7 <4.0.0",
+4
View File
@@ -42,6 +42,7 @@
"express-http-proxy": "^1.1.0",
"express-session": "^1.14.2",
"fs-extra": "^4.0.2",
"fuse.js": "^3.0.0",
"heapdump": "^0.3.7",
"helmet": "^3.8.1",
"http-proxy": "^1.8.1",
@@ -72,6 +73,8 @@
"passport-oauth2-refresh": "^1.0.0",
"passport-saml": "^0.15.0",
"pug": "^2.0.0-beta6",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.4",
"request": "^2.69.0",
"requestretry": "^1.13.0",
@@ -95,6 +98,7 @@
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.16.0",
"bunyan": "0.22.1",
"chai": "3.5.0",
"chai-spies": "",
@@ -0,0 +1,41 @@
# For sending event data to metabase and google analytics
# ---
# by default,
# event not sent to MB.
# for MB, add event-tracking-mb='true'
# by default, event sent to MB via sendMB
# this can be changed to use sendMBOnce via event-tracking-send-once='true' attribute
# event not sent to GA.
# for GA, add event-tracking-ga attribute, where the value is the GA category
# event-tracking-trigger attribute is required to send event
define [
'base'
], (App) ->
App.directive 'eventTracking', ['event_tracking', (event_tracking) ->
return {
scope: {
eventTracking: '@',
eventSegmentation: '=?'
}
link: (scope, element, attrs) ->
sendGA = attrs.eventTrackingGa || false
sendMB = attrs.eventTrackingMb || false
sendMBFunction = if attrs.eventTrackingSendOnce then 'sendMBOnce' else 'sendMB'
segmentation = scope.eventSegmentation || {}
segmentation.page = window.location.pathname
sendEvent = () ->
if sendMB
event_tracking[sendMBFunction] scope.eventTracking, segmentation
if sendGA
event_tracking.send attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
if attrs.eventTrackingTrigger == 'load'
sendEvent()
else if attrs.eventTrackingTrigger == 'click'
element.on 'click', (e) ->
sendEvent()
}
]
@@ -17,3 +17,7 @@ define [
App.filter "relativeDate", () ->
(date) ->
moment(date).calendar()
App.filter "fromNowDate", () ->
(date) ->
moment(date).fromNow()
@@ -8,6 +8,7 @@ define [
@openFile(entity)
openFile: (file) ->
@ide.fileTreeManager.selectEntity(file)
@$scope.ui.view = "file"
@$scope.openFile = null
@$scope.$apply()
@@ -2,12 +2,17 @@ define [
"base"
"libs/jquery-layout"
], (App) ->
App.directive "layout", ["$parse", "ide", ($parse, ide) ->
App.directive "layout", ["$parse", "$compile", "ide", ($parse, $compile, ide) ->
return {
compile: () ->
pre: (scope, element, attrs) ->
name = attrs.layout
customTogglerPane = scope.$eval(attrs.customTogglerPane or "false")
customTogglerMsgWhenOpen = scope.$eval(attrs.customTogglerMsgWhenOpen or "false")
customTogglerMsgWhenClosed = scope.$eval(attrs.customTogglerMsgWhenClosed or "false")
hasCustomToggler = customTogglerPane != false and customTogglerMsgWhenOpen != false and customTogglerMsgWhenClosed != false
if attrs.spacingOpen?
spacingOpen = parseInt(attrs.spacingOpen, 10)
else
@@ -23,6 +28,10 @@ define [
spacing_closed: spacingClosed
slidable: false
enableCursorHotkey: false
onopen: (pane) =>
onPaneOpen(pane)
onclose: (pane) =>
onPaneClose(pane)
onresize: () =>
onInternalResize()
maskIframesOnResize: scope.$eval(
@@ -62,6 +71,15 @@ define [
right: state.east.size
})
repositionCustomToggler = () ->
if !customTogglerEl?
return
state = element.layout().readState()
positionAnchor = if customTogglerPane == "east" then "right" else "left"
paneState = state[customTogglerPane]
if paneState?
customTogglerEl.css(positionAnchor, if paneState.initClosed then 0 else paneState.size)
resetOpenStates = () ->
state = element.layout().readState()
if attrs.openEast? and state.east?
@@ -73,6 +91,8 @@ define [
state = element.layout().readState()
scope.$broadcast "layout:#{name}:resize", state
repositionControls()
if hasCustomToggler
repositionCustomToggler()
resetOpenStates()
oldWidth = element.width()
@@ -94,6 +114,46 @@ define [
if attrs.resizeOn?
scope.$on attrs.resizeOn, () -> onExternalResize()
if hasCustomToggler
state = element.layout().readState()
customTogglerScope = scope.$new()
customTogglerScope.isOpen = true
customTogglerScope.isVisible = true
if state[customTogglerPane]?.initClosed == true
customTogglerScope.isOpen = false
customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
customTogglerScope.tooltipPlacement = if customTogglerPane == "east" then "left" else "right"
customTogglerScope.handleClick = () ->
element.layout().toggle(customTogglerPane)
repositionCustomToggler()
customTogglerEl = $compile("
<a href
ng-show=\"isVisible\"
class=\"custom-toggler #{ 'custom-toggler-' + customTogglerPane }\"
ng-class=\"isOpen ? 'custom-toggler-open' : 'custom-toggler-closed'\"
tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\"
tooltip-placement=\"{{ tooltipPlacement }}\"
ng-click=\"handleClick()\">
")(customTogglerScope)
element.append(customTogglerEl)
onPaneOpen = (pane) ->
if !hasCustomToggler and pane != customTogglerPane
return
customTogglerEl.scope().$applyAsync () ->
customTogglerEl.scope().isOpen = true
onPaneClose = (pane) ->
if !hasCustomToggler and pane != customTogglerPane
return
customTogglerEl.scope().$applyAsync () ->
customTogglerEl.scope().isOpen = false
# Save state when exiting
$(window).unload () ->
ide.localStorage("layout.#{name}", element.layout().readState())
@@ -128,6 +188,11 @@ define [
element.layout().hide("east")
else
element.layout().show("east")
if hasCustomToggler
customTogglerEl.scope().$applyAsync () ->
customTogglerEl.scope().isOpen = !value
customTogglerEl.scope().isVisible = !value
post: (scope, element, attrs) ->
name = attrs.layout
@@ -1,9 +1,8 @@
define [
"ide/editor/Document"
"ide/editor/directives/aceEditor"
"ide/editor/directives/cmEditor"
"ide/editor/directives/toggleSwitch"
"ide/editor/controllers/SavingNotificationController"
"ide/editor/controllers/EditorToolbarController"
], (Document) ->
class EditorManager
constructor: (@ide, @$scope) ->
@@ -189,6 +188,3 @@ define [
@$scope.editor.trackChanges = want
else
@_syncTimeout = setTimeout tryToggle, 100
toggleRichText: () ->
@$scope.editor.richText = !@$scope.editor.richText
@@ -1,7 +0,0 @@
define [
"base"
"ide/editor/Document"
], (App, Document) ->
App.controller "EditorToolbarController", ($scope, ide) ->
$scope.toggleRichText = () ->
ide.editorManager.toggleRichText()
@@ -1,33 +0,0 @@
define [
"base"
], (App) ->
App.directive "cmEditor", () ->
return {
scope: {
sharejsDoc: "="
}
link: (scope, element, attrs) ->
cm = Frontend.richText.init(element.find('.cm-editor-wrapper')[0])
scope.$watch "sharejsDoc", (sharejsDoc, oldSharejsDoc) ->
if oldSharejsDoc?
detachFromCM(oldSharejsDoc)
if sharejsDoc?
attachToCM(sharejsDoc)
attachToCM = (sharejsDoc) ->
scope.$applyAsync () ->
Frontend.richText.openDoc(cm, sharejsDoc.getSnapshot())
sharejsDoc.attachToCM(cm)
detachFromCM = (sharejsDoc) ->
sharejsDoc.detachFromCM()
scope.$on 'destroy', () ->
detachFromCM(scope.sharejsDoc)
template: """
<div class="cm-editor-wrapper"></div>
"""
}
@@ -0,0 +1,37 @@
define [
"base"
], (App) ->
App.directive "toggleSwitch", () ->
restrict: "E"
scope:
description: "@"
labelFalse: "@"
labelTrue: "@"
ngModel: "="
template: """
<fieldset class="toggle-switch">
<legend class="sr-only">{{description}}</legend>
<input
type="radio"
name="editor-mode"
class="toggle-switch-input"
id="toggle-switch-false-{{$id}}"
ng-value="false"
ng-model="ngModel"
>
<label for="toggle-switch-false-{{$id}}" class="toggle-switch-label">{{labelFalse}}</label>
<input
type="radio"
class="toggle-switch-input"
name="editor-mode"
id="toggle-switch-true-{{$id}}"
ng-value="true"
ng-model="ngModel"
>
<label for="toggle-switch-true-{{$id}}" class="toggle-switch-label">{{labelTrue}}</label>
<span class="toggle-switch-selection" aria-hidden="true"></span>
</fieldset>
"""
@@ -4,6 +4,7 @@ define [
"ide/history/util/displayNameForUser"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
"ide/history/controllers/HistoryV2DiffController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager, displayNameForUser) ->
class HistoryManager
@@ -49,6 +49,13 @@ define [
diff: null
}
restoreFile: (version, pathname) ->
url = "/project/#{@$scope.project_id}/restore_file"
@ide.$http.post(url, {
version, pathname,
_csrf: window.csrfToken
})
MAX_RECENT_UPDATES_TO_SELECT: 5
autoSelectRecentUpdates: () ->
return if @$scope.history.updates.length == 0
@@ -204,7 +211,7 @@ define [
# Map of original pathname -> doc summary
docs_summary = Object.create(null)
updatePathnameWithUpdateVersions = (pathname, update, deleted) ->
updatePathnameWithUpdateVersions = (pathname, update, deletedAtV) ->
# 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
@@ -222,8 +229,8 @@ define [
doc_summary.toV,
update.toV
)
if deleted?
doc_summary.deleted = true
if deletedAtV?
doc_summary.deletedAtV = deletedAtV
# Put updates in ascending chronological order
updates = updates.slice().reverse()
@@ -241,7 +248,7 @@ define [
updatePathnameWithUpdateVersions(add.pathname, update)
if project_op.remove?
remove = project_op.remove
updatePathnameWithUpdateVersions(remove.pathname, update, true)
updatePathnameWithUpdateVersions(remove.pathname, update, project_op.atV)
return docs_summary
@@ -3,15 +3,6 @@ define [
"ide/history/util/displayNameForUser"
], (App, displayNameForUser) ->
App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)->
$scope.$watch "ui.view", ->
if $scope.ui.view == "history"
if $scope.project?.features?.versioning
$scope.versioningPopupType = "default"
else if $scope.ui.view == "history"
sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)->
$scope.versioningPopupType = chosenVariation
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
$scope.hoveringOverListSelectors = false
@@ -0,0 +1,40 @@
define [
"base"
], (App) ->
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
$scope.restoreState =
inflight: false
error: false
$scope.restoreDeletedFile = () ->
pathname = $scope.history.selection.pathname
return if !pathname?
version = $scope.history.selection.docs[pathname]?.deletedAtV
return if !version?
event_tracking.sendMB "history-v2-restore-deleted"
$scope.restoreState.inflight = true
ide.historyManager
.restoreFile(version, pathname)
.then (response) ->
{ data } = response
openEntity(data)
.catch () ->
ide.showGenericMessageModal('Sorry, something went wrong with the restore')
.finally () ->
$scope.restoreState.inflight = false
openEntity = (data) ->
iterations = 0
{id, type} = data
do tryOpen = () ->
if iterations > 5
return
iterations += 1
entity = ide.fileTreeManager.findEntityById(id)
if entity? and type == 'doc'
ide.editorManager.openDoc(entity)
else if entity? and type == 'file'
ide.binaryFilesManager.openFile(entity)
else
setTimeout(tryOpen, 500)
+1
View File
@@ -24,6 +24,7 @@ define [
"directives/stopPropagation"
"directives/focus"
"directives/equals"
"directives/eventTracking"
"directives/fineUpload"
"directives/onEnter"
"directives/selectAll"
@@ -19,24 +19,8 @@ define [
url = "#{url}&cc=#{couponCode}"
$scope.startedFreeTrial = true
switch source
when "dropbox"
sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) ->
event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
when "history"
sixpack.participate 'teaser-history', ['default', 'focused'], (variant) ->
event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
else
event_tracking.sendMB "subscription-start-trial", { source, plan }
event_tracking.sendMB "subscription-start-trial", { source, plan }
w.location = url
if $scope.shouldABTestPlans
sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
if chosenVariation in ['heron', 'ibis']
plan = "collaborator_#{chosenVariation}"
go()
else
go()
go()
@@ -1,6 +1,7 @@
define [
"base",
"directives/creditCards"
"libs/recurly-4.8.5"
], (App)->
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
@@ -21,10 +22,6 @@ define [
value: "credit_card"
$scope.data =
number: ""
month: ""
year: ""
cvv: ""
first_name: ""
last_name: ""
postal_code: ""
@@ -34,16 +31,25 @@ define [
city:""
country:window.countryCode
coupon: window.couponCode
mmYY: ""
$scope.validation =
correctCardNumber : true
correctExpiry: true
correctCvv: true
$scope.validation = {}
$scope.processing = false
recurly.configure window.recurlyApiKey
recurly.configure
publicKey: window.recurlyApiKey
style:
all:
fontFamily: '"Open Sans", sans-serif',
fontSize: '16px',
fontColor: '#7a7a7a'
month:
placeholder: 'MM'
year:
placeholder: 'YY'
cvv:
placeholder: 'CVV'
pricing = recurly.Pricing()
window.pricing = pricing
@@ -73,6 +79,8 @@ define [
$scope.normalPrice += (basePrice * pricing.price.taxes[0].rate)
$scope.$apply()
$scope.applyCoupon = ->
pricing.coupon($scope.data.coupon).done()
@@ -83,26 +91,6 @@ define [
$scope.currencyCode = newCurrency
pricing.currency(newCurrency).done()
$scope.updateExpiry = () ->
parsedDateObj = ccUtils.parseExpiry $scope.data.mmYY
if parsedDateObj?
$scope.data.month = parsedDateObj.month
$scope.data.year = parsedDateObj.year
$scope.validateCardNumber = validateCardNumber = ->
$scope.validation.errorFields = {}
if $scope.data.number?.length != 0
$scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number)
$scope.validateExpiry = validateExpiry = ->
$scope.validation.errorFields = {}
if $scope.data.month?.length != 0 and $scope.data.year?.length != 0
$scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year)
$scope.validateCvv = validateCvv = ->
$scope.validation.errorFields = {}
if $scope.data.cvv?.length != 0
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
$scope.inputHasError = inputHasError = (formItem) ->
if !formItem?
@@ -114,10 +102,7 @@ define [
if $scope.paymentMethod.value == 'paypal'
return $scope.data.country != ""
else
return (form.$valid and
$scope.validation.correctCardNumber and
$scope.validation.correctExpiry and
$scope.validation.correctCvv)
return form.$valid
$scope.updateCountry = ->
pricing.address({country:$scope.data.country}).done()
@@ -178,4 +163,74 @@ define [
recurly.token $scope.data, completeSubscription
$scope.countries = [
{code:'AF',name:'Afghanistan'},{code:'AL',name:'Albania'},{code:'DZ',name:'Algeria'},{code:'AS',name:'American Samoa'},
{code:'AD',name:'Andorra'},{code:'AO',name:'Angola'},{code:'AI',name:'Anguilla'},{code:'AQ',name:'Antarctica'},
{code:'AG',name:'Antigua and Barbuda'},{code:'AR',name:'Argentina'},{code:'AM',name:'Armenia'},{code:'AW',name:'Aruba'},
{code:'AC',name:'Ascension Island'},{code:'AU',name:'Australia'},{code:'AT',name:'Austria'},{code:'AZ',name:'Azerbaijan'},
{code:'BS',name:'Bahamas'},{code:'BH',name:'Bahrain'},{code:'BD',name:'Bangladesh'},{code:'BB',name:'Barbados'},
{code:'BE',name:'Belgium'},{code:'BZ',name:'Belize'},{code:'BJ',name:'Benin'},{code:'BM',name:'Bermuda'},
{code:'BT',name:'Bhutan'},{code:'BO',name:'Bolivia'},{code:'BA',name:'Bosnia and Herzegovina'},{code:'BW',name:'Botswana'},
{code:'BV',name:'Bouvet Island'},{code:'BR',name:'Brazil'},{code:'BQ',name:'British Antarctic Territory'},
{code:'IO',name:'British Indian Ocean Territory'},{code:'VG',name:'British Virgin Islands'},{code:'BN',name:'Brunei'},
{code:'BG',name:'Bulgaria'},{code:'BF',name:'Burkina Faso'},{code:'BI',name:'Burundi'},{code:'KH',name:'Cambodia'},
{code:'CM',name:'Cameroon'},{code:'CA',name:'Canada'},{code:'IC',name:'Canary Islands'},
{code:'CT',name:'Canton and Enderbury Islands'},{code:'CV',name:'Cape Verde'},{code:'KY',name:'Cayman Islands'},
{code:'CF',name:'Central African Republic'},{code:'EA',name:'Ceuta and Melilla'},{code:'TD',name:'Chad'},
{code:'CL',name:'Chile'},{code:'CN',name:'China'},{code:'CX',name:'Christmas Island'},{code:'CP',name:'Clipperton Island'},
{code:'CC',name:'Cocos [Keeling] Islands'},{code:'CO',name:'Colombia'},{code:'KM',name:'Comoros'},{code:'CD',name:'Congo [DRC]'},
{code:'CK',name:'Cook Islands'},{code:'CR',name:'Costa Rica'},{code:'HR',name:'Croatia'},{code:'CU',name:'Cuba'},
{code:'CY',name:'Cyprus'},{code:'CZ',name:'Czech Republic'},{code:'DK',name:'Denmark'},{code:'DG',name:'Diego Garcia'},
{code:'DJ',name:'Djibouti'},{code:'DM',name:'Dominica'},{code:'DO',name:'Dominican Republic'},
{code:'NQ',name:'Dronning Maud Land'},{code:'TL',name:'East Timor'},{code:'EC',name:'Ecuador'},{code:'EG',name:'Egypt'},
{code:'SV',name:'El Salvador'},{code:'EE',name:'Estonia'},{code:'ET',name:'Ethiopia'},
{code:'FK',name:'Falkland Islands [Islas Malvinas]'},{code:'FO',name:'Faroe Islands'},{code:'FJ',name:'Fiji'},
{code:'FI',name:'Finland'},{code:'FR',name:'France'},{code:'GF',name:'French Guiana'},{code:'PF',name:'French Polynesia'},
{code:'TF',name:'French Southern Territories'},{code:'FQ',name:'French Southern and Antarctic Territories'},
{code:'GA',name:'Gabon'},{code:'GM',name:'Gambia'},{code:'GE',name:'Georgia'},{code:'DE',name:'Germany'},
{code:'GH',name:'Ghana'},{code:'GI',name:'Gibraltar'},{code:'GR',name:'Greece'},{code:'GL',name:'Greenland'},
{code:'GD',name:'Grenada'},{code:'GP',name:'Guadeloupe'},{code:'GU',name:'Guam'},{code:'GT',name:'Guatemala'},
{code:'GG',name:'Guernsey'},{code:'GW',name:'Guinea-Bissau'},{code:'GY',name:'Guyana'},{code:'HT',name:'Haiti'},
{code:'HM',name:'Heard Island and McDonald Islands'},{code:'HN',name:'Honduras'},{code:'HK',name:'Hong Kong'},
{code:'HU',name:'Hungary'},{code:'IS',name:'Iceland'},{code:'IN',name:'India'},{code:'ID',name:'Indonesia'},
{code:'IE',name:'Ireland'},{code:'IM',name:'Isle of Man'},{code:'IL',name:'Israel'},{code:'IT',name:'Italy'},
{code:'JM',name:'Jamaica'},{code:'JP',name:'Japan'},{code:'JE',name:'Jersey'},{code:'JT',name:'Johnston Island'},
{code:'JO',name:'Jordan'},{code:'KZ',name:'Kazakhstan'},{code:'KE',name:'Kenya'},{code:'KI',name:'Kiribati'},
{code:'KW',name:'Kuwait'},{code:'KG',name:'Kyrgyzstan'},{code:'LA',name:'Laos'},{code:'LV',name:'Latvia'},
{code:'LS',name:'Lesotho'},{code:'LY',name:'Libya'},{code:'LI',name:'Liechtenstein'},{code:'LT',name:'Lithuania'},
{code:'LU',name:'Luxembourg'},{code:'MO',name:'Macau'},{code:'MK',name:'Macedonia [FYROM]'},{code:'MG',name:'Madagascar'},
{code:'MW',name:'Malawi'},{code:'MY',name:'Malaysia'},{code:'MV',name:'Maldives'},{code:'ML',name:'Mali'},
{code:'MT',name:'Malta'},{code:'MH',name:'Marshall Islands'},{code:'MQ',name:'Martinique'},{code:'MR',name:'Mauritania'},
{code:'MU',name:'Mauritius'},{code:'YT',name:'Mayotte'},{code:'FX',name:'Metropolitan France'},{code:'MX',name:'Mexico'},
{code:'FM',name:'Micronesia'},{code:'MI',name:'Midway Islands'},{code:'MD',name:'Moldova'},{code:'MC',name:'Monaco'},
{code:'MN',name:'Mongolia'},{code:'ME',name:'Montenegro'},{code:'MS',name:'Montserrat'},{code:'MA',name:'Morocco'},
{code:'MZ',name:'Mozambique'},{code:'NA',name:'Namibia'},{code:'NR',name:'Nauru'},{code:'NP',name:'Nepal'},
{code:'NL',name:'Netherlands'},{code:'AN',name:'Netherlands Antilles'},{code:'NT',name:'Neutral Zone'},
{code:'NC',name:'New Caledonia'},{code:'NZ',name:'New Zealand'},{code:'NI',name:'Nicaragua'},{code:'NE',name:'Niger'},
{code:'NG',name:'Nigeria'},{code:'NU',name:'Niue'},{code:'NF',name:'Norfolk Island'},{code:'VD',name:'North Vietnam'},
{code:'MP',name:'Northern Mariana Islands'},{code:'NO',name:'Norway'},{code:'OM',name:'Oman'},
{code:'QO',name:'Outlying Oceania'},{code:'PC',name:'Pacific Islands Trust Territory'},{code:'PK',name:'Pakistan'},
{code:'PW',name:'Palau'},{code:'PS',name:'Palestinian Territories'},{code:'PA',name:'Panama'},{code:'PZ',name:'Panama Canal Zone'},
{code:'PY',name:'Paraguay'},{code:'YD',name:'People&apos;s Democratic Republic of Yemen'},{code:'PE',name:'Peru'},
{code:'PH',name:'Philippines'},{code:'PN',name:'Pitcairn Islands'},{code:'PL',name:'Poland'},{code:'PT',name:'Portugal'},
{code:'PR',name:'Puerto Rico'},{code:'QA',name:'Qatar'},{code:'RO',name:'Romania'},{code:'RU',name:'Russia'},
{code:'RW',name:'Rwanda'},{code:'RE',name:'R&eacute;union'},{code:'BL',name:'Saint Barth&eacute;lemy'},
{code:'SH',name:'Saint Helena'},{code:'KN',name:'Saint Kitts and Nevis'},{code:'LC',name:'Saint Lucia'},
{code:'MF',name:'Saint Martin'},{code:'PM',name:'Saint Pierre and Miquelon'},{code:'VC',name:'Saint Vincent and the Grenadines'},
{code:'WS',name:'Samoa'},{code:'SM',name:'San Marino'},{code:'SA',name:'Saudi Arabia'},{code:'SN',name:'Senegal'},
{code:'RS',name:'Serbia'},{code:'CS',name:'Serbia and Montenegro'},{code:'SC',name:'Seychelles'},{code:'SL',name:'Sierra Leone'},
{code:'SG',name:'Singapore'},{code:'SK',name:'Slovakia'},{code:'SI',name:'Slovenia'},{code:'SB',name:'Solomon Islands'},
{code:'ZA',name:'South Africa'},{code:'GS',name:'South Georgia and the South Sandwich Islands'},{code:'KR',name:'South Korea'},
{code:'ES',name:'Spain'},{code:'LK',name:'Sri Lanka'},{code:'SR',name:'Suriname'},{code:'SJ',name:'Svalbard and Jan Mayen'},
{code:'SZ',name:'Swaziland'},{code:'SE',name:'Sweden'},{code:'CH',name:'Switzerland'},
{code:'ST',name:'S&atilde;o Tom&eacute; and Pr&iacute;ncipe'},{code:'TW',name:'Taiwan'},{code:'TJ',name:'Tajikistan'},
{code:'TZ',name:'Tanzania'},{code:'TH',name:'Thailand'},{code:'TG',name:'Togo'},{code:'TK',name:'Tokelau'},{code:'TO',name:'Tonga'},
{code:'TT',name:'Trinidad and Tobago'},{code:'TA',name:'Tristan da Cunha'},{code:'TN',name:'Tunisia'},{code:'TR',name:'Turkey'},
{code:'TM',name:'Turkmenistan'},{code:'TC',name:'Turks and Caicos Islands'},{code:'TV',name:'Tuvalu'},
{code:'UM',name:'U.S. Minor Outlying Islands'},{code:'PU',name:'U.S. Miscellaneous Pacific Islands'},
{code:'VI',name:'U.S. Virgin Islands'},{code:'UG',name:'Uganda'},{code:'UA',name:'Ukraine'},{code:'AE',name:'United Arab Emirates'},
{code:'GB',name:'United Kingdom'},{code:'US',name:'United States'},{code:'UY',name:'Uruguay'},{code:'UZ',name:'Uzbekistan'},
{code:'VU',name:'Vanuatu'},{code:'VA',name:'Vatican City'},{code:'VE',name:'Venezuela'},{code:'VN',name:'Vietnam'},
{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
{code:'ZM',name:'Zambia'},{code:'AX',name:'&angst;land Islandscode:'}
]
+1 -167
View File
@@ -1,6 +1,6 @@
define [
"base"
"libs/recurly-3.0.5"
"libs/recurly-4.8.5"
], (App, recurly) ->
@@ -11,164 +11,6 @@ define [
return {
currencyCode:currencyCode
heron:
USD:
student:
monthly: "$6"
annual: "$60"
collaborator:
monthly: "$12"
annual: "$144"
EUR:
student:
monthly: "€5"
annual: "€50"
collaborator:
monthly: "€11"
annual: "€132"
GBP:
student:
monthly: "£5"
annual: "£50"
collaborator:
monthly: "£10"
annual: "£120"
SEK:
student:
monthly: "45 kr"
annual: "450 kr"
collaborator:
monthly: "90 kr"
annual: "1080 kr"
CAD:
student:
monthly: "$7"
annual: "$70"
collaborator:
monthly: "$14"
annual: "$168"
NOK:
student:
monthly: "45 kr"
annual: "450 kr"
collaborator:
monthly: "90 kr"
annual: "1080 kr"
DKK:
student:
monthly: "40 kr"
annual: "400 kr"
collaborator:
monthly: "70 kr"
annual: "840 kr"
AUD:
student:
monthly: "$8"
annual: "$80"
collaborator:
monthly: "$15"
annual: "$180"
NZD:
student:
monthly: "$8"
annual: "$80"
collaborator:
monthly: "$15"
annual: "$180"
CHF:
student:
monthly: "Fr 6"
annual: "Fr 60"
collaborator:
monthly: "Fr 12"
annual: "Fr 144"
SGD:
student:
monthly: "$8"
annual: "$80"
collaborator:
monthly: "$16"
annual: "$192"
ibis:
USD:
student:
monthly: "$10"
annual: "$100"
collaborator:
monthly: "$18"
annual: "$216"
EUR:
student:
monthly: "€9"
annual: "€90"
collaborator:
monthly: "€17"
annual: "€204"
GBP:
student:
monthly: "£7"
annual: "£70"
collaborator:
monthly: "£14"
annual: "£168"
SEK:
student:
monthly: "75 kr"
annual: "750 kr"
collaborator:
monthly: "140 kr"
annual: "1680 kr"
CAD:
student:
monthly: "$12"
annual: "$120"
collaborator:
monthly: "$22"
annual: "$264"
NOK:
student:
monthly: "75 kr"
annual: "750 kr"
collaborator:
monthly: "140 kr"
annual: "1680 kr"
DKK:
student:
monthly: "68 kr"
annual: "680 kr"
collaborator:
monthly: "110 kr"
annual: "1320 kr"
AUD:
student:
monthly: "$13"
annual: "$130"
collaborator:
monthly: "$22"
annual: "$264"
NZD:
student:
monthly: "$14"
annual: "$140"
collaborator:
monthly: "$22"
annual: "$264"
CHF:
student:
monthly: "Fr 10"
annual: "Fr 100"
collaborator:
monthly: "Fr 18"
annual: "Fr 216"
SGD:
student:
monthly: "$14"
annual: "$140"
collaborator:
monthly: "$25"
annual: "$300"
plans:
USD:
symbol: "$"
@@ -312,14 +154,6 @@ define [
$scope.shouldABTestPlans = window.shouldABTestPlans
if $scope.shouldABTestPlans
sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
$scope.plansVariant = chosenVariation
event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation}
if chosenVariation in ['heron', 'ibis']
# overwrite student plans with alternative
for currency, _v of $scope.plans
$scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student']
$scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator']
$scope.showPlans = true
else
$scope.showPlans = true
@@ -3,8 +3,6 @@ define [
], (App)->
App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) ->
sixpack.convert 'plans-1610', () ->
SUBSCRIPTION_URL = "/user/subscription/update"
@@ -1,4 +1,11 @@
define [], () ->
# Simple event emitter implementation, but has a slightly unusual API for
# removing specific listeners. If a specific listener needs to be removed
# (instead of all listeners), then it needs to use a "namespace":
# Create a listener on the foo event with bar namespace: .on 'foo.bar'
# Trigger all events for the foo event (including namespaces): .trigger 'foo'
# Remove all listeners for the foo event (including namespaces): .off 'foo'
# Remove a listener for the foo event with the bar namespace: .off 'foo.bar'
class EventEmitter
on: (event, callback) ->
@events ||= {}
-16
View File
@@ -1,16 +0,0 @@
import CodeMirror, { Doc } from 'codemirror'
export function init (rootEl) {
CodeMirror.defineMIME('application/x-tex', 'latex')
CodeMirror.defineMIME('application/x-latex', 'latex')
return CodeMirror(rootEl, {
mode: 'latex'
})
}
export function openDoc (cm, content) {
const newDoc = Doc(content, 'latex')
cm.swapDoc(newDoc)
return newDoc
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>favicon</title>
<path d="M6.12,4.38A7.08,7.08,0,0,0,2.67,10.2,5.07,5.07,0,1,0,9.55,5.46a4.68,4.68,0,0,0-1.8-.34A7.48,7.48,0,0,0,5.2,8.07a3.33,3.33,0,1,1,2.54,5.47A3.33,3.33,0,0,1,5.2,12.37,4,4,0,0,1,4.24,9C4.89,5.11,9.55,2.87,13,2a35.85,35.85,0,0,0-4.6,2.65c4.17,1.61,4.84-1.9,6.79-3.47C13.25.42,6.13.14,6.12,4.38Z"/>
</svg>

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -84,8 +84,4 @@
@import "../js/libs/pdfListView/TextLayer.css";
@import "../js/libs/pdfListView/AnnotationsLayer.css";
@import "../js/libs/pdfListView/HighlightsLayer.css";
// CodeMirror
& when (@show-rich-text) {
@import "vendor/codemirror.css";
}
@import "vendor/codemirror.css";
+10 -4
View File
@@ -1,8 +1,14 @@
.system-message {
padding: (@line-height-computed / 4) (@line-height-computed / 2);
background-color: @state-warning-bg;
color: #333;
border-bottom: 1px solid @common-border-color;
padding: (@line-height-computed / 4) (@line-height-computed / 2);
background-color: @sys-msg-background;
color: @sys-msg-color;
border-bottom: @sys-msg-border;
}
.system-message .close when (@is-overleaf = true) {
color: #FFF;
opacity: 1;
text-shadow: none;
}
.clickable {
+86 -80
View File
@@ -12,9 +12,11 @@
@import "./editor/online-users.less";
@import "./editor/hotkeys.less";
@import "./editor/review-panel.less";
@import "./editor/rich-text.less";
@import "./editor/publish-modal.less";
@ui-layout-toggler-def-height: 50px;
@ui-resizer-extra-hit-area: 8px;
@ui-resizer-size: 7px;
@keyframes blink {
0% {
@@ -79,11 +81,13 @@
.full-size;
}
#editor when (@show-rich-text = true) {
top: 40px; // TODO: replace with toolbar height var
#editor-rich-text {
top: @editor-toolbar-height;
}
#editor-rich-text when (@show-rich-text = true) {
top: 40px; // TODO: replace with toolbar height var
.toolbar-editor {
height: @editor-toolbar-height;
background-color: @editor-toolbar-bg;
}
.loading-screen {
@@ -107,7 +111,7 @@
height: 0;
background: @editor-loading-logo-background-url no-repeat bottom / 100%;
&::after {
&::after {
content: '';
position: absolute;
height: inherit;
@@ -116,7 +120,7 @@
left: 0;
background: @editor-loading-logo-foreground-url no-repeat bottom / 100%;
transition: height .5s;
}
}
}
.loading-screen-label {
margin: 0;
@@ -302,40 +306,31 @@
}
.ui-layout-resizer when (@is-overleaf = true) {
margin-left: -(@ui-resizer-extra-hit-area) !important;
margin-right: -(@ui-resizer-extra-hit-area - 1px) !important;
padding-left: @ui-resizer-extra-hit-area !important;
padding-right: @ui-resizer-extra-hit-area !important;
z-index: 5 !important;
box-sizing: content-box;
background-image: linear-gradient(90deg,
transparent,
transparent (@ui-resizer-extra-hit-area - 1px),
@editor-resizer-bg-color (@ui-resizer-extra-hit-area - 1px),
@editor-resizer-bg-color (@ui-resizer-extra-hit-area + 1px),
transparent (@ui-resizer-extra-hit-area + 1px),
transparent);
.ui-layout-toggler {
padding: 0 @ui-resizer-extra-hit-area !important;
background-image: linear-gradient(90deg,
transparent,
transparent (@ui-resizer-extra-hit-area - 1px),
@editor-toggler-bg-color (@ui-resizer-extra-hit-area - 1px),
@editor-toggler-bg-color (@ui-resizer-extra-hit-area + 1px),
transparent (@ui-resizer-extra-hit-area + 1px),
transparent);
&:hover {
background-image: linear-gradient(90deg,
transparent,
transparent (@ui-resizer-extra-hit-area - 2px),
@editor-toggler-hover-bg-color (@ui-resizer-extra-hit-area - 2px),
@editor-toggler-hover-bg-color (@ui-resizer-extra-hit-area + 2px),
transparent (@ui-resizer-extra-hit-area + 2px),
transparent);
width: @ui-resizer-size !important;
background-color: @editor-resizer-bg-color;
&.ui-layout-resizer-closed {
&::before,
&::after {
content: none;
}
}
&::before,
&::after {
content: '\2847';
display: block;
position: absolute;
text-align: center;
left: -2px;
-webkit-font-smoothing: antialiased;
width: 100%;
font-size: 24px;
top: 25%;
color: @ol-blue-gray-2;
}
&::after {
top: 75%;
}
}
.ui-layout-resizer-west.ui-layout-resizer-open, .ui-layout-resizer-east.ui-layout-resizer-closed {
@@ -353,54 +348,66 @@
}
}
.ui-layout-toggler.ui-layout-toggler-closed when (@is-overleaf = true) {
background-color: @editor-resizer-bg-color;
background-image: none;
line-height: @ui-layout-toggler-def-height;
&::before {
content: "\22EE"; // Vertical ellipsis
display: block;
color: #FFF;
font-weight: 700;
font-size: @font-size-h2;
width: @ui-resizer-extra-hit-area / 2;
.ui-layout-toggler when (@is-overleaf = true) {
display: none !important;
}
.custom-toggler when (@is-overleaf = true) {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: @ui-resizer-size !important;;
height: 50px;
margin-top: -25px;
top: 50%;
z-index: 6;
background-color: @editor-toggler-bg-color;
&:hover,
&:focus {
outline: none;
text-decoration: none;
}
// Increase hit area
&::before {
content: '';
display: block;
position: absolute;
top: 0;
right: -3px;
bottom: 0;
left: -3px;
}
&::after {
font-family: FontAwesome;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 65%;
font-weight: bold;
color: #FFF;
user-select: none;
pointer-events: none;
}
&:hover {
background-color: @editor-toggler-hover-bg-color;
background-image: none;
}
.ui-layout-resizer-west > & {
border-radius: 0 @border-radius-base @border-radius-base 0;
&::before {
margin-left: -2px;
}
}
.ui-layout-resizer-east > & {
border-radius: @border-radius-base 0 0 @border-radius-base;
&::before {
margin-left: (-1 - @ui-resizer-extra-hit-area);
}
}
}
.custom-toggler-east::after {
content: '\f105';
}
.custom-toggler-west::after {
content: '\f104';
}
.ui-layout-toggler-east when (@is-overleaf = true) {
&.ui-layout-toggler-open {
cursor: e-resize !important
.custom-toggler-closed.custom-toggler-east::after {
content: '\f104';
}
&.ui-layout-toggler-closed {
cursor: w-resize !important
.custom-toggler-closed.custom-toggler-west::after {
content: '\f105';
}
}
.ui-layout-toggler-west when (@is-overleaf = true) {
&.ui-layout-toggler-open {
cursor: w-resize !important
}
&.ui-layout-toggler-closed {
cursor: e-resize !important
}
}
.ui-layout-resizer-dragging {
background-color: @editor-resizer-bg-color-dragging;
@@ -624,4 +631,3 @@
height: auto;
border-bottom: 1px solid @modal-header-border-color;
}
@@ -270,7 +270,7 @@
}
.synctex-controls when (@is-overleaf = true) {
margin-right: -11px;
margin-right: -8px;
}
.synctex-control {
display: block;
@@ -0,0 +1,89 @@
.modal-body-publish {
#search-input-container {
overflow: hidden;
margin: 5px 0 10px;
}
.table-content-name {
width: 100%;
margin-bottom: 10px;
font-weight: 300;
}
.table-content-category {
font-weight: 300;
text-align: right;
font-style: italic;
width: 30%;
float: right;
text-transform: capitalize
}
.table-content-category ~ .table-content-name {
width: 70%;
display: inline-block;
}
.wl-icon:before{
font-size: 14px;
}
.button-as-link{
color: green;
text-transform: none;
background: none;
padding: 0;
border: none;
border-radius: 0;
font-size: 14px;
@extend a;
&:hover,
&:active,
&:focus {
color: green;
background: none;
}
text-align: left;
}
.affix-content-title {
color: @gray-light;
font-size: 1.2em;
padding-left: 10px;
}
.affix-subcontent {
margin: 5px 0 50px;
}
.overbox {
padding: @line-height-computed / 2;
background-color: white;
margin-top: @line-height-computed / 2;
border: 1px solid @gray-lighter;
}
.content-as-table {
.table-content,
.table-content > * {
display: table;
}
.table-content-icon {
float: left;
height: 80px;
width: 106px;
border: 1px solid @gray-lightest;
display: flex;
align-items: center;
overflow: hidden;
* {
width: 100%;
}
}
.table-content-text {
float: right;
width: calc(~'100% - 106px');
vertical-align: top;
padding-left: 15px;
}
.table-content-slogan {
height: 80px;
overflow: hidden;
}
.table-content-link {
padding-top: 10px;
}
}
}
@@ -0,0 +1,222 @@
@rt-font-family: 'Source Sans Pro', 'Helvetica', 'Arial', sans-serif;
// @rt-font-family-serif: 'Palatino Linotype', 'Book Antiqua', Palatino, serif;
.rich-text {
font-family: @rt-font-family;
font-size: 1.15em;
pre, .CodeMirror-linewidget {
font-family: @rt-font-family;
}
// TODO: Change prefix away from wl- ?
/****************************************************************************/
.preamble h1 {
text-align: center;
font-size: 2em;
color: @text-color;
}
.preamble ul.authors {
margin-left: 0;
padding-left: 0;
list-style: none;
text-align: center;
}
.preamble ul.authors li {
padding-bottom: 5px;
}
/****************************************************************************/
.wl-indent-0 {
padding-left: 2.5em !important;
}
.wl-indent-1 {
padding-left: 3.5em !important;
}
.wl-indent-2 {
padding-left: 4.5em !important;
}
.wl-indent-3 {
padding-left: 5.5em !important;
}
.wl-indent-4 {
padding-left: 6.5em !important;
}
.wl-indent-env-0 {
padding-left: 4px !important;
}
.wl-indent-env-1 {
padding-left: 1.5em !important;
}
.wl-indent-env-2 {
padding-left: 2.5em !important;
}
.wl-indent-env-3 {
padding-left: 3.5em !important;
}
.wl-indent-env-4 {
padding-left: 4.5em !important;
}
.wl-enumerate-item-open {
text-align: right;
width: 1.5em;
display: inline-block;
padding-right: 0.2em;
}
.wl-item-open {
text-align: right;
width: 1.5em;
display: inline-block;
padding-right: 0.2em;
}
.wl-input {
font-style: oblique;
}
/****************************************************************************/
.wl-abstract-open, .wl-abstract-close {
border-top: 1px solid #999;
font-size: large;
font-weight: bold;
width: 100%;
}
.wl-figure {
max-height: 120px;
width: auto;
margin: 0 auto;
}
.wl-figure-wrap {
padding: 10px 0;
background-color: #f5f5f5;
box-shadow: 2px 2px 2px #DFDFDF;
width: 96%;
margin: 0 auto;
text-align: center;
}
.wl-figure-caption {
padding: 3px 0 4px;
font-size: small;
margin: 0 auto;
text-align: center;
}
/****************************************************************************/
.wl-chapter, .wl-chapter-open, .wl-chapter-close {
font-size: 2.2em;
font-weight: bold;
}
.wl-chapter-open, .wl-chapter-close {
color: #999;
}
/****************************************************************************/
.wl-section, .wl-section-open, .wl-section-close {
font-size: 1.8em;
font-weight: bold;
}
.wl-section-open, .wl-section-close {
color: #999;
}
/****************************************************************************/
.wl-subsection, .wl-subsection-open, .wl-subsection-close {
font-size: 1.5em;
font-weight: bold;
}
.wl-subsection-open, .wl-subsection-close {
color: #999;
}
/****************************************************************************/
.wl-subsubsection, .wl-subsubsection-open, .wl-subsubsection-close {
font-size: 1.1em;
font-weight: bold;
}
.wl-subsubsection-open, .wl-subsubsection-close {
color: #999;
}
/****************************************************************************/
.wl-textbf {
font-weight: bold;
}
.wl-textbf-open {
font-weight: bold;
color: #999;
}
.wl-textbf-close {
font-weight: bold;
color: #999;
}
.wl-label-bracket {
font-weight: bold;
color: #999;
}
.wl-label-open,
.wl-input-link {
.wl-icon {
padding-left: 5px;
padding-right: 2px;
vertical-align: middle;
// @include wl-icon-size(inherit);
}
}
.wl-img-default {
width: 0.9em ;
padding: 0 1px 1px;
}
.wl-label-close {
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 4px;
font-size: small;
}
/****************************************************************************/
.wl-textit {
font-style: italic;
}
.wl-textit-open {
font-style: italic;
color: #999;
}
.wl-textit-close {
font-style: italic;
color: #999;
}
}
@@ -183,3 +183,61 @@
}
}
}
.toggle-wrapper {
width: 200px;
height: 24px;
}
.toggle-switch {
position: relative;
height: 100%;
width: 100%;
background-color: @toggle-switch-bg;
border-radius: @btn-border-radius-base;
}
.toggle-switch-label {
position: relative;
display: block;
font-weight: normal;
z-index: 2;
float: left;
width: 50%;
height: 100%;
line-height: 24px;
text-align: center;
margin-bottom: 0;
cursor: pointer;
user-select: none;
transition: color 0.12s ease-out;
}
.toggle-switch-input {
position: absolute;
opacity: 0;
}
.toggle-switch-input:checked + .toggle-switch-label {
color: #fff;
font-weight: bold;
}
.toggle-switch-selection {
display: block;
position: absolute;
z-index: 1;
top: 2px;
left: 2px;
right: 2px;
width: calc(~"50% - 2px");
height: calc(~"100% - 4px");
background: @toggle-switch-highlight-color;
border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base;
transition: transform 0.12s ease-out, border-radius 0.12s ease-out;
}
.toggle-switch-input:checked:nth-child(4) ~ .toggle-switch-selection {
transform: translate(100%);
border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
}
@@ -7,6 +7,11 @@
display: flex;
align-items: center;
}
.error-container.full-height when (@is-overleaf = true) {
margin-top: -(@header-height + @content-margin-vertical) / 2;
}
.error-figure {
display: none;
flex: 0 0 50%;
@@ -15,6 +20,11 @@
display: block;
}
}
.error-figure when (@is-overleaf = true) {
display: none;
}
.error-figure-500 {
&::before {
content: '';
@@ -56,6 +66,11 @@
flex: 0 1 50%;
padding: @line-height-computed * 2;
}
.error-details when (@is-overleaf = true) {
flex-grow: 1;
}
.error-status {
font-family: @font-family-serif;
margin-bottom: (@line-height-computed / 4);
@@ -68,7 +83,7 @@
color: @gray;
margin-bottom: @line-height-computed * 2;
}
.error-btn {
.error-btn when (@is-overleaf = false) {
color: @navbar-default-link-color;
border: 2px solid @navbar-default-link-color;
border-radius: @border-radius-base;
@@ -83,4 +98,13 @@
background-color: @navbar-default-link-hover-bg;
border: 2px solid @navbar-default-link-hover-color;
}
}
.error-btn when (@is-overleaf = true) {
.btn;
.btn-primary;
display: block;
@media (min-width: @screen-sm-min) {
display: inline-block;
}
}
@@ -1,5 +1,6 @@
.v1-import-title {
text-align: center;
margin-top: @line-height-computed / 2;
}
.v1-import-row {
@@ -8,9 +9,6 @@
}
.v1-import-col {
flex-basis: 50%;
flex-grow: 0;
flex-shrink: 0;
padding-left: 15px;
padding-right: 15px;
}
@@ -33,9 +31,8 @@
}
.v1-import-warning {
text-align: center;
color: #fdce02;
font-size: 14em;
color: #4B7FD1;
font-size: 10em;
line-height: 1em;
}
@@ -1,3 +1,8 @@
.recurly-hosted-field {
&:extend(.form-control);
}
.recurly {
display: block;
position: relative;
@@ -35,7 +35,7 @@
color: white;
padding: 0;
line-height: 1;
&:hover, &:active {
&:hover, &:active, &:focus {
color: white;
}
}
@@ -71,4 +71,8 @@
}
}
}
}
.capitalised {
text-transform:capitalize;
}
@@ -1,14 +1,23 @@
.translations-message {
padding: (@line-height-computed / 4) (@line-height-computed / 2);
background-color: @state-warning-bg;
color: #333;
border-bottom: 1px solid @common-border-color;
.system-message;
text-align:center;
img {
vertical-align: text-bottom;
margin-bottom: -1px;
}
}
}
.translations-message when (@is-overleaf = true) {
.close {
color: #FFF;
opacity: 1;
text-shadow: none;
}
a {
color: #FFF;
&:hover,
&:focus {
color: #FFF;
}
}
}
@@ -309,8 +309,9 @@ input[type="checkbox"],
.has-warning {
.form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg);
}
.has-error {
.has-external-error {
.form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);
color:@red;
}
.form-control.ng-dirty.ng-invalid:not(:focus) {
@@ -576,10 +576,3 @@
padding: @navbar-default-padding-v 0;
}
}
.system-messages {
padding-top: @header-height;
position: absolute;
width: 100%;
z-index: 1;
}
@@ -932,6 +932,14 @@
@synctex-controls-z-index : 3;
@synctex-controls-padding : 0 2px;
// Editor toolbar
@editor-toolbar-height : 32px;
@editor-toolbar-bg : #fff;
// Toggle switch
@toggle-switch-bg : @gray-lightest;
@toggle-switch-highlight-color : @brand-primary;
// Chat
@chat-bg : transparent;
@chat-message-color : @text-color;
@@ -959,4 +967,9 @@
@tag-max-width : 150px;
@tag-bg-hover-color : darken(@label-default-bg, 10%);
@tag-top-adjustment : -2px;
@labels-font-size : 75%;
@labels-font-size : 75%;
// System messages
@sys-msg-background : @state-warning-bg;
@sys-msg-color : #333;
@sys-msg-border : 1px solid @common-border-color;

Some files were not shown because too many files have changed in this diff Show More