Merge branch 'master' into pr-ol-beta-editor-styling

This commit is contained in:
James Allen 2017-12-04 10:01:27 +00:00
commit f21870aac2
217 changed files with 10366 additions and 1351 deletions

View file

@ -38,7 +38,7 @@ data/*
app.js
app/js/*
test/UnitTests/js/*
test/unit/js/*
test/smoke/js/*
test/acceptance/js/*
cookies.txt
@ -73,3 +73,4 @@ Gemfile.lock
app/views/external
/modules/
docker-shared.yml

View file

@ -39,6 +39,7 @@ module.exports = (grunt) ->
app:
options:
index: "app.js"
logFile: "app.log"
watch:
coffee:
@ -130,9 +131,9 @@ module.exports = (grunt) ->
unit_tests:
expand: true,
flatten: false,
cwd: 'test/UnitTests/coffee',
cwd: 'test/unit/coffee',
src: ['**/*.coffee'],
dest: 'test/UnitTests/js/',
dest: 'test/unit/js/',
ext: '.js'
acceptance_tests:
@ -195,6 +196,7 @@ module.exports = (grunt) ->
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
"pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
"ace": "#{PackageVersions.lib('ace')}"
"fineuploader": "libs/#{PackageVersions.lib('fineuploader')}"
shim:
"pdfjs-dist/build/pdf":
deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"]
@ -219,12 +221,12 @@ module.exports = (grunt) ->
clean:
app: ["app/js"]
unit_tests: ["test/UnitTests/js"]
unit_tests: ["test/unit/js"]
acceptance_tests: ["test/acceptance/js"]
mochaTest:
unit:
src: ["test/UnitTests/js/#{grunt.option('feature') or '**'}/*.js"]
src: ["test/unit/js/#{grunt.option('feature') or '**'}/*.js"]
options:
reporter: grunt.option('reporter') or 'spec'
grep: grunt.option("grep")
@ -407,7 +409,7 @@ module.exports = (grunt) ->
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests']
grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests']
grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests', 'compile:acceptance_tests']
grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css']
grunt.registerTask 'quickcompile:coffee', 'Compiles only changed coffee files',['newer:coffee']

View file

@ -42,7 +42,7 @@ pipeline {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/tpr-webmodule'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/tpr-webmodule.git ']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]])
checkout([$class: 'GitSCM', branches: [[name: '*/sk-unlisted-projects']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]])
checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-account-merge'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-account-merge.git']]])
}
@ -70,6 +70,19 @@ pipeline {
sh 'ls -l node_modules/.bin'
}
}
stage('Unit Tests') {
steps {
sh 'make clean install' // Removes js files, so do before compile
sh 'make test_unit MOCHA_ARGS="--reporter=tap"'
}
}
stage('Acceptance Tests') {
steps {
sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"'
}
}
stage('Compile') {
agent {
@ -79,7 +92,7 @@ pipeline {
}
}
steps {
sh 'node_modules/.bin/grunt compile --verbose'
sh 'node_modules/.bin/grunt compile compile:tests --verbose'
// replace the build number placeholder for sentry
sh 'node_modules/.bin/grunt version'
}
@ -109,25 +122,6 @@ pipeline {
}
}
stage('Unit Test') {
agent {
docker {
image 'node:6.9.5'
reuseNode true
}
}
steps {
sh 'env NODE_ENV=development ./node_modules/.bin/grunt test:unit --reporter=tap'
}
}
stage('Acceptance Tests') {
steps {
sh 'docker pull sharelatex/acceptance-test-runner'
sh 'docker run --rm -v $(pwd):/app --env SHARELATEX_ALLOW_PUBLIC_ACCESS=true sharelatex/acceptance-test-runner'
}
}
stage('Package') {
steps {
sh 'rm -rf ./node_modules/grunt*'

77
services/web/Makefile Normal file
View file

@ -0,0 +1,77 @@
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm npm -q
BUILD_NUMBER ?= local
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
PROJECT_NAME = web
all: install test
@echo "Run:"
@echo " make install to set up the project dependencies (in docker)"
@echo " make test to run all the tests for the project (in docker)"
add: docker-shared.yml
$(NPM) install --save ${P}
add_dev: docker-shared.yml
$(NPM) install --save-dev ${P}
install: docker-shared.yml
$(NPM) install
clean:
rm -f app.js
rm -rf app/js
rm -rf test/unit/js
rm -rf test/acceptance/js
for dir in modules/*; \
do \
rm -f $$dir/index.js; \
rm -rf $$dir/app/js; \
rm -rf $$dir/test/unit/js; \
rm -rf $$dir/test/acceptance/js; \
done
# Regenerate docker-shared.yml - not stictly a 'clean',
# but lets `make clean install` work nicely
bin/generate_volumes_file
# Deletes node_modules volume
docker-compose down --volumes
# Need regenerating if you change the web modules you have installed
docker-shared.yml:
bin/generate_volumes_file
test: test_unit test_acceptance
test_unit: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS}
test_acceptance: test_acceptance_app test_acceptance_modules
test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service
test_acceptance_app_start_service: test_acceptance_app_stop_service docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance
test_acceptance_app_stop_service: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo
test_acceptance_app_run: docker-shared.yml
docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS}
test_acceptance_modules: docker-shared.yml
# Break and error on any module failure
set -e; \
for dir in modules/*; \
do \
if [ -e $$dir/Makefile ]; then \
(make test_acceptance_module MODULE=$$dir) \
fi \
done
test_acceptance_module: docker-shared.yml
cd $(MODULE) && make test_acceptance
.PHONY:
all add install update test test_unit test_acceptance \
test_acceptance_start_service test_acceptance_stop_service \
test_acceptance_run

View file

@ -17,6 +17,69 @@ web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related
Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`).
New Docker-based build process
------------------------------
Note that the Grunt workflow from above should still work, but we are transitioning to a
Docker based testing workflow, which is documented below:
### Running the app
The app runs natively using npm and Node on the local system:
```
$ npm install
$ npm run start
```
*Ideally the app would run in Docker like the tests below, but with host networking not supported in OS X, we need to run it natively until all services are Dockerised.*
### Unit Tests
The test suites run in Docker.
Unit tests can be run in the `test_unit` container defined in `docker-compose.tests.yml`.
The makefile contains a short cut to run these:
```
make install # Only needs running once, or when npm packages are updated
make unit_test
```
During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI:
```
make unit_test MOCHA_ARGS='--grep=AuthorizationManager'
```
### Acceptance Tests
Acceptance tests are run against a live service, which runs in the `acceptance_test` container defined in `docker-compose.tests.yml`.
To run the tests out-of-the-box, the makefile defines:
```
make install # Only needs running once, or when npm packages are updated
make acceptance_test
```
However, during development it is often useful to leave the service running for rapid iteration on the acceptance tests. This can be done with:
```
make acceptance_test_start_service
make acceptance_test_run # Run as many times as needed during development
make acceptance_test_stop_service
```
`make acceptance_test` just runs these three commands in sequence.
During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI:
```
make acceptance_test_run MOCHA_ARGS='--grep=AuthorizationManager'
```
Unit test status
----------------

View file

@ -204,6 +204,48 @@ module.exports = DocumentUpdaterHandler =
logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
updateProjectStructure : (project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback = (error) ->)->
return callback() if !settings.apis.project_history?.enabled
docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', oldDocs, newDocs)
fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', oldFiles, newFiles)
timer = new metrics.Timer("set-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
body =
url: url
json: { docUpdates, fileUpdates, userId }
return callback() if (docUpdates.length + fileUpdates.length) < 1
request.post body, (error, res, body)->
timer.done()
if error?
logger.error {error, url, project_id}, "error update project structure in doc updater"
callback(error)
else if res.statusCode >= 200 and res.statusCode < 300
logger.error {project_id}, "updated project structure in doc updater"
callback(null)
else
logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
_getRenameUpdates: (entityType, oldEntities, newEntities) ->
updates = []
for oldEntity in oldEntities
id = oldEntity[entityType]._id
newEntity = _.find newEntities, (newEntity) ->
newEntity[entityType]._id.toString() == id.toString()
if newEntity.path != oldEntity.path
updates.push
id: id
pathname: oldEntity.path
newPathname: newEntity.path
updates
PENDINGUPDATESKEY = "PendingUpdates"
DOCLINESKEY = "doclines"
DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates"

View file

@ -150,11 +150,11 @@ module.exports = EditorController =
logger.log project_id:project_id, "recived message to delete project"
ProjectDeleter.deleteProject project_id, callback
renameEntity: (project_id, entity_id, entityType, newName, callback)->
renameEntity: (project_id, entity_id, entityType, newName, userId, callback)->
newName = sanitize.escape(newName)
Metrics.inc "editor.rename-entity"
logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, ->
ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, userId, (err) ->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity"
return callback(err)
@ -162,13 +162,13 @@ module.exports = EditorController =
EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
callback?()
moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
moveEntity: (project_id, entity_id, folder_id, entityType, userId, callback)->
Metrics.inc "editor.move-entity"
LockManager.getLock project_id, (err)->
if err?
logger.err err:err, project_id:project_id, "could not get lock for move entity"
return callback(err)
ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, userId, =>
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity"
return callback(err)

View file

@ -12,6 +12,7 @@ CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = EditorHttpController =
joinProject: (req, res, next) ->
@ -112,9 +113,10 @@ module.exports = EditorHttpController =
entity_id = req.params.entity_id
entity_type = req.params.entity_type
name = req.body.name
user_id = AuthenticationController.getLoggedInUserId(req)
if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400
EditorController.renameEntity project_id, entity_id, entity_type, name, (error) ->
EditorController.renameEntity project_id, entity_id, entity_type, name, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
@ -123,7 +125,8 @@ module.exports = EditorHttpController =
entity_id = req.params.entity_id
entity_type = req.params.entity_type
folder_id = req.body.folder_id
EditorController.moveEntity project_id, entity_id, folder_id, entity_type, (error) ->
user_id = AuthenticationController.getLoggedInUserId(req)
EditorController.moveEntity project_id, entity_id, folder_id, entity_type, user_id, (error) ->
return next(error) if error?
res.sendStatus 204

View file

@ -2,6 +2,7 @@ logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
module.exports = HistoryController =
initializeProject: (callback = (error, history_id) ->) ->
@ -27,11 +28,22 @@ module.exports = HistoryController =
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error
selectHistoryApi: (req, res, next = (error) ->) ->
project_id = req.params?.Project_id
# find out which type of history service this project uses
ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err?
if project?.overleaf?.history?.display
req.useProjectHistory = true
else
req.useProjectHistory = false
next()
proxyToHistoryApi: (req, res, next = (error) ->) ->
user_id = AuthenticationController.getLoggedInUserId req
url = HistoryController.buildHistoryServiceUrl() + req.url
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
logger.log url: url, "proxying to track-changes api"
logger.log url: url, "proxying to history api"
getReq = request(
url: url
method: req.method
@ -40,11 +52,13 @@ module.exports = HistoryController =
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"
logger.error url: url, err: error, "history API error"
next(error)
buildHistoryServiceUrl: () ->
if settings.apis.project_history?.enabled
buildHistoryServiceUrl: (useProjectHistory) ->
# choose a history service, either document-level (trackchanges)
# or project-level (project_history)
if settings.apis.project_history?.enabled && useProjectHistory
return settings.apis.project_history.url
else
return settings.apis.trackchanges.url

View file

@ -0,0 +1,28 @@
EditorRealTimeController = require "../Editor/EditorRealTimeController"
MetaHandler = require './MetaHandler'
logger = require 'logger-sharelatex'
module.exports = MetaController =
getMetadata: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "getting all labels for project"
MetaHandler.getAllMetaForProject project_id, (err, projectMeta) ->
if err?
logger.err {project_id, err}, "[MetaController] error getting all labels from project"
return next err
res.json {projectId: project_id, projectMeta: projectMeta}
broadcastMetadataForDoc: (req, res, next) ->
project_id = req.params.project_id
doc_id = req.params.doc_id
logger.log {project_id, doc_id}, "getting labels for doc"
MetaHandler.getMetaForDoc project_id, doc_id, (err, docMeta) ->
if err?
logger.err {project_id, doc_id, err}, "[MetaController] error getting labels from doc"
return next err
EditorRealTimeController.emitToRoom project_id, 'broadcastDocMeta', {
docId: doc_id, meta: docMeta
}
res.sendStatus 200

View file

@ -0,0 +1,66 @@
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
DocumentUpdaterHandler = require '../DocumentUpdater/DocumentUpdaterHandler'
packageMapping = require "./packageMapping"
module.exports = MetaHandler =
labelRegex: () ->
/\\label{(.{0,80}?)}/g
usepackageRegex: () ->
/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/g
ReqPackageRegex: () ->
/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/g
getAllMetaForProject: (projectId, callback=(err, projectMeta)->) ->
DocumentUpdaterHandler.flushProjectToMongo projectId, (err) ->
if err?
return callback err
ProjectEntityHandler.getAllDocs projectId, (err, docs) ->
if err?
return callback err
projectMeta = MetaHandler.extractMetaFromProjectDocs docs
callback null, projectMeta
getMetaForDoc: (projectId, docId, callback=(err, docMeta)->) ->
DocumentUpdaterHandler.flushDocToMongo projectId, docId, (err) ->
if err?
return callback err
ProjectEntityHandler.getDoc projectId, docId, (err, lines) ->
if err?
return callback err
docMeta = MetaHandler.extractMetaFromDoc lines
callback null, docMeta
extractMetaFromDoc: (lines) ->
docMeta = {labels: [], packages: {}}
packages = []
label_re = MetaHandler.labelRegex()
package_re = MetaHandler.usepackageRegex()
req_package_re = MetaHandler.ReqPackageRegex()
for line in lines
while labelMatch = label_re.exec line
if label = labelMatch[1]
docMeta.labels.push label
while packageMatch = package_re.exec line
if messy = packageMatch[1]
for pkg in messy.split ','
if clean = pkg.trim()
packages.push clean
while packageMatch = req_package_re.exec line
if messy = packageMatch[1]
for pkg in messy.split ','
if clean = pkg.trim()
packages.push clean
for pkg in packages
if packageMapping[pkg]?
docMeta.packages[pkg] = packageMapping[pkg]
return docMeta
extractMetaFromProjectDocs: (projectDocs) ->
projectMeta = {}
for _path, doc of projectDocs
projectMeta[doc._id] = MetaHandler.extractMetaFromDoc doc.lines
return projectMeta

File diff suppressed because one or more lines are too long

View file

@ -24,13 +24,17 @@ AnalyticsManager = require "../Analytics/AnalyticsManager"
Sources = require "../Authorization/Sources"
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
Modules = require '../../infrastructure/Modules'
crypto = require 'crypto'
module.exports = ProjectController =
_isInPercentageRollout: (objectId, percentage) ->
if Settings.bypassPercentageRollouts = true
_isInPercentageRollout: (rolloutName, objectId, percentage) ->
if Settings.bypassPercentageRollouts == true
return true
counter = parseInt(objectId.toString().substring(18, 24), 16)
data = "#{rolloutName}:#{objectId.toString()}"
md5hash = crypto.createHash('md5').update(data).digest('hex')
counter = parseInt(md5hash.slice(26, 32), 16)
return (counter % 100) < percentage
updateProjectSettings: (req, res, next) ->
@ -145,6 +149,11 @@ module.exports = ProjectController =
NotificationsHandler.getUserNotifications user_id, cb
projects: (cb)->
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb
v1Projects: (cb) ->
Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) ->
if error? and error.message == 'No V1 connection'
return cb(null, projects: [], tags: [], noConnection: true)
return cb(error, projects[0]) # hooks.fire returns an array of results, only need first
hasSubscription: (cb)->
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
user: (cb) ->
@ -154,11 +163,12 @@ module.exports = ProjectController =
logger.err err:err, "error getting data for project list page"
return next(err)
logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0]
v1Tags = results.v1Projects?.tags or []
tags = results.tags[0].concat(v1Tags)
notifications = require("underscore").map results.notifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
projects = ProjectController._buildProjectList results.projects
projects = ProjectController._buildProjectList results.projects, results.v1Projects?.projects
user = results.user
ProjectController._injectProjectOwners projects, (error, projects) ->
return next(error) if error?
@ -170,6 +180,8 @@ module.exports = ProjectController =
notifications: notifications or []
user: user
hasSubscription: results.hasSubscription[0]
isShowingV1Projects: results.v1Projects?
noV1Connection: results.v1Projects?.noConnection
}
if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key?
@ -222,44 +234,6 @@ module.exports = ProjectController =
#don't need to wait for this to complete
ProjectUpdateHandler.markAsOpened project_id, ->
cb()
showTrackChangesOnboarding: (cb) ->
cb = underscore.once(cb)
if !user_id?
return cb()
timestamp = user_id.toString().substring(0,8)
userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 )
if userSignupDate > new Date("2017-03-09") # 8th March
# Don't show for users who registered after it was released
return cb(null, false)
timeout = setTimeout cb, 500
AnalyticsManager.getLastOccurance user_id, "shown-track-changes-onboarding-2", (error, event) ->
clearTimeout timeout
if error?
return cb(null, false)
else if event?
return cb(null, false)
else
logger.log { user_id, event }, "track changes onboarding not shown yet to this user"
return cb(null, true)
showPerUserTCNotice: (cb) ->
cb = underscore.once(cb)
if !user_id?
return cb()
timestamp = user_id.toString().substring(0,8)
userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 )
if userSignupDate > new Date("2017-08-09")
# Don't show for users who registered after it was released
return cb(null, false)
timeout = setTimeout cb, 500
AnalyticsManager.getLastOccurance user_id, "shown-per-user-tc-notice", (error, event) ->
clearTimeout timeout
if error?
return cb(null, false)
else if event?
return cb(null, false)
else
logger.log { user_id, event }, "per user track changes notice not shown yet to this user"
return cb(null, true)
isTokenMember: (cb) ->
cb = underscore.once(cb)
if !user_id?
@ -267,14 +241,18 @@ module.exports = ProjectController =
CollaboratorsHandler.userIsTokenMember user_id, project_id, cb
showAutoCompileOnboarding: (cb) ->
cb = underscore.once(cb)
# Force autocompile rollout if query param set
if req.query?.ac == 't'
return cb(null, { enabled: true, showOnboarding: true })
if !user_id?
return cb()
# Extract data from user's ObjectId
timestamp = parseInt(user_id.toString().substring(0, 8), 16)
rolloutPercentage = 60 # Percentage of users to roll out to
if !ProjectController._isInPercentageRollout(user_id, rolloutPercentage)
rolloutPercentage = 20 # Percentage of users to roll out to
if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage)
# Don't show if user is not part of roll out
return cb(null, { enabled: false, showOnboarding: false })
userSignupDate = new Date(timestamp * 1000)
@ -282,7 +260,7 @@ module.exports = ProjectController =
# Don't show for users who registered after it was released
return cb(null, { enabled: true, showOnboarding: false })
timeout = setTimeout cb, 500
AnalyticsManager.getLastOccurance user_id, "shown-autocompile-onboarding", (error, event) ->
AnalyticsManager.getLastOccurance user_id, "shown-autocompile-onboarding-2", (error, event) ->
clearTimeout timeout
if error?
return cb(null, { enabled: true, showOnboarding: false })
@ -291,6 +269,23 @@ module.exports = ProjectController =
else
logger.log { user_id, event }, "autocompile onboarding not shown yet to this user"
return cb(null, { enabled: true, showOnboarding: true })
couldShowLinkSharingOnboarding: (cb) ->
cb = underscore.once(cb)
if !user_id?
return cb()
# Extract data from user's ObjectId
timestamp = parseInt(user_id.toString().substring(0, 8), 16)
userSignupDate = new Date(timestamp * 1000)
if userSignupDate > new Date("2017-11-13")
# Don't show for users who registered after it was released
return cb(null, false)
timeout = setTimeout cb, 500
AnalyticsManager.getLastOccurance user_id, "shown-linksharing-onboarding", (error, event) ->
clearTimeout timeout
if error? || event?
return cb(null, false)
else
return cb(null, true)
}, (err, results)->
if err?
logger.err err:err, "error getting details for project page"
@ -298,14 +293,13 @@ module.exports = ProjectController =
project = results.project
user = results.user
subscription = results.subscription
{ showTrackChangesOnboarding, showPerUserTCNotice, showAutoCompileOnboarding } = results
{ showAutoCompileOnboarding } = results
daysSinceLastUpdated = (new Date() - project.lastUpdated) / 86400000
logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor"
token = TokenAccessHandler.getRequestToken(req, project_id)
isTokenMember = results.isTokenMember
enableTokenAccessUI = ProjectController._isInPercentageRollout(project.owner_ref, 0)
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel)->
return next(error) if error?
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
@ -344,8 +338,6 @@ module.exports = ProjectController =
syntaxValidation: user.ace.syntaxValidation
}
trackChangesState: project.track_changes
showTrackChangesOnboarding: !!showTrackChangesOnboarding
showPerUserTCNotice: !!showPerUserTCNotice
autoCompileEnabled: !!showAutoCompileOnboarding?.enabled
showAutoCompileOnboarding: !!showAutoCompileOnboarding?.showOnboarding
privilegeLevel: privilegeLevel
@ -356,10 +348,10 @@ module.exports = ProjectController =
languages: Settings.languages
themes: THEME_LIST
maxDocLength: Settings.max_doc_length
enableTokenAccessUI: enableTokenAccessUI
showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding
timer.done()
_buildProjectList: (allProjects)->
_buildProjectList: (allProjects, v1Projects = [])->
{owned, readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly} = allProjects
projects = []
for project in owned
@ -369,6 +361,8 @@ module.exports = ProjectController =
projects.push ProjectController._buildProjectViewModel(project, "readWrite", Sources.INVITE)
for project in readOnly
projects.push ProjectController._buildProjectViewModel(project, "readOnly", Sources.INVITE)
for project in v1Projects
projects.push ProjectController._buildV1ProjectViewModel(project)
# Token-access
# Only add these projects if they're not already present, this gives us cascading access
# from 'owner' => 'token-read-only'
@ -393,9 +387,25 @@ module.exports = ProjectController =
archived: !!project.archived
owner_ref: project.owner_ref
tokens: project.tokens
isV1Project: false
}
return model
_buildV1ProjectViewModel: (project) ->
{
id: project.id
name: project.title
lastUpdated: new Date(project.updated_at * 1000) # Convert from epoch
accessLevel: if project.owner?.user_is_owner then "owner" else "readOnly"
archived: project.removed || project.archived
owner: {
# Unlisted V1 projects don't have an owner, so just show N/A
first_name: if project.owner then project.owner.name else 'N/A'
last_name: ''
}
isV1Project: true
}
_injectProjectOwners: (projects, callback = (error, projects) ->) ->
users = {}
for project in projects

View file

@ -355,47 +355,50 @@ module.exports = ProjectEntityHandler =
else
callback()
moveEntity: (project_id, entity_id, folder_id, entityType, callback = (error) ->)->
moveEntity: (project_id, entity_id, destFolderId, entityType, userId, callback = (error) ->)->
self = @
destinationFolder_id = folder_id
logger.log entityType:entityType, entity_id:entity_id, project_id:project_id, folder_id:folder_id, "moving entity"
logger.log {entityType, entity_id, project_id, destFolderId}, "moving entity"
if !entityType?
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
logger.err {err: "No entityType set", project_id, entity_id}
return callback("No entityType set")
entityType = entityType.toLowerCase()
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
return callback(err) if err?
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)->
projectLocator.findElement {project, element_id: entity_id, type: entityType}, (err, entity, entityPath)->
return callback(err) if err?
if entityType.match(/folder/)
ensureFolderIsNotMovedIntoChild = (callback = (error) ->) ->
projectLocator.findElement {project: project, element_id: folder_id, type:"folder"}, (err, destEntity, destPath) ->
logger.log destPath: destPath.fileSystem, folderPath: path.fileSystem, "checking folder is not moving into child folder"
if (destPath.fileSystem.slice(0, path.fileSystem.length) == path.fileSystem)
logger.log "destination is a child folder, aborting"
callback(new Error("destination folder is a child folder of me"))
else
callback()
else
ensureFolderIsNotMovedIntoChild = (callback = () ->) -> callback()
ensureFolderIsNotMovedIntoChild (error) ->
self._checkValidMove project, entityType, entityPath, destFolderId, (error) ->
return callback(error) if error?
self._removeElementFromMongoArray Project, project_id, path.mongo, (err)->
return callback(err) if err?
# We've updated the project structure by removing the element, so must refresh it.
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
self.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) =>
return callback(error) if error?
self._removeElementFromMongoArray Project, project_id, entityPath.mongo, (err, newProject)->
return callback(err) if err?
ProjectEntityHandler._putElement project, destinationFolder_id, entity, entityType, (err, result)->
self._putElement newProject, destFolderId, entity, entityType, (err, result, newProject)->
return callback(err) if err?
opts =
project_id:project_id
project_name:project.name
startPath:path.fileSystem
endPath:result.path.fileSystem,
rev:entity.rev
tpdsUpdateSender.moveEntity opts, callback
project_id: project_id
project_name: project.name
startPath: entityPath.fileSystem
endPath: result.path.fileSystem,
rev: entity.rev
tpdsUpdateSender.moveEntity opts
self.getAllEntitiesFromProject newProject, (error, newDocs, newFiles
) =>
return callback(error) if error?
documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
documentUpdaterHandler.updateProjectStructure project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback
_checkValidMove: (project, entityType, entityPath, destFolderId, callback = (error) ->) ->
return callback() if !entityType.match(/folder/)
projectLocator.findElement { project, element_id: destFolderId, type:"folder"}, (err, destEntity, destFolderPath) ->
return callback(err) if err?
logger.log destFolderPath: destFolderPath.fileSystem, folderPath: entityPath.fileSystem, "checking folder is not moving into child folder"
isNestedFolder = destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) == entityPath.fileSystem
if isNestedFolder
callback(new Error("destination folder is a child folder of me"))
else
callback()
deleteEntity: (project_id, entity_id, entityType, callback = (error) ->)->
self = @
@ -417,25 +420,30 @@ module.exports = ProjectEntityHandler =
callback null
renameEntity: (project_id, entity_id, entityType, newName, callback)->
renameEntity: (project_id, entity_id, entityType, newName, userId, callback)->
logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType))
if !entityType?
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path, folder)=>
if err?
return callback err
conditons = {_id:project_id}
update = "$set":{}
namePath = path.mongo+".name"
update["$set"][namePath] = newName
endPath = path.fileSystem.replace(entity.name, newName)
tpdsUpdateSender.moveEntity({project_id:project_id, startPath:path.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev})
Project.update conditons, update, {}, (err)->
if callback?
callback err
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (error, project)=>
return callback(error) if error?
ProjectEntityHandler.getAllEntitiesFromProject project, (error, oldDocs, oldFiles) =>
return callback(error) if error?
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (error, entity, path)=>
return callback(error) if error?
endPath = path.fileSystem.replace(entity.name, newName)
conditions = {_id:project_id}
update = "$set":{}
namePath = path.mongo+".name"
update["$set"][namePath] = newName
tpdsUpdateSender.moveEntity({project_id:project_id, startPath:path.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev})
Project.findOneAndUpdate conditions, update, { "new": true}, (error, newProject) ->
return callback(error) if error?
ProjectEntityHandler.getAllEntitiesFromProject newProject, (error, newDocs, newFiles) =>
return callback(error) if error?
documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
documentUpdaterHandler.updateProjectStructure project_id, userId, oldDocs, newDocs, oldFiles, newFiles, callback
_cleanUpEntity: (project, entity, entityType, callback = (error) ->) ->
if(entityType.indexOf("file") != -1)
@ -487,17 +495,15 @@ module.exports = ProjectEntityHandler =
async.series jobs, callback
_removeElementFromMongoArray : (model, model_id, path, callback)->
conditons = {_id:model_id}
_removeElementFromMongoArray : (model, model_id, path, callback = (err, project) ->)->
conditions = {_id:model_id}
update = {"$unset":{}}
update["$unset"][path] = 1
model.update conditons, update, {}, (err)->
model.update conditions, update, {}, (err)->
pullUpdate = {"$pull":{}}
nonArrayPath = path.slice(0, path.lastIndexOf("."))
pullUpdate["$pull"][nonArrayPath] = null
model.update conditons, pullUpdate, {}, (err)->
if callback?
callback(err)
model.findOneAndUpdate conditions, pullUpdate, {"new": true}, callback
_insertDeletedDocReference: (project_id, doc, callback = (error) ->) ->
Project.update {
@ -534,8 +540,7 @@ module.exports = ProjectEntityHandler =
countFolder project.rootFolder[0], callback
_putElement: (project, folder_id, element, type, callback = (err, path)->)->
_putElement: (project, folder_id, element, type, callback = (err, path, project)->)->
sanitizeTypeOfElement = (elementType)->
lastChar = elementType.slice -1
if lastChar != "s"
@ -576,11 +581,11 @@ module.exports = ProjectEntityHandler =
update = "$push":{}
update["$push"][mongopath] = element
logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, mongopath:mongopath, "adding element to project"
Project.update conditions, update, {}, (err)->
Project.findOneAndUpdate conditions, update, {"new": true}, (err, project)->
if err?
logger.err err: err, project_id: project._id, 'error saving in putElement project'
return callback(err)
callback(err, {path:newPath})
callback(err, {path:newPath}, project)
confirmFolder = (project, folder_id, callback)->

View file

@ -1,5 +1,7 @@
logger = require('logger-sharelatex')
Settings = require('settings-sharelatex')
_ = require('underscore')
Features = require "../../infrastructure/Features"
Path = require "path"
fs = require "fs"
@ -20,12 +22,11 @@ module.exports = HomeController =
HomeController.home(req, res)
home: (req, res)->
if homepageExists
if Features.hasFeature('homepage') and homepageExists
res.render 'external/home'
else
res.redirect "/login"
externalPage: (page, title) ->
return (req, res, next = (error) ->) ->
path = Path.resolve(__dirname + "/../../../views/external/#{page}.pug")

View file

@ -97,20 +97,39 @@ module.exports = SubscriptionController =
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
RecurlyWrapper.getSubscription subscription.recurlySubscription_id,
includeAccount: true,
(err, usersSubscription)->
# always render the page, but skip the recurly link if
# we can't get it for some reason
if err?
logger.err {err, userId: user._id}, "error getting billing details link from recurly, proceeding"
hostedLoginToken = usersSubscription?.account?.hosted_login_token
recurlySubdomain = Settings?.apis?.recurly?.subdomain
if err? || !hostedLoginToken || !recurlySubdomain
billingDetailsLink = null
else
billingDetailsLink = [
"https://",
recurlySubdomain,
".recurly.com/account/billing_info/edit?ht=",
hostedLoginToken
].join("")
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log {user, subscription, hasSubOrIsGroupMember, groupSubscriptions, billingDetailsLink}, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
billingDetailsLink: billingDetailsLink
userCustomSubscriptionPage: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
@ -124,31 +143,6 @@ module.exports = SubscriptionController =
title: "your_subscription"
subscription: subscription
editBillingDetailsPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "update_billing_details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
updateBillingDetails: (req, res, next) ->
res.redirect "/user/subscription?saved_billing_details=true"
createSubscription: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
recurly_token_id = req.body.recurly_token_id

View file

@ -15,8 +15,6 @@ module.exports =
webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
webRouter.post '/user/subscription/billing-details/update', AuthenticationController.requireLogin(), SubscriptionController.updateBillingDetails
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription

View file

@ -52,6 +52,7 @@ module.exports = TokenAccessController =
else
logger.log {token, projectId: project._id},
"[TokenAccess] deny anonymous read-and-write token access"
AuthenticationController._setRedirectInSession(req)
return res.redirect('/restricted')
if project.owner_ref.toString() == userId
logger.log {userId, projectId: project._id},

View file

@ -51,7 +51,6 @@ module.exports = UserController =
updateUserSettings : (req, res)->
user_id = AuthenticationController.getLoggedInUserId(req)
usingExternalAuth = settings.ldap? or settings.saml?
logger.log user_id: user_id, "updating account settings"
User.findById user_id, (err, user)->
if err? or !user?
@ -84,7 +83,7 @@ module.exports = UserController =
user.ace.syntaxValidation = req.body.syntaxValidation
user.save (err)->
newEmail = req.body.email?.trim().toLowerCase()
if !newEmail? or newEmail == user.email or usingExternalAuth
if !newEmail? or newEmail == user.email or req.externalAuthenticationSystemUsed()
# end here, don't update email
AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name})
return res.sendStatus 200

View file

@ -14,6 +14,7 @@ PackageVersions = require "./PackageVersions"
htmlEncoder = new require("node-html-encoder").Encoder("numerical")
fingerprints = {}
Path = require 'path'
Features = require "./Features"
jsPath =
if Settings.useMinifiedJs
@ -23,6 +24,7 @@ jsPath =
ace = PackageVersions.lib('ace')
pdfjs = PackageVersions.lib('pdfjs')
fineuploader = PackageVersions.lib('fineuploader')
getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}"
@ -36,6 +38,7 @@ getFileContent = (filePath)->
logger.log "Generating file fingerprints..."
pathList = [
["#{jsPath}libs/#{fineuploader}.js"]
["#{jsPath}libs/require.js"]
["#{jsPath}ide.js"]
["#{jsPath}main.js"]
@ -88,8 +91,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
publicApiRouter.use addSetContentDisposition
webRouter.use (req, res, next)->
req.externalAuthenticationSystemUsed = res.locals.externalAuthenticationSystemUsed = ->
Settings.ldap? or Settings.saml?
req.externalAuthenticationSystemUsed = Features.externalAuthenticationSystemUsed
res.locals.externalAuthenticationSystemUsed = Features.externalAuthenticationSystemUsed
req.hasFeature = res.locals.hasFeature = Features.hasFeature
next()
webRouter.use (req, res, next)->

View file

@ -0,0 +1,16 @@
Settings = require 'settings-sharelatex'
module.exports = Features =
externalAuthenticationSystemUsed: ->
Settings.ldap? or Settings.saml? or Settings.overleaf?.oauth?
hasFeature: (feature) ->
switch feature
when 'homepage'
return Settings.enableHomepage
when 'registration'
return not Features.externalAuthenticationSystemUsed()
when 'github-sync'
return Settings.enableGithubSync
else
throw new Error("unknown feature: #{feature}")

View file

@ -2,7 +2,10 @@ mongoose = require('mongoose')
Settings = require 'settings-sharelatex'
logger = require('logger-sharelatex')
mongoose.connect(Settings.mongo.url, server: poolSize: 10)
mongoose.connect(Settings.mongo.url, {
server: {poolSize: 10},
config: {autoIndex: false}
})
mongoose.connection.on 'connected', () ->
logger.log {url:Settings.mongo.url}, 'mongoose default connection open'

View file

@ -2,6 +2,7 @@ version = {
"pdfjs": "1.7.225"
"moment": "2.9.0"
"ace": "1.2.5"
"fineuploader": "5.15.4"
}
module.exports = {

View file

@ -33,8 +33,20 @@ ProjectSchema = new Schema
imageName : { type: String }
track_changes : { type: Object }
tokens :
readOnly : { type: String, index: {unique: true} }
readAndWrite : { type: String, index: {unique: true} }
readOnly : {
type: String,
index: {
unique: true,
partialFilterExpression: {'tokens.readOnly': {$exists: true}}
}
}
readAndWrite : {
type: String,
index: {
unique: true,
partialFilterExpression: {'tokens.readAndWrite': {$exists: true}}
}
}
tokenAccessReadOnly_refs : [ type:ObjectId, ref:'User' ]
tokenAccessReadAndWrite_refs : [ type:ObjectId, ref:'User' ]
overleaf :
@ -61,12 +73,14 @@ applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (f
_.each folder.folders, (folder)->
applyToAllFilesRecursivly(folder, fun)
ProjectSchema.methods.getSafeProjectName = ->
safeProjectName = this.name.replace(new RegExp("\\W", "g"), '_')
return sanitize.escape(safeProjectName)
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
conn = mongoose.createConnection(Settings.mongo.url, {
server: {poolSize: Settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
Project = conn.model('Project', ProjectSchema)

View file

@ -30,9 +30,10 @@ ProjectInviteSchema = new Schema(
}
)
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
conn = mongoose.createConnection(Settings.mongo.url, {
server: {poolSize: Settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)

View file

@ -25,11 +25,13 @@ SubscriptionSchema.statics.findAndModify = (query, update, callback)->
this.update query, update, ->
self.findOne query, callback
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
conn = mongoose.createConnection(Settings.mongo.url, {
server: {poolSize: Settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
Subscription = conn.model('Subscription', SubscriptionSchema)
mongoose.model 'Subscription', SubscriptionSchema
exports.Subscription = Subscription
exports.SubscriptionSchema = SubscriptionSchema
exports.SubscriptionSchema = SubscriptionSchema

View file

@ -6,7 +6,11 @@ ObjectId = Schema.ObjectId
SystemMessageSchema = new Schema
content : type: String, default:''
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
conn = mongoose.createConnection(Settings.mongo.url, {
server: {poolSize: Settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
exports.SystemMessage = conn.model('SystemMessage', SystemMessageSchema)

View file

@ -64,7 +64,10 @@ UserSchema = new Schema
accessToken: { type: String }
refreshToken: { type: String }
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10)
conn = mongoose.createConnection(Settings.mongo.url, {
server: {poolSize: Settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
User = conn.model('User', UserSchema)

View file

@ -9,7 +9,10 @@ UserStubSchema = new Schema
last_name : { type : String, default : '' }
overleaf : { id: { type: Number } }
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10)
conn = mongoose.createConnection(Settings.mongo.url, {
server: {poolSize: Settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
UserStub = conn.model('UserStub', UserStubSchema)

View file

@ -43,8 +43,10 @@ SudoModeController = require('./Features/SudoMode/SudoModeController')
SudoModeMiddlewear = require('./Features/SudoMode/SudoModeMiddlewear')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
MetaController = require('./Features/Metadata/MetaController')
LabelsController = require('./Features/Labels/LabelsController')
TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
Features = require('./infrastructure/Features')
logger = require("logger-sharelatex")
_ = require("underscore")
@ -63,10 +65,9 @@ module.exports = class Router
webRouter.get '/logout', UserController.logout
webRouter.get '/restricted', AuthorizationMiddlewear.restricted
# Left as a placeholder for implementing a public register page
webRouter.get '/register', UserPagesController.registerPage
AuthenticationController.addEndpointToLoginWhitelist '/register'
if Features.hasFeature('registration')
webRouter.get '/register', UserPagesController.registerPage
AuthenticationController.addEndpointToLoginWhitelist '/register'
EditorRouter.apply(webRouter, privateApiRouter)
CollaboratorsRouter.apply(webRouter, privateApiRouter)
@ -195,13 +196,16 @@ module.exports = class Router
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
webRouter.get '/project/:project_id/metadata', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.getMetadata
webRouter.post '/project/:project_id/doc/:doc_id/metadata', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.broadcastMetadataForDoc
webRouter.get '/project/:project_id/labels', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), LabelsController.getAllLabels
webRouter.post '/project/:project_id/doc/:doc_id/labels', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), LabelsController.broadcastLabelsForDoc

View file

@ -132,7 +132,8 @@ html(itemscope, itemtype='http://schema.org/Product')
// minimal requirejs configuration (can be extended/overridden)
window.requirejs = {
"paths" : {
"moment": "libs/#{lib('moment')}"
"moment": "libs/#{lib('moment')}",
"fineuploader": "libs/#{lib('fineuploader')}"
},
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"config":{
@ -148,6 +149,7 @@ html(itemscope, itemtype='http://schema.org/Product')
)
include contact-us-modal
include v1-tooltip
include sentry

View file

@ -54,7 +54,7 @@ nav.navbar.navbar-default
// logged out
if !getSessionUser()
// register link
if !externalAuthenticationSystemUsed()
if hasFeature('registration')
li
a(href="/register") #{translate('register')}

View file

@ -20,8 +20,6 @@ block content
p.loading-screen-error(ng-if="state.error").ng-cloak
span(ng-bind-html="state.error")
include ./editor/feature-onboarding
.global-alerts(ng-cloak)
.alert.alert-danger.small(ng-if="connection.forced_disconnect")
strong #{translate("disconnected")}
@ -122,19 +120,18 @@ block requirejs
window.isTokenMember = #{!!isTokenMember};
window.maxDocLength = #{maxDocLength};
window.trackChangesState = data.trackChangesState;
window.showTrackChangesOnboarding = #{!!showTrackChangesOnboarding};
window.showPerUserTCNotice = #{!!showPerUserTCNotice};
window.autoCompileEnabled = #{!!autoCompileEnabled};
window.showAutoCompileOnboarding = #{!!showAutoCompileOnboarding}
window.showLinkSharingOnboarding = #{!!showLinkSharingOnboarding}
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.enableTokenAccessUI = #{enableTokenAccessUI}
window.requirejs = {
"paths" : {
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
"moment": "libs/#{lib('moment')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"ace": "#{lib('ace')}"
"ace": "#{lib('ace')}",
"fineuploader": "libs/#{lib('fineuploader')}"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0,
@ -168,5 +165,3 @@ block requirejs
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
src=buildJsPath('libs/require.js')
)

View file

@ -108,3 +108,17 @@ div.full-size(
p #{translate("auto_compile_onboarding_description")}
button.btn.btn-default.btn-block(ng-click="dismiss()")
| #{translate("got_it")}
#onboarding-linksharing.onboarding-linksharing.popover(
ng-controller="LinkSharingOnboardingController"
ng-if="permissions.admin && onboarding.linkSharing == 'unseen'"
ng-class="placement"
)
.popover-inner
h3.popover-title #{translate("link_sharing")}
.popover-content
p #{translate("try_out_link_sharing")}
img(src="/img/onboarding/linksharing/link-sharing.png" width="100%")
p #{translate("try_link_sharing_description")}
button.btn.btn-default.btn-block(ng-click="dismiss()")
| #{translate("got_it")}

View file

@ -1,112 +0,0 @@
.feat-onboard(
ng-controller="FeatureOnboardingController"
ng-class="('feat-onboard-step' + onboarding.innerStep)"
ng-if="!state.loading && ui.showCollabFeaturesOnboarding"
ng-cloak
stop-propagation="click"
)
a.feat-onboard-dismiss(
href
ng-click="dismiss();"
) &times;
.feat-onboard-wrapper
h1.feat-onboard-title
| Introducing&nbsp;
span.feat-onboard-highlight Commenting
| &amp;
span.feat-onboard-highlight Track Changes
p.feat-onboard-description
span.feat-onboard-highlight Commenting
| and
span.feat-onboard-highlight Track Changes
| will make it easier for you to work with peers in your documents.
.feat-onboard-tutorial-wrapper
button.btn.btn-primary.feat-onboard-nav-btn(
ng-click="gotoPrevStep();"
ng-disabled="onboarding.innerStep === 1;")
i.fa.fa-arrow-left
div(ng-show="onboarding.innerStep === 1;")
video.feat-onboard-video(
video-play-state="onboarding.innerStep === 1;"
autoplay
loop
)
source(ng-src="{{ '/img/onboarding/review-panel/open-review.mp4' }}", type="video/mp4")
img(ng-src="{{ '/img/onboarding/review-panel/open-review.gif' }}", alt="Open review panel demo")
div(ng-show="onboarding.innerStep === 2;")
video.feat-onboard-video(
video-play-state="onboarding.innerStep === 2;"
autoplay
loop
)
source(ng-src="{{ '/img/onboarding/review-panel/commenting.mp4' }}", type="video/mp4")
img(ng-src="{{ '/img/onboarding/review-panel/commenting.gif' }}", alt="Commenting demo")
div(ng-show="onboarding.innerStep === 3;")
video.feat-onboard-video(
video-play-state="onboarding.innerStep === 3;"
autoplay
loop
)
source(ng-src="{{ '/img/onboarding/review-panel/add-changes.mp4' }}", type="video/mp4")
img(ng-src="{{ '/img/onboarding/review-panel/add-changes.gif' }}", alt="Add changes demo")
div(ng-show="onboarding.innerStep === 4;")
video.feat-onboard-video(
video-play-state="onboarding.innerStep === 4;"
autoplay
loop
)
source(ng-src="{{ '/img/onboarding/review-panel/accept-changes.mp4' }}", type="video/mp4")
img(ng-src="{{ '/img/onboarding/review-panel/accept-changes.gif' }}", alt="Accept changes demo")
button.btn.btn-primary.feat-onboard-nav-btn(
ng-click="gotoNextStep();"
ng-disabled="onboarding.innerStep === onboarding.nSteps;")
i.fa.fa-arrow-right
div(ng-switch="onboarding.innerStep")
.row(ng-switch-when="1")
.col-xs-6
h2.feat-onboard-adv-title Commenting
p.feat-onboard-description Want to discuss specific parts of the text?
p.feat-onboard-description Use our brand-new commenting system.
.col-xs-6
h2.feat-onboard-adv-title Track Changes
p.feat-onboard-description See changes in your documents, live.
p.feat-onboard-description Track, accept and reject changes individually.
.row(ng-switch-when="2")
.col-xs-12
h2.feat-onboard-adv-title Commenting
p.feat-onboard-description Just select a span of text and click on
span.feat-onboard-highlight &ldquo;Add comment&rdquo;
| .
p.feat-onboard-description
span.feat-onboard-highlight Comments
| can be
span.feat-onboard-highlight replied
| to,
span.feat-onboard-highlight resolved
| and permanently
span.feat-onboard-highlight deleted
| .
.row(ng-switch-when="3")
.col-xs-12
h2.feat-onboard-adv-title Track Changes
p.feat-onboard-description
| Let your peers know what you've been up to.
p.feat-onboard-description
| Click on the
span.feat-onboard-highlight &ldquo;Track Changes&rdquo;
| toggle to start marking your insertions, as well as your deletions.
.row(ng-switch-when="4")
.col-xs-12
h2.feat-onboard-adv-title Track Changes
p.feat-onboard-description Upon reviewing,
span.feat-onboard-highlight changes
| can be accepted or undone.
p.feat-onboard-description
| Click&nbsp;
span.feat-onboard-highlight &ldquo;Accept&rdquo;
| or&nbsp;
span.feat-onboard-highlight &ldquo;Reject&rdquo;
| to incorporate or discard an individual change.

View file

@ -333,6 +333,7 @@ script(type='text/ng-template', id='newDocModalTemplate')
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type='text/ng-template', id='newFolderModalTemplate')
.modal-header
h3 #{translate("new_folder")}
@ -363,6 +364,34 @@ script(type='text/ng-template', id='newFolderModalTemplate')
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type="text/template", id="qq-file-uploader-template")
div.qq-uploader-selector
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
div.qq-upload-button-selector.btn.btn-primary.btn-lg
div #{translate('upload')}
span.or.btn-lg #{translate('or')}
span.drag-here.btn-lg #{translate('drag_here')}
span.qq-drop-processing-selector
span #{translate('processing')}
span.qq-drop-processing-spinner-selector
ul.qq-upload-list-selector
li
div.qq-progress-bar-container-selector
div(
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
class="qq-progress-bar-selector qq-progress-bar"
)
span.qq-upload-file-selector.qq-upload-file
span.qq-upload-size-selector.qq-upload-size
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-header
h3 #{translate("upload_files")}
@ -384,11 +413,7 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-body(
fine-upload
endpoint="/project/{{ project_id }}/upload"
waiting-for-response-text="{{inserting_files}}"
failed-upload-text="{{upload_failed_sorry}}"
upload-button-text="{{select_files}}"
drag-area-text="{{drag_files}}"
hint-text="{{hint_press_and_hold_control_key}}"
template-id="qq-file-uploader-template"
multiple="true"
auto-upload="false"
on-complete-callback="onComplete"
@ -400,10 +425,10 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
control="control"
params="{'folder_id': parent_folder_id}"
)
span #{translate("upload_files")}
.modal-footer
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
script(type='text/ng-template', id='deleteEntityModalTemplate')
.modal-header
h3 #{translate("delete")} {{ entity.name }}

View file

@ -99,7 +99,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
i.review-icon
p.toolbar-label
| #{translate("review")}
a.btn.btn-full-height(
a.btn.btn-full-height#shareButton(
href,
ng-if="permissions.admin",
ng-click="openShareProjectModal();",

View file

@ -91,9 +91,7 @@
)
li.rp-tc-state-separator
li.rp-tc-state-item.rp-tc-state-item-guests(
ng-if="__enableTokenAccessUI"
)
li.rp-tc-state-item.rp-tc-state-item-guests
span.rp-tc-state-item-name(
ng-class="{ 'rp-tc-state-item-name-disabled' : reviewPanel.trackChangesOnForEveryone}"
tooltip=translate('tc_switch_guests_tip')
@ -577,37 +575,6 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
)
span #{translate("close")}
script(type="text/ng-template", id="perUserTCNoticeModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="$close()"
) &times;
h3 #{translate("per_user_tc_title")}
.modal-body
.teaser-video-container
video.teaser-video(autoplay, loop)
source(ng-src="{{ '/img/teasers/track-changes/per-user-track-changes.mp4' }}", type="video/mp4")
img(src="/img/teasers/track-changes/per-user-track-changes.gif")
h4.teaser-title #{translate("you_can_use_per_user_tc")}
.row
.col-md-8.col-md-offset-2
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("turn_tc_on_individuals")}
li
i.fa.fa-check &nbsp;
| #{translate("keep_tc_on_like_before")}
.modal-footer()
button.btn.btn-default(
ng-click="$close()"
)
span #{translate("close")}
script(type="text/ng-template", id="bulkActionsModalTemplate")
.modal-header
button.close(

View file

@ -9,18 +9,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.modal-body.modal-body-share
.container-fluid
//- Private
.row.public-access-level(ng-show="project.publicAccesLevel == 'private' && __enableTokenAccessUI == false")
.col-xs-12.text-center
| #{translate("this_project_is_private")}
| &nbsp;&nbsp;
a(
href
ng-click="openMakePublicModal()"
) #{translate("make_public")}
//- Private (with token-access available)
.row.public-access-level(ng-show="project.publicAccesLevel == 'private' && __enableTokenAccessUI == true")
.row.public-access-level(ng-show="project.publicAccesLevel == 'private'")
.col-xs-12.text-center
| #{translate('link_sharing_is_off')}.
| &nbsp;&nbsp;
@ -206,31 +196,6 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
ng-click="done()"
) #{translate("close")}
script(type="text/ng-template", id="makePublicModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 #{translate("make_project_public")}?
.modal-body.modal-body-share
p #{translate("make_project_public_consequences")}
p
select.form-control(
ng-model="inputs.privileges"
name="privileges"
)
option(value="readAndWrite") #{translate("allow_public_editing")}
option(value="readOnly") #{translate("allow_public_read_only")}
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-info(
ng-click="makePublic()"
) #{translate("make_public")}
script(type="text/ng-template", id="makeTokenBasedModalTemplate")
.modal-header
button.close(

View file

@ -6,6 +6,7 @@ block content
script#data(type="application/json").
!{JSON.stringify({ projects: projects, tags: tags, notifications: notifications }).replace(/\//g, '\\/')}
script(type="text/javascript").
window.data = JSON.parse($("#data").text());
window.algolia = {

View file

@ -0,0 +1,40 @@
.col-xs-6
input.select-item(
select-individual,
type="checkbox",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"
)
span
a.projectName(
ng-href="{{projectLink(project)}}"
stop-propagation="click"
) {{project.name}}
span(
ng-controller="TagListController"
)
.tag-label(
ng-repeat='tag in project.tags'
stop-propagation="click"
)
a.label.label-default.tag-label-name(
href,
ng-click="selectTag(tag)"
) {{tag.name}}
a.label.label-default.tag-label-remove(
href
ng-click="removeProjectFromTag(project, tag)"
) &times;
.col-xs-2
span.owner {{ownerName()}}
span(ng-if="isLinkSharingProject(project)")
| &nbsp;
i.fa.fa-link.small(
tooltip=translate("link_sharing")
tooltip-placement="right"
tooltip-append-to-body="true"
)
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}

View file

@ -213,6 +213,33 @@ script(type='text/ng-template', id='deleteProjectsModalTemplate')
ng-click="delete()"
) #{translate("confirm")}
script(type="text/template", id="qq-project-uploader-template")
div.qq-uploader-selector
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
div.qq-upload-button-selector.btn.btn-primary.btn-lg
div #{translate('select_a_zip_file')}
span.or.btn-lg #{translate('or')}
span.drag-here.btn-lg #{translate('drag_a_zip_file')}
span.qq-drop-processing-selector
span #{translate('creating_project')}
span.qq-drop-processing-spinner-selector
ul.qq-upload-list-selector
li
div.qq-progress-bar-container-selector
div(
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
class="qq-progress-bar-selector qq-progress-bar"
)
span.qq-upload-file-selector.qq-upload-file
span.qq-upload-size-selector.qq-upload-size
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
script(type="text/ng-template", id="uploadProjectModalTemplate")
.modal-header
button.close(
@ -224,15 +251,11 @@ script(type="text/ng-template", id="uploadProjectModalTemplate")
.modal-body(
fine-upload
endpoint="/project/new/upload"
waiting-for-response-text="Creating project..."
failed-upload-text="Upload failed. Is it a valid zip file?"
upload-button-text="Select a .zip file"
drag-area-text="drag .zip file"
template-id="qq-project-uploader-template"
multiple="false"
allowed-extensions="['zip']"
on-complete-callback="onComplete"
)
span #{translate("upload_a_zipped_project")}
.modal-footer
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}

View file

@ -114,6 +114,10 @@
) #{translate("delete_forever")}
.row.row-spaced
if noV1Connection
.col-xs-12
.alert.alert-warning No V1 Connection
.col-xs-12
.card.card-thin.project-list-card
ul.list-unstyled.project-list.structured-list(
@ -142,47 +146,15 @@
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
ng-controller="ProjectListItemController"
)
.row(select-row)
.col-xs-6
input.select-item(
select-individual,
type="checkbox",
ng-model="project.selected"
stop-propagation="click"
aria-label=translate('select_project') + " '{{ project.name }}'"
)
span
a.projectName(
ng-href="{{projectLink(project)}}"
stop-propagation="click"
) {{project.name}}
span(
ng-controller="TagListController"
)
.tag-label(
ng-repeat='tag in project.tags'
stop-propagation="click"
)
a.label.label-default.tag-label-name(
href,
ng-click="selectTag(tag)"
) {{tag.name}}
a.label.label-default.tag-label-remove(
href
ng-click="removeProjectFromTag(project, tag)"
) &times;
.col-xs-2
span.owner {{ownerName()}}
span(ng-if="isLinkSharingProject(project)")
| &nbsp;
i.fa.fa-link.small(
tooltip=translate("link_sharing")
tooltip-placement="right"
tooltip-append-to-body="true"
)
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}
.row(
ng-if="!project.isV1Project"
select-row
)
include ./item
.row(
ng-if="project.isV1Project"
)
include ./v1-item
li(
ng-if="visibleProjects.length == 0",
ng-cloak

View file

@ -45,6 +45,9 @@
a(href) #{translate("shared_with_you")}
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
a(href) #{translate("deleted_projects")}
if isShowingV1Projects
li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')")
a(href) #{translate("v1_projects")}
li.separator
h2 #{translate("folders")}
li.tag(
@ -62,6 +65,13 @@
)
span.name {{tag.name}}
span.subdued ({{tag.project_ids.length}})
span.v1-badge(
ng-if="tag.isV1",
ng-cloak,
aria-label=translate("v1_badge")
tooltip-template="'v1TagTooltipTemplate'"
tooltip-append-to-body="true"
)
span.dropdown.tag-menu(dropdown)
a.dropdown-toggle(
href="#",

View file

@ -0,0 +1,18 @@
.col-xs-6
span.v1-badge(
aria-label=translate("v1_badge")
tooltip-template="'v1ProjectTooltipTemplate'"
tooltip-append-to-body="true"
)
span
if settings.overleaf && settings.overleaf.host
a.projectName(
href=settings.overleaf.host + "/{{project.id}}"
stop-propagation="click"
) {{project.name}}
.col-xs-2
span.owner {{ownerName()}}
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}

View file

@ -61,7 +61,10 @@ block content
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
p.pull-right
p
a(href="/user/subscription/billing-details/edit").btn.btn-info #{translate("update_your_billing_details")}
if billingDetailsLink
a(href=billingDetailsLink, target="_blank").btn.btn-info #{translate("update_your_billing_details")}
else
a(href=billingDetailsLink, disabled).btn.btn-info.btn-disabled #{translate("update_your_billing_details")}
| &nbsp;
a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
when "canceled"

View file

@ -1,27 +0,0 @@
extends ../layout
block scripts
script(src=buildJsPath('libs/recurly.min.js', {fingerprint:false}))
block content
.content.content-alt
.container
.row
.col-md-6.col-md-offset-3
.card
.page-header
h1.text-centered #{translate("update_your_billing_details")}
#billingDetailsForm #{translate("loading_billing_form")}...
script(type="text/javascript").
Recurly.config(!{recurlyConfig})
Recurly.buildBillingInfoUpdateForm({
target : "#billingDetailsForm",
successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails",
signature : "!{signature}",
accountCode : "#{user.id}"
});

View file

@ -0,0 +1,6 @@
script(type="text/ng-template", id="v1ProjectTooltipTemplate")
span This project is from Overleaf version 1 and has not been imported to the beta yet
script(type="text/ng-template", id="v1TagTooltipTemplate")
span This folder is from Overleaf version 1 and has not been imported to the beta yet

View file

@ -0,0 +1,4 @@
#!/bin/bash
set -e;
MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000"
$MOCHA "$@"

View file

@ -0,0 +1,16 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling test/acceptance/coffee;
$COFFEE -o test/acceptance/js -c test/acceptance/coffee;
for dir in modules/*;
do
if [ -d $dir/test/acceptance ]; then
echo Compiling $dir/test/acceptance/coffee;
$COFFEE -o $dir/test/acceptance/js -c $dir/test/acceptance/coffee;
fi
done

23
services/web/bin/compile_app Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling app.coffee;
$COFFEE -c app.coffee;
echo Compiling app/coffee;
$COFFEE -o app/js -c app/coffee;
for dir in modules/*;
do
if [ -d $dir/app/coffee ]; then
echo Compiling $dir/app/coffee;
$COFFEE -o $dir/app/js -c $dir/app/coffee;
fi
if [ -e $dir/index.coffee ]; then
echo Compiling $dir/index.coffee;
$COFFEE -c $dir/index.coffee;
fi
done

View file

@ -0,0 +1,15 @@
#!/bin/bash
set -e;
COFFEE=node_modules/.bin/coffee
echo Compiling test/unit/coffee;
$COFFEE -o test/unit/js -c test/unit/coffee;
for dir in modules/*;
do
if [ -d $dir/test/unit ]; then
echo Compiling $dir/test/unit/coffee;
$COFFEE -o $dir/test/unit/js -c $dir/test/unit/coffee;
fi
done

View file

@ -0,0 +1,26 @@
#!/usr/bin/env python2
from os import listdir
from os.path import isfile, isdir, join
volumes = []
for module in listdir("modules/"):
if module[0] != '.':
if isfile(join("modules", module, 'index.coffee')):
volumes.append(join("modules", module, 'index.coffee'))
for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee', 'test/acceptance/config', 'test/acceptance/files']:
if isdir(join("modules", module, directory)):
volumes.append(join("modules", module, directory))
volumes_string = map(lambda vol: "- ./" + vol + ":/app/" + vol + ":ro", volumes)
volumes_string = "\n ".join(volumes_string)
with open("docker-shared.template.yml", "r") as f:
docker_shared_file = f.read()
docker_shared_file = docker_shared_file.replace("MODULE_VOLUMES", volumes_string)
with open("docker-shared.yml", "w") as f:
f.write(docker_shared_file)

14
services/web/bin/unit_test Executable file
View file

@ -0,0 +1,14 @@
#!/bin/bash
set -e;
MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
$MOCHA "$@" test/unit/js
for dir in modules/*;
do
if [ -d $dir/test/unit/js ]; then
$MOCHA "$@" $dir/test/unit/js
fi
done

View file

@ -35,12 +35,12 @@ module.exports = settings =
# Databases
# ---------
mongo:
url : 'mongodb://127.0.0.1/sharelatex'
url : process.env['MONGO_URL'] || "mongodb://127.0.0.1/sharelatex"
redis:
web:
host: "localhost"
port: "6379"
host: process.env['REDIS_HOST'] || "localhost"
port: process.env['REDIS_PORT'] || "6379"
password: ""
# websessions:
@ -74,8 +74,8 @@ module.exports = settings =
# ]
api:
host: "localhost"
port: "6379"
host: process.env['REDIS_HOST'] || "localhost"
port: process.env['REDIS_PORT'] || "6379"
password: ""
# Service locations
@ -87,6 +87,7 @@ module.exports = settings =
internal:
web:
port: webPort = 3000
host: process.env['LISTEN_ADDRESS'] or 'localhost'
documentupdater:
port: docUpdaterPort = 3003
@ -99,7 +100,7 @@ module.exports = settings =
user: httpAuthUser
pass: httpAuthPass
documentupdater:
url : "http://localhost:#{docUpdaterPort}"
url : "http://#{process.env['DOCUPDATER_HOST'] or 'localhost'}:#{docUpdaterPort}"
thirdPartyDataStore:
url : "http://localhost:3002"
emptyProjectFlushDelayMiliseconds: 5 * seconds
@ -113,7 +114,7 @@ module.exports = settings =
enabled: false
url : "http://localhost:3054"
docstore:
url : "http://localhost:3016"
url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"
pubUrl: "http://localhost:3016"
chat:
url: "http://localhost:3010"

View file

@ -0,0 +1,36 @@
version: "2"
volumes:
node_modules:
services:
npm:
extends:
file: docker-shared.yml
service: app
command: npm install
test_unit:
extends:
file: docker-shared.yml
service: app
command: npm run test:unit
test_acceptance:
extends:
file: docker-shared.yml
service: app
environment:
REDIS_HOST: redis
MONGO_URL: "mongodb://mongo/sharelatex"
SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true'
depends_on:
- redis
- mongo
command: npm run start
redis:
image: redis
mongo:
image: mongo:3.4.6

View file

@ -0,0 +1,30 @@
version: "2"
# We mount all the directories explicitly so that we are only mounting
# the coffee directories, so that the compiled js is only written inside
# the container, and not back to the local filesystem, where it would be
# root owned, and conflict with working outside of the container.
services:
app:
image: node:6.9.5
volumes:
- ./package.json:/app/package.json
- ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json
- node_modules:/app/node_modules
- ./bin:/app/bin
# Copying the whole public dir is fine for now, and needed for
# some unit tests to pass, but we will want to isolate the coffee
# and vendor js files, so that the compiled js files are not written
# back to the local filesystem.
- ./public:/app/public
- ./app.coffee:/app/app.coffee:ro
- ./app/coffee:/app/app/coffee:ro
- ./app/templates:/app/app/templates:ro
- ./app/views:/app/app/views:ro
- ./config:/app/config
- ./test/unit/coffee:/app/test/unit/coffee:ro
- ./test/acceptance/coffee:/app/test/acceptance/coffee:ro
- ./test/smoke/coffee:/app/test/smoke/coffee:ro
MODULE_VOLUMES
working_dir: /app

View file

@ -1,12 +1,6 @@
*/app/js
*/test/unit/js
*/index.js
ldap
admin-panel
groovehq
launchpad
learn-wiki
references-search
sharelatex-saml
templates
tpr-webmodule
# Ignore all modules except for a whitelist
*
!dropbox
!github-sync
!public-registration
!.gitignore

View file

@ -9,6 +9,17 @@
"directories": {
"public": "./public"
},
"scripts": {
"test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done",
"test:acceptance:run": "bin/acceptance_test $@",
"test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@",
"test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js",
"test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@",
"compile:unit_tests": "bin/compile_unit_tests",
"compile:acceptance_tests": "bin/compile_acceptance_tests",
"compile:app": "bin/compile_app",
"start": "npm -q run compile:app && node app.js"
},
"dependencies": {
"archiver": "0.9.0",
"async": "0.6.2",
@ -98,6 +109,7 @@
"grunt-postcss": "^0.8.0",
"grunt-sed": "^0.1.1",
"grunt-shell": "^2.1.0",
"mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"sandboxed-module": "0.2.0",
"sinon": "^1.17.0",
"timekeeper": "",

View file

@ -1,16 +1,13 @@
define [
"base"
], (App) ->
"fineuploader"
], (App, qq) ->
App.directive 'fineUpload', ($timeout) ->
return {
scope: {
multiple: "="
endpoint: "@"
waitingForResponseText: "@"
failedUploadText: "@"
uploadButtonText: "@"
dragAreaText: "@"
hintText: "@"
templateId: "@"
allowedExtensions: "="
onCompleteCallback: "="
onUploadCallback: "="
@ -25,17 +22,12 @@ define [
link: (scope, element, attrs) ->
multiple = scope.multiple or false
endpoint = scope.endpoint
templateId = scope.templateId
if scope.allowedExtensions?
validation =
validation =
allowedExtensions: scope.allowedExtensions
else
validation = {}
text =
waitingForResponse: scope.waitingForResponseText or "Processing..."
failUpload: scope.failedUploadText or "Failed :("
uploadButton: scope.uploadButtonText or "Upload"
dragAreaText = scope.dragAreaText or "drag here"
hintText = scope.hintText or ""
maxConnections = scope.maxConnections or 1
onComplete = scope.onCompleteCallback or () ->
onUpload = scope.onUploadCallback or () ->
@ -69,21 +61,8 @@ define [
onError: onError
onSubmit: onSubmit
onCancel: onCancel
text: text
template: """
<div class="qq-uploader">
<div class="qq-upload-drop-area"><span>{dragZoneText}</span></div>
<div class="qq-upload-button btn btn-primary btn-lg">
<div>{uploadButtonText}</div>
</div>
<span class="or btn-lg"> or </span>
<span class="drag-here btn-lg">#{dragAreaText}</span>
<span class="qq-drop-processing"><span>{dropProcessingText}</span><span class="qq-drop-processing-spinner"></span></span>
<div class="small">#{hintText}</div>
<ul class="qq-upload-list"></ul>
</div>
"""
template: templateId
window.q = q
scope.control?.q = q
return q
}
}

View file

@ -9,11 +9,11 @@ define [
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
"ide/references/ReferencesManager"
"ide/labels/LabelsManager"
"ide/metadata/MetadataManager"
"ide/review-panel/ReviewPanelManager"
"ide/SafariScrollPatcher"
"ide/FeatureOnboardingController",
"ide/AutoCompileOnboardingController",
"ide/LinkSharingOnboardingController",
"ide/settings/index"
"ide/share/index"
"ide/chat/index"
@ -47,12 +47,12 @@ define [
PdfManager
BinaryFilesManager
ReferencesManager
LabelsManager
MetadataManager
ReviewPanelManager
SafariScrollPatcher
) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, labels) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) ->
# Don't freak out if we're already in an apply callback
$scope.$originalApply = $scope.$apply
$scope.$apply = (fn = () ->) ->
@ -72,27 +72,16 @@ define [
view: "editor"
chatOpen: false
pdfLayout: 'sideBySide'
pdfHidden: false,
pdfWidth: 0,
reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}"),
miniReviewPanelVisible: false,
pdfHidden: false
pdfWidth: 0
reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}")
miniReviewPanelVisible: false
}
$scope.onboarding = {
autoCompile: if window.showAutoCompileOnboarding then 'unseen' else 'dismissed'
linkSharing: if window.showLinkSharingOnboarding then 'unseen' else 'dismissed'
}
$scope.user = window.user
$scope.__enableTokenAccessUI = window.enableTokenAccessUI == true
# TODO: remove after rollout and testing
window.turnOnTokenAccessUI = () ->
$scope.__enableTokenAccessUI = true
$scope.$digest
window.turnOffTokenAccessUI = () ->
$scope.__enableTokenAccessUI = false
$scope.$digest
$scope.$watch "project.features.trackChangesVisible", (visible) ->
return if !visible?
$scope.ui.showCollabFeaturesOnboarding = window.showTrackChangesOnboarding and visible
$scope.shouldABTestPlans = false
if $scope.user.signUpDate >= '2016-10-27'
@ -149,7 +138,7 @@ define [
ide.pdfManager = new PdfManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
ide.labelsManager = new LabelsManager(ide, $scope, labels)
ide.metadataManager = new MetadataManager(ide, $scope, metadata)
inited = false
$scope.$on "project:joined", () ->
@ -164,11 +153,21 @@ define [
$timeout(
() ->
if $scope.permissions.write
ide.labelsManager.loadProjectLabelsFromServer()
ide.metadataManager.loadProjectMetaFromServer()
_labelsInitialLoadDone = true
, 200
)
# Count the first 'doc:opened' as a sign that the ide is loaded
# and broadcast a message. This is a good event to listen for
# if you want to wait until the ide is fully loaded and initialized
_loaded = false
$scope.$on 'doc:opened', () ->
if _loaded
return
$scope.$broadcast('ide:loaded')
_loaded = true
DARK_THEMES = [
"ambiance", "chaos", "clouds_midnight", "cobalt", "idle_fingers",
"merbivore", "merbivore_soft", "mono_industrial", "monokai",

View file

@ -23,4 +23,4 @@ define [
$scope.dismiss = () ->
$scope.onboarding.autoCompile = 'dismissed'
event_tracking.sendMB "shown-autocompile-onboarding"
event_tracking.sendMB "shown-autocompile-onboarding-2"

View file

@ -1,32 +0,0 @@
define [
"base"
], (App) ->
App.controller "FeatureOnboardingController", ($scope, settings, event_tracking) ->
$scope.onboarding =
innerStep: 1
nSteps: 4
$scope.dismiss = () ->
event_tracking.sendMB "shown-track-changes-onboarding-2"
$scope.$applyAsync(() -> $scope.ui.showCollabFeaturesOnboarding = false)
$scope.gotoPrevStep = () ->
if $scope.onboarding.innerStep > 1
$scope.$applyAsync(() -> $scope.onboarding.innerStep--)
$scope.gotoNextStep = () ->
if $scope.onboarding.innerStep < 4
$scope.$applyAsync(() -> $scope.onboarding.innerStep++)
handleKeydown = (e) ->
switch e.keyCode
when 37 then $scope.gotoPrevStep() # left directional key
when 39, 13 then $scope.gotoNextStep() # right directional key, enter
when 27 then $scope.dismiss() # escape
$(document).on "keydown", handleKeydown
$(document).on "click", $scope.dismiss
$scope.$on "$destroy", () ->
$(document).off "keydown", handleKeydown
$(document).off "click", $scope.dismiss

View file

@ -0,0 +1,21 @@
define [
"base"
], (App) ->
App.controller "LinkSharingOnboardingController", ($scope, $timeout, event_tracking) ->
popover = angular.element('#onboarding-linksharing')
popover.hide()
$scope.dismiss = () ->
$scope.onboarding.linkSharing = 'dismissed'
event_tracking.sendMB "shown-linksharing-onboarding"
$scope.$on 'ide:loaded', () ->
shareBtn = angular.element('#shareButton')
offset = shareBtn.offset()
popover.show()
$scope.placement = 'bottom'
popover.css({
top: '' + (2) + 'px',
right: '' + (window.innerWidth - offset.left - (shareBtn.width() * 1.5) ) + 'px'
})

View file

@ -162,7 +162,7 @@ define [
clearChaosMonkey: () ->
clearTimeout @_cm
MAX_PENDING_OP_SIZE: 30 # pending ops bigger than this are always considered unsaved
MAX_PENDING_OP_SIZE: 64 # pending ops bigger than this are always considered unsaved
pollSavedStatus: () ->
# returns false if doc has ops waiting to be acknowledged or

View file

@ -104,8 +104,8 @@ define [
getInflightOp: () -> @_doc.inflightOp
getPendingOp: () -> @_doc.pendingOp
getRecentAck: () ->
# check if we have received an ack recently (within the flush delay)
@lastAcked? and new Date() - @lastAcked < @_doc._flushDelay
# check if we have received an ack recently (within a factor of two of the single user flush delay)
@lastAcked? and new Date() - @lastAcked < 2 * SINGLE_USER_FLUSH_DELAY
getOpSize: (op) ->
# compute size of an op from its components
# (total number of characters inserted and deleted)

View file

@ -9,11 +9,14 @@ define [
"ide/editor/directives/aceEditor/highlights/HighlightsManager"
"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
"ide/editor/directives/aceEditor/metadata/MetadataManager"
"ide/editor/directives/aceEditor/labels/LabelsManager"
"ide/labels/services/labels"
"ide/metadata/services/metadata"
"ide/graphics/services/graphics"
"ide/preamble/services/preamble"
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) ->
"ide/files/services/files"
], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager, LabelsManager) ->
EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist')
@ -35,7 +38,7 @@ define [
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, graphics, preamble, $http) ->
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, metadata, graphics, preamble, files, $http, $q) ->
monkeyPatchSearch($rootScope, $compile)
return {
@ -97,13 +100,14 @@ define [
if scope.spellCheck # only enable spellcheck when explicitly required
spellCheckCache = $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache, $http)
spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache, $http, $q)
undoManager = new UndoManager(scope, editor, element)
highlightsManager = new HighlightsManager(scope, editor, element)
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
trackChangesManager = new TrackChangesManager(scope, editor, element)
labelsManager = new LabelsManager(scope, editor, element, labels)
autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager, graphics, preamble)
metadataManager = new MetadataManager(scope, editor, element, metadata)
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, labelsManager, graphics, preamble, files)
# Prevert Ctrl|Cmd-S from triggering save dialog
@ -115,16 +119,16 @@ define [
editor.commands.removeCommand "transposeletters"
editor.commands.removeCommand "showSettingsMenu"
editor.commands.removeCommand "foldall"
# For European keyboards, the / is above 7 so needs Shift pressing.
# This comes through as Command-Shift-/ on OS X, which is mapped to
# This comes through as Command-Shift-/ on OS X, which is mapped to
# toggleBlockComment.
# This doesn't do anything for LaTeX, so remap this to togglecomment to
# work for European keyboards as normal.
# On Windows, the key combo comes as Ctrl-Shift-7.
editor.commands.removeCommand "toggleBlockComment"
editor.commands.removeCommand "togglecomment"
editor.commands.addCommand {
name: "togglecomment",
bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" },
@ -140,7 +144,7 @@ define [
exec: (editor) ->
ace.require("ace/ext/searchbox").Search(editor, true)
readOnly: true
# Bold text on CMD+B
editor.commands.addCommand
name: "bold",
@ -154,7 +158,7 @@ define [
text = editor.getCopyText()
editor.insert("\\textbf{" + text + "}")
readOnly: false
# Italicise text on CMD+I
editor.commands.addCommand
name: "italics",
@ -171,7 +175,7 @@ define [
scope.$watch "onCtrlEnter", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "compile",
bindKey: win: "Ctrl-Enter", mac: "Command-Enter"
exec: (editor) =>
@ -180,7 +184,7 @@ define [
scope.$watch "onCtrlJ", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "toggle-review-panel",
bindKey: win: "Ctrl-J", mac: "Command-J"
exec: (editor) =>
@ -189,7 +193,7 @@ define [
scope.$watch "onCtrlShiftC", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "add-new-comment",
bindKey: win: "Ctrl-Shift-C", mac: "Command-Shift-C"
exec: (editor) =>
@ -198,7 +202,7 @@ define [
scope.$watch "onCtrlShiftA", (callback) ->
if callback?
editor.commands.addCommand
editor.commands.addCommand
name: "toggle-track-changes",
bindKey: win: "Ctrl-Shift-A", mac: "Command-Shift-A"
exec: (editor) =>
@ -302,7 +306,7 @@ define [
if updateCount == 100
event_tracking.send 'editor-interaction', 'multi-doc-update'
scope.$emit "#{scope.name}:change"
onScroll = (scrollTop) ->
return if !scope.eventsBridge?
height = editor.renderer.layerConfig.maxHeight
@ -311,7 +315,7 @@ define [
onScrollbarVisibilityChanged = (event, vRenderer) ->
return if !scope.eventsBridge?
scope.eventsBridge.emit "aceScrollbarVisibilityChanged", vRenderer.scrollBarV.isVisible, vRenderer.scrollBarV.width
if scope.eventsBridge?
editor.renderer.on "scrollbarVisibilityChanged", onScrollbarVisibilityChanged
@ -356,6 +360,7 @@ define [
session.setOption("useWorker", scope.syntaxValidation);
# now attach session to editor
editor.setReadOnly(true) # set to readonly until document change handlers are attached
editor.setSession(session)
doc = session.getDocument()
@ -364,6 +369,8 @@ define [
editor.initing = true
sharejs_doc.attachToAce(editor)
editor.initing = false
# now ready to edit document
editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false
resetScrollMargins()
@ -401,14 +408,14 @@ define [
session = editor.getSession()
session.off "changeScrollTop"
doc = session.getDocument()
doc.off "change", onChange
editor.renderer.on "changeCharacterSize", () ->
scope.$apply () ->
scope.rendererData.lineHeight = editor.renderer.lineHeight
scope.$watch "rendererData", (rendererData) ->
if rendererData?
rendererData.lineHeight = editor.renderer.lineHeight

View file

@ -1,16 +1,16 @@
define [
"ide/editor/directives/aceEditor/auto-complete/CommandManager"
"ide/editor/directives/aceEditor/auto-complete/EnvironmentManager"
"ide/editor/directives/aceEditor/auto-complete/PackageManager"
"ide/editor/directives/aceEditor/auto-complete/Helpers"
"ace/ace"
"ace/ext-language_tools"
], (CommandManager, EnvironmentManager, Helpers) ->
], (CommandManager, EnvironmentManager, PackageManager, Helpers) ->
Range = ace.require("ace/range").Range
aceSnippetManager = ace.require('ace/snippets').snippetManager
class AutoCompleteManager
constructor: (@$scope, @editor, @element, @labelsManager, @graphics, @preamble) ->
@suggestionManager = new CommandManager()
constructor: (@$scope, @editor, @element, @metadataManager, @labelsManager, @graphics, @preamble, @files) ->
@monkeyPatchAutocomplete()
@ -34,10 +34,15 @@ define [
enableLiveAutocompletion: false
})
commandCompleter = new CommandManager(@metadataManager)
SnippetCompleter = new EnvironmentManager()
PackageCompleter = new PackageManager()
Graphics = @graphics
Preamble = @preamble
Files = @files
GraphicsCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
@ -63,7 +68,28 @@ define [
}
callback null, result
labelsManager = @labelsManager
metadataManager = @metadataManager
FilesCompleter =
getCompletions: (editor, session, pos, prefix, callback) =>
context = Helpers.getContext(editor, pos)
{lineUpToCursor, commandFragment, lineBeyondCursor, needsClosingBrace} = context
if commandFragment
match = commandFragment.match(/^\\(input|include){(\w*)/)
if match
commandName = match[1]
currentArg = match[2]
result = []
for file in Files.getTeXFiles()
if file.id != @$scope.docId
path = file.path
result.push {
caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
meta: "file",
score: 50
}
callback null, result
LabelsCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
@ -74,13 +100,14 @@ define [
commandName = refMatch[1]
currentArg = refMatch[2]
result = []
result.push {
caption: "\\#{commandName}{}",
snippet: "\\#{commandName}{}",
meta: "cross-reference",
score: 60
}
for label in labelsManager.getAllLabels()
if commandName != 'ref' # ref is in top 100 commands
result.push {
caption: "\\#{commandName}{}",
snippet: "\\#{commandName}{}",
meta: "cross-reference",
score: 60
}
for label in metadataManager.getAllLabels()
result.push {
caption: "\\#{commandName}{#{label}#{if needsClosingBrace then '}' else ''}",
value: "\\#{commandName}{#{label}#{if needsClosingBrace then '}' else ''}",
@ -126,11 +153,13 @@ define [
callback null, result
@editor.completers = [
@suggestionManager,
SnippetCompleter,
ReferencesCompleter,
LabelsCompleter,
commandCompleter
SnippetCompleter
PackageCompleter
ReferencesCompleter
LabelsCompleter
GraphicsCompleter
FilesCompleter
]
disable: () ->
@ -149,11 +178,9 @@ define [
lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
lastTwoChars = lineUpToCursor.slice(-2)
# Don't offer autocomplete on double-backslash, backslash-colon, etc
if lastTwoChars.match(/^\\[^a-z]$/)
if lastTwoChars.match(/^\\[^a-zA-Z]$/)
@editor?.completer?.detach?()
return
if commandName in ['begin', 'end']
return
# Check that this change was made by us, not a collaborator
# (Cursor is still one place behind)
# NOTE: this is also the case when a user backspaces over a highlighted region
@ -229,8 +256,9 @@ define [
99999
)
)
if lineBeyondCursor
if partialCommandMatch = lineBeyondCursor.match(/^([a-z0-9]+)\{/)
if partialCommandMatch = lineBeyondCursor.match(/^([a-zA-Z0-9]+)\{/)
# We've got a partial command after the cursor
commandTail = partialCommandMatch[1]
# remove rest of the partial command, right of cursor

View file

@ -1,81 +1,8 @@
define [], () ->
noArgumentCommands = [
'item', 'hline', 'lipsum', 'centering', 'noindent', 'textwidth', 'draw',
'maketitle', 'newpage', 'verb', 'bibliography', 'hfill', 'par',
'in', 'sum', 'cdot', 'ldots', 'linewidth', 'left', 'right', 'today',
'clearpage', 'newline', 'endinput', 'tableofcontents', 'vfill',
'bigskip', 'fill', 'cleardoublepage', 'infty', 'leq', 'geq', 'times',
'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'varepsilon', 'zeta',
'eta', 'theta', 'vartheta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi',
'pi', 'varpi', 'rho', 'varrho', 'sigma', 'varsigma', 'tau', 'upsilon',
'phi', 'varphi', 'chi', 'psi', 'omega', 'Gamma', 'Delta', 'Theta',
'Lambda', 'Xi', 'Pi', 'Sigma', 'Upsilon', 'Phi', 'Psi', 'Omega'
]
singleArgumentCommands = [
'chapter', 'usepackage', 'section', 'label', 'textbf', 'subsection',
'vspace', 'cite', 'textit', 'documentclass', 'includegraphics', 'input',
'emph','caption', 'ref', 'title', 'author', 'texttt', 'include',
'hspace', 'bibitem', 'url', 'large', 'subsubsection', 'textsc', 'date',
'footnote', 'small', 'thanks', 'underline', 'graphicspath', 'pageref',
'section*', 'subsection*', 'subsubsection*', 'sqrt', 'text',
'normalsize', 'footnotesize', 'Large', 'paragraph', 'pagestyle',
'thispagestyle', 'bibliographystyle', 'hat'
]
doubleArgumentCommands = [
'newcommand', 'frac', 'dfrac', 'renewcommand', 'setlength', 'href',
'newtheorem'
]
tripleArgumentCommands = [
'addcontentsline', 'newacronym', 'multicolumn'
]
special = ['LaTeX', 'TeX']
define [
"./top_hundred_snippets"
], (topHundred) ->
rawCommands = [].concat(
noArgumentCommands,
singleArgumentCommands,
doubleArgumentCommands,
tripleArgumentCommands,
special
)
noArgumentCommands = for cmd in noArgumentCommands
{
caption: "\\#{cmd}"
snippet: "\\#{cmd}"
meta: "cmd"
}
singleArgumentCommands = for cmd in singleArgumentCommands
{
caption: "\\#{cmd}{}"
snippet: "\\#{cmd}{$1}"
meta: "cmd"
}
doubleArgumentCommands = for cmd in doubleArgumentCommands
{
caption: "\\#{cmd}{}{}"
snippet: "\\#{cmd}{$1}{$2}"
meta: "cmd"
}
tripleArgumentCommands = for cmd in tripleArgumentCommands
{
caption: "\\#{cmd}{}{}{}"
snippet: "\\#{cmd}{$1}{$2}{$3}"
meta: "cmd"
}
special = for cmd in special
{
caption: "\\#{cmd}{}"
snippet: "\\#{cmd}{}"
meta: "cmd"
}
staticCommands = [].concat(
noArgumentCommands,
singleArgumentCommands,
doubleArgumentCommands,
tripleArgumentCommands,
special
)
commandNames = (snippet.caption.match(/\w+/)[0] for snippet in topHundred)
class Parser
constructor: (@doc, @prefix) ->
@ -129,10 +56,10 @@ define [], () ->
return realCommands
# Ignore single letter commands since auto complete is moot then.
commandRegex: /\\([a-zA-Z][a-zA-Z]+)/
commandRegex: /\\([a-zA-Z]{2,})/
nextCommand: () ->
i = @doc.search(@commandRegex)
i = @doc.search @commandRegex
if i == -1
return false
else
@ -166,13 +93,21 @@ define [], () ->
return false
class CommandManager
constructor: (@metadataManager) ->
getCompletions: (editor, session, pos, prefix, callback) ->
packages = @metadataManager.getAllPackages()
packageCommands = []
for pkg, snippets of packages
for snippet in snippets
packageCommands.push snippet
doc = session.getValue()
parser = new Parser(doc, prefix)
commands = parser.parse()
completions = []
for command in commands
if command[0] not in rawCommands
if command[0] not in commandNames
caption = "\\#{command[0]}"
score = if caption == prefix then 99 else 50
snippet = caption
@ -191,9 +126,9 @@ define [], () ->
meta: "cmd"
score: score
}
completions = completions.concat staticCommands
completions = completions.concat topHundred, packageCommands
callback(null, completions)
callback null, completions
loadCommandsFromDoc: (doc) ->
parser = new Parser(doc)

View file

@ -17,8 +17,8 @@ define [
# \includegraphics[width=\textwidth]{..
# should not match the \textwidth.
blankArguments = lineUpToCursor.replace /\[([^\]]*)\]/g, (args) ->
Array(args.length+1).join('.')
if m = blankArguments.match(/(\\[^\\]+)$/)
Array(args.length + 1).join('.')
if m = blankArguments.match(/(\\[^\\]*)$/)
return m.index
else
return -1

View file

@ -0,0 +1,41 @@
define () ->
packages = [
'inputenc', 'graphicx', 'amsmath', 'geometry', 'amssymb', 'hyperref',
'babel', 'color', 'xcolor', 'url', 'natbib', 'fontenc', 'fancyhdr',
'amsfonts', 'booktabs', 'amsthm', 'float', 'tikz', 'caption',
'setspace', 'multirow', 'array', 'multicol', 'titlesec', 'enumitem',
'ifthen', 'listings', 'blindtext', 'subcaption', 'times', 'bm',
'subfigure', 'algorithm', 'fontspec', 'biblatex', 'tabularx',
'microtype', 'etoolbox', 'parskip', 'calc', 'verbatim', 'mathtools',
'epsfig', 'wrapfig', 'lipsum', 'cite', 'textcomp', 'longtable',
'textpos', 'algpseudocode', 'enumerate', 'subfig', 'pdfpages',
'epstopdf', 'latexsym', 'lmodern', 'pifont', 'ragged2e', 'rotating',
'dcolumn', 'xltxtra', 'marvosym', 'indentfirst', 'xspace', 'csquotes',
'xparse', 'changepage', 'soul', 'xunicode', 'comment', 'mathrsfs',
'tocbibind', 'lastpage', 'algorithm2e', 'pgfplots', 'lineno',
'graphics', 'algorithmic', 'fullpage', 'mathptmx', 'todonotes',
'ulem', 'tweaklist', 'moderncvstyleclassic', 'collection',
'moderncvcompatibility', 'gensymb', 'helvet', 'siunitx', 'adjustbox',
'placeins', 'colortbl', 'appendix', 'makeidx', 'supertabular', 'ifpdf',
'framed', 'aliascnt', 'layaureo', 'authblk'
]
packageSnippets = for pkg in packages
{
caption: "\\usepackage{#{pkg}}"
snippet: "\\usepackage{#{pkg}}"
meta: "pkg"
}
packageSnippets.push {
caption: "\\usepackage{}"
snippet: "\\usepackage{$1}"
meta: "pkg"
score: 70
}
class PackageManager
getCompletions: (editor, session, pos, prefix, callback) ->
callback null, packageSnippets
return PackageManager

View file

@ -0,0 +1,691 @@
define -> [{
"caption": "\\begin{}",
"snippet": "\\begin{$1}",
"meta": "cmd",
"score": 7.849662248028187
}, {
"caption": "\\begin{}[]",
"snippet": "\\begin{$1}[$2]",
"meta": "cmd",
"score": 7.849662248028187
}, {
"caption": "\\begin{}{}",
"snippet": "\\begin{$1}{$2}",
"meta": "cmd",
"score": 7.849662248028187
}, {
"caption": "\\end{}",
"snippet": "\\end{$1}",
"meta": "cmd",
"score": 7.847906405228455
}, {
"caption": "\\usepackage{}",
"snippet": "\\usepackage{$1}",
"meta": "cmd",
"score": 5.427890758130527
}, {
"caption": "\\usepackage[]{}",
"snippet": "\\usepackage[$1]{$2}",
"meta": "cmd",
"score": 5.427890758130527
}, {
"caption": "\\item",
"snippet": "\\item",
"meta": "cmd",
"score": 3.800886892251021
}, {
"caption": "\\item[]",
"snippet": "\\item[$1]",
"meta": "cmd",
"score": 3.800886892251021
}, {
"caption": "\\section{}",
"snippet": "\\section{$1}",
"meta": "cmd",
"score": 3.0952612541683835
}, {
"caption": "\\textbf{}",
"snippet": "\\textbf{$1}",
"meta": "cmd",
"score": 2.627755982816738
}, {
"caption": "\\cite{}",
"snippet": "\\cite{$1}",
"meta": "cmd",
"score": 2.341195220791228
}, {
"caption": "\\label{}",
"snippet": "\\label{$1}",
"meta": "cmd",
"score": 1.897791904799601
}, {
"caption": "\\textit{}",
"snippet": "\\textit{$1}",
"meta": "cmd",
"score": 1.6842996195493385
}, {
"caption": "\\includegraphics[]{}",
"snippet": "\\includegraphics[$1]{$2}",
"meta": "cmd",
"score": 1.4595731795525781
}, {
"caption": "\\documentclass[]{}",
"snippet": "\\documentclass[$1]{$2}",
"meta": "cmd",
"score": 1.4425339817971206
}, {
"caption": "\\documentclass{}",
"snippet": "\\documentclass{$1}",
"meta": "cmd",
"score": 1.4425339817971206
}, {
"caption": "\\frac{}{}",
"snippet": "\\frac{$1}{$2}",
"meta": "cmd",
"score": 1.4341091141105058
}, {
"caption": "\\subsection{}",
"snippet": "\\subsection{$1}",
"meta": "cmd",
"score": 1.3890912739512353
}, {
"caption": "\\hline",
"snippet": "\\hline",
"meta": "cmd",
"score": 1.3209538327406387
}, {
"caption": "\\caption{}",
"snippet": "\\caption{$1}",
"meta": "cmd",
"score": 1.2569477427490174
}, {
"caption": "\\centering",
"snippet": "\\centering",
"meta": "cmd",
"score": 1.1642881814937829
}, {
"caption": "\\vspace{}",
"snippet": "\\vspace{$1}",
"meta": "cmd",
"score": 0.9533807826673939
}, {
"caption": "\\title{}",
"snippet": "\\title{$1}",
"meta": "cmd",
"score": 0.9202908262245683
}, {
"caption": "\\author{}",
"snippet": "\\author{$1}",
"meta": "cmd",
"score": 0.8973590434087177
}, {
"caption": "\\author[]{}",
"snippet": "\\author[$1]{$2}",
"meta": "cmd",
"score": 0.8973590434087177
}, {
"caption": "\\maketitle",
"snippet": "\\maketitle",
"meta": "cmd",
"score": 0.7504160124360846
}, {
"caption": "\\textwidth",
"snippet": "\\textwidth",
"meta": "cmd",
"score": 0.7355328080889112
}, {
"caption": "\\newcommand{}{}",
"snippet": "\\newcommand{$1}{$2}",
"meta": "cmd",
"score": 0.7264891987129375
}, {
"caption": "\\newcommand{}[]{}",
"snippet": "\\newcommand{$1}[$2]{$3}",
"meta": "cmd",
"score": 0.7264891987129375
}, {
"caption": "\\date{}",
"snippet": "\\date{$1}",
"meta": "cmd",
"score": 0.7225518453076786
}, {
"caption": "\\emph{}",
"snippet": "\\emph{$1}",
"meta": "cmd",
"score": 0.7060308784832261
}, {
"caption": "\\textsc{}",
"snippet": "\\textsc{$1}",
"meta": "cmd",
"score": 0.6926466355384758
}, {
"caption": "\\multicolumn{}{}{}",
"snippet": "\\multicolumn{$1}{$2}{$3}",
"meta": "cmd",
"score": 0.5473606021405326
}, {
"caption": "\\input{}",
"snippet": "\\input{$1}",
"meta": "cmd",
"score": 0.4966021927742672
}, {
"caption": "\\alpha",
"snippet": "\\alpha",
"meta": "cmd",
"score": 0.49520006391384913
}, {
"caption": "\\in",
"snippet": "\\in",
"meta": "cmd",
"score": 0.4716039670146658
}, {
"caption": "\\mathbf{}",
"snippet": "\\mathbf{$1}",
"meta": "cmd",
"score": 0.4682018419466319
}, {
"caption": "\\right",
"snippet": "\\right",
"meta": "cmd",
"score": 0.4299239459457309
}, {
"caption": "\\left",
"snippet": "\\left",
"meta": "cmd",
"score": 0.42937815279867964
}, {
"caption": "\\left[]",
"snippet": "\\left[$1]",
"meta": "cmd",
"score": 0.42937815279867964
}, {
"caption": "\\sum",
"snippet": "\\sum",
"meta": "cmd",
"score": 0.42607994509619934
}, {
"caption": "\\noindent",
"snippet": "\\noindent",
"meta": "cmd",
"score": 0.42355747798114207
}, {
"caption": "\\chapter{}",
"snippet": "\\chapter{$1}",
"meta": "cmd",
"score": 0.422097569591803
}, {
"caption": "\\par",
"snippet": "\\par",
"meta": "cmd",
"score": 0.413853376001159
}, {
"caption": "\\lambda",
"snippet": "\\lambda",
"meta": "cmd",
"score": 0.39389600578684125
}, {
"caption": "\\subsubsection{}",
"snippet": "\\subsubsection{$1}",
"meta": "cmd",
"score": 0.3727781330132016
}, {
"caption": "\\bibitem{}",
"snippet": "\\bibitem{$1}",
"meta": "cmd",
"score": 0.3689547570562042
}, {
"caption": "\\bibitem[]{}",
"snippet": "\\bibitem[$1]{$2}",
"meta": "cmd",
"score": 0.3689547570562042
}, {
"caption": "\\text{}",
"snippet": "\\text{$1}",
"meta": "cmd",
"score": 0.3608680734736821
}, {
"caption": "\\setlength{}{}",
"snippet": "\\setlength{$1}{$2}",
"meta": "cmd",
"score": 0.354445763583904
}, {
"caption": "\\setlength",
"snippet": "\\setlength",
"meta": "cmd",
"score": 0.354445763583904
}, {
"caption": "\\mathcal{}",
"snippet": "\\mathcal{$1}",
"meta": "cmd",
"score": 0.35084018920966636
}, {
"caption": "\\newline",
"snippet": "\\newline",
"meta": "cmd",
"score": 0.3311721696201715
}, {
"caption": "\\newpage",
"snippet": "\\newpage",
"meta": "cmd",
"score": 0.3277033727934986
}, {
"caption": "\\renewcommand{}{}",
"snippet": "\\renewcommand{$1}{$2}",
"meta": "cmd",
"score": 0.3267437011085663
}, {
"caption": "\\renewcommand",
"snippet": "\\renewcommand",
"meta": "cmd",
"score": 0.3267437011085663
}, {
"caption": "\\theta",
"snippet": "\\theta",
"meta": "cmd",
"score": 0.3210417159232142
}, {
"caption": "\\hspace{}",
"snippet": "\\hspace{$1}",
"meta": "cmd",
"score": 0.3147206476372336
}, {
"caption": "\\beta",
"snippet": "\\beta",
"meta": "cmd",
"score": 0.3061799530337638
}, {
"caption": "\\texttt{}",
"snippet": "\\texttt{$1}",
"meta": "cmd",
"score": 0.3019066753744355
}, {
"caption": "\\times",
"snippet": "\\times",
"meta": "cmd",
"score": 0.2957960629411553
}, {
"caption": "\\citep{}",
"snippet": "\\citep{$1}",
"meta": "cmd",
"score": 0.2941882834697057
}, {
"caption": "\\color[]{}",
"snippet": "\\color[$1]{$2}",
"meta": "cmd",
"score": 0.2864294797053033
}, {
"caption": "\\color{}",
"snippet": "\\color{$1}",
"meta": "cmd",
"score": 0.2864294797053033
}, {
"caption": "\\mu",
"snippet": "\\mu",
"meta": "cmd",
"score": 0.27635652476799255
}, {
"caption": "\\bibliography{}",
"snippet": "\\bibliography{$1}",
"meta": "cmd",
"score": 0.2659628337907604
}, {
"caption": "\\linewidth",
"snippet": "\\linewidth",
"meta": "cmd",
"score": 0.2639498312518439
}, {
"caption": "\\delta",
"snippet": "\\delta",
"meta": "cmd",
"score": 0.2620578600722735
}, {
"caption": "\\sigma",
"snippet": "\\sigma",
"meta": "cmd",
"score": 0.25940147926344487
}, {
"caption": "\\pi",
"snippet": "\\pi",
"meta": "cmd",
"score": 0.25920934567729714
}, {
"caption": "\\hat{}",
"snippet": "\\hat{$1}",
"meta": "cmd",
"score": 0.25264309033778715
}, {
"caption": "\\hat",
"snippet": "\\hat",
"meta": "cmd",
"score": 0.25264309033778715
}, {
"caption": "\\bibliographystyle{}",
"snippet": "\\bibliographystyle{$1}",
"meta": "cmd",
"score": 0.25122317941387773
}, {
"caption": "\\small",
"snippet": "\\small",
"meta": "cmd",
"score": 0.2447632045426295
}, {
"caption": "\\small{}",
"snippet": "\\small{$1}",
"meta": "cmd",
"score": 0.2447632045426295
}, {
"caption": "\\LaTeX",
"snippet": "\\LaTeX",
"meta": "cmd",
"score": 0.2334089308452787
}, {
"caption": "\\LaTeX{}",
"snippet": "\\LaTeX{$1}",
"meta": "cmd",
"score": 0.2334089308452787
}, {
"caption": "\\cdot",
"snippet": "\\cdot",
"meta": "cmd",
"score": 0.23029085545522762
}, {
"caption": "\\footnote{}",
"snippet": "\\footnote{$1}",
"meta": "cmd",
"score": 0.2253056071787701
}, {
"caption": "\\newtheorem{}[]{}",
"snippet": "\\newtheorem{$1}[$2]{$3}",
"meta": "cmd",
"score": 0.215689795055434
}, {
"caption": "\\newtheorem{}{}",
"snippet": "\\newtheorem{$1}{$2}",
"meta": "cmd",
"score": 0.215689795055434
}, {
"caption": "\\newtheorem{}{}[]",
"snippet": "\\newtheorem{$1}{$2}[$3]",
"meta": "cmd",
"score": 0.215689795055434
}, {
"caption": "\\Delta",
"snippet": "\\Delta",
"meta": "cmd",
"score": 0.21386475063892618
}, {
"caption": "\\tau",
"snippet": "\\tau",
"meta": "cmd",
"score": 0.21236188205859796
}, {
"caption": "\\hfill",
"snippet": "\\hfill",
"meta": "cmd",
"score": 0.2058248088519886
}, {
"caption": "\\leq",
"snippet": "\\leq",
"meta": "cmd",
"score": 0.20498894440637172
}, {
"caption": "\\footnotesize",
"snippet": "\\footnotesize",
"meta": "cmd",
"score": 0.2038592081252624
}, {
"caption": "\\footnotesize{}",
"snippet": "\\footnotesize{$1}",
"meta": "cmd",
"score": 0.2038592081252624
}, {
"caption": "\\large",
"snippet": "\\large",
"meta": "cmd",
"score": 0.20377416734108866
}, {
"caption": "\\large{}",
"snippet": "\\large{$1}",
"meta": "cmd",
"score": 0.20377416734108866
}, {
"caption": "\\sqrt{}",
"snippet": "\\sqrt{$1}",
"meta": "cmd",
"score": 0.20240160977404634
}, {
"caption": "\\epsilon",
"snippet": "\\epsilon",
"meta": "cmd",
"score": 0.2005136761359043
}, {
"caption": "\\Large",
"snippet": "\\Large",
"meta": "cmd",
"score": 0.1987771081149759
}, {
"caption": "\\Large{}",
"snippet": "\\Large{$1}",
"meta": "cmd",
"score": 0.1987771081149759
}, {
"caption": "\\cvitem{}{}",
"snippet": "\\cvitem{$1}{$2}",
"meta": "cmd",
"score": 0.19605476980016281
}, {
"caption": "\\rho",
"snippet": "\\rho",
"meta": "cmd",
"score": 0.1959287380541684
}, {
"caption": "\\omega",
"snippet": "\\omega",
"meta": "cmd",
"score": 0.19326783415115262
}, {
"caption": "\\mathrm{}",
"snippet": "\\mathrm{$1}",
"meta": "cmd",
"score": 0.19117752976172653
}, {
"caption": "\\boldsymbol{}",
"snippet": "\\boldsymbol{$1}",
"meta": "cmd",
"score": 0.18137737738638837
}, {
"caption": "\\boldsymbol",
"snippet": "\\boldsymbol",
"meta": "cmd",
"score": 0.18137737738638837
}, {
"caption": "\\gamma",
"snippet": "\\gamma",
"meta": "cmd",
"score": 0.17940276535431304
}, {
"caption": "\\clearpage",
"snippet": "\\clearpage",
"meta": "cmd",
"score": 0.1789117552185788
}, {
"caption": "\\infty",
"snippet": "\\infty",
"meta": "cmd",
"score": 0.17837290019711305
}, {
"caption": "\\phi",
"snippet": "\\phi",
"meta": "cmd",
"score": 0.17405809173097808
}, {
"caption": "\\partial",
"snippet": "\\partial",
"meta": "cmd",
"score": 0.17168102367966637
}, {
"caption": "\\include{}",
"snippet": "\\include{$1}",
"meta": "cmd",
"score": 0.1547080054979312
}, {
"caption": "\\address{}",
"snippet": "\\address{$1}",
"meta": "cmd",
"score": 0.1525055392611109
}, {
"caption": "\\address[]{}",
"snippet": "\\address[$1]{$2}",
"meta": "cmd",
"score": 0.1525055392611109
}, {
"caption": "\\address{}{}{}",
"snippet": "\\address{$1}{$2}{$3}",
"meta": "cmd",
"score": 0.1525055392611109
}, {
"caption": "\\quad",
"snippet": "\\quad",
"meta": "cmd",
"score": 0.15242755832392743
}, {
"caption": "\\email{}",
"snippet": "\\email{$1}",
"meta": "cmd",
"score": 0.1522294670109857
}, {
"caption": "\\paragraph{}",
"snippet": "\\paragraph{$1}",
"meta": "cmd",
"score": 0.152074250347974
}, {
"caption": "\\varepsilon",
"snippet": "\\varepsilon",
"meta": "cmd",
"score": 0.05411564201390573
}, {
"caption": "\\zeta",
"snippet": "\\zeta",
"meta": "cmd",
"score": 0.023330249803752954
}, {
"caption": "\\eta",
"snippet": "\\eta",
"meta": "cmd",
"score": 0.11088718379889091
}, {
"caption": "\\vartheta",
"snippet": "\\vartheta",
"meta": "cmd",
"score": 0.0025822992078068712
}, {
"caption": "\\iota",
"snippet": "\\iota",
"meta": "cmd",
"score": 0.0024774003791525486
}, {
"caption": "\\kappa",
"snippet": "\\kappa",
"meta": "cmd",
"score": 0.04887876299369008
}, {
"caption": "\\nu",
"snippet": "\\nu",
"meta": "cmd",
"score": 0.09206962821059342
}, {
"caption": "\\xi",
"snippet": "\\xi",
"meta": "cmd",
"score": 0.06496042899265699
}, {
"caption": "\\varpi",
"snippet": "\\varpi",
"meta": "cmd",
"score": 0.0007039358167790341
}, {
"caption": "\\varrho",
"snippet": "\\varrho",
"meta": "cmd",
"score": 0.0011279491613898612
}, {
"caption": "\\varsigma",
"snippet": "\\varsigma",
"meta": "cmd",
"score": 0.0010424880711234978
}, {
"caption": "\\varsigma{}",
"snippet": "\\varsigma{$1}",
"meta": "cmd",
"score": 0.0010424880711234978
}, {
"caption": "\\upsilon",
"snippet": "\\upsilon",
"meta": "cmd",
"score": 0.00420715572598688
}, {
"caption": "\\varphi",
"snippet": "\\varphi",
"meta": "cmd",
"score": 0.03351251516668212
}, {
"caption": "\\chi",
"snippet": "\\chi",
"meta": "cmd",
"score": 0.043373492287805675
}, {
"caption": "\\psi",
"snippet": "\\psi",
"meta": "cmd",
"score": 0.09994508706163642
}, {
"caption": "\\Gamma",
"snippet": "\\Gamma",
"meta": "cmd",
"score": 0.04801549269801977
}, {
"caption": "\\Theta",
"snippet": "\\Theta",
"meta": "cmd",
"score": 0.038090902146599444
}, {
"caption": "\\Lambda",
"snippet": "\\Lambda",
"meta": "cmd",
"score": 0.032206594305977686
}, {
"caption": "\\Xi",
"snippet": "\\Xi",
"meta": "cmd",
"score": 0.01060997225400494
}, {
"caption": "\\Pi",
"snippet": "\\Pi",
"meta": "cmd",
"score": 0.021264671817473237
}, {
"caption": "\\Sigma",
"snippet": "\\Sigma",
"meta": "cmd",
"score": 0.05769642802079917
}, {
"caption": "\\Upsilon",
"snippet": "\\Upsilon",
"meta": "cmd",
"score": 0.00032875192955749566
}, {
"caption": "\\Phi",
"snippet": "\\Phi",
"meta": "cmd",
"score": 0.0538724950042562
}, {
"caption": "\\Psi",
"snippet": "\\Psi",
"meta": "cmd",
"score": 0.03056589143021648
}, {
"caption": "\\Omega",
"snippet": "\\Omega",
"meta": "cmd",
"score": 0.09490387997853639
}]

View file

@ -0,0 +1,85 @@
define [
"ace/ace"
], () ->
Range = ace.require("ace/range").Range
getLastCommandFragment = (lineUpToCursor) ->
if m = lineUpToCursor.match(/(\\[^\\]+)$/)
return m[1]
else
return null
class MetadataManager
constructor: (@$scope, @editor, @element, @Metadata) ->
@debouncer = {} # DocId => Timeout
onChange = (change) =>
if change.remote
return
if change.action not in ['remove', 'insert']
return
cursorPosition = @editor.getCursorPosition()
end = change.end
range = new Range(end.row, 0, end.row, end.column)
lineUpToCursor = @editor.getSession().getTextRange range
if lineUpToCursor.trim() == '%' or lineUpToCursor.slice(0, 1) == '\\'
range = new Range(end.row, 0, end.row, end.column + 80)
lineUpToCursor = @editor.getSession().getTextRange range
commandFragment = getLastCommandFragment lineUpToCursor
linesContainPackage = _.any(
change.lines,
(line) -> line.match(/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
)
linesContainReqPackage = _.any(
change.lines,
(line) -> line.match(/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
)
linesContainLabel = _.any(
change.lines,
(line) -> line.match(/\\label{(.{0,80}?)}/)
)
linesContainMeta =
linesContainPackage or
linesContainLabel or
linesContainReqPackage
lastCommandFragmentIsLabel = commandFragment?.slice(0, 7) == '\\label{'
lastCommandFragmentIsPackage = commandFragment?.slice(0, 11) == '\\usepackage'
lastCommandFragmentIsReqPack = commandFragment?.slice(0, 15) == '\\RequirePackage'
lastCommandFragmentIsMeta =
lastCommandFragmentIsPackage or
lastCommandFragmentIsLabel or
lastCommandFragmentIsReqPack
if linesContainMeta or lastCommandFragmentIsMeta
@scheduleLoadCurrentDocMetaFromServer()
@editor.on "changeSession", (e) =>
e.oldSession.off "change", onChange
e.session.on "change", onChange
loadDocMetaFromServer: (docId) ->
@Metadata.loadDocMetaFromServer docId
scheduleLoadCurrentDocMetaFromServer: () ->
# De-bounce loading labels with a timeout
currentDocId = @$scope.docId
existingTimeout = @debouncer[currentDocId]
if existingTimeout?
clearTimeout(existingTimeout)
delete @debouncer[currentDocId]
@debouncer[currentDocId] = setTimeout(
() =>
@loadDocMetaFromServer currentDocId
delete @debouncer[currentDocId]
, 1000
, this
)
getAllLabels: () ->
@Metadata.getAllLabels()
getAllPackages: () ->
@Metadata.getAllPackages()

View file

@ -5,9 +5,9 @@ define [
Range = ace.require("ace/range").Range
class SpellCheckManager
constructor: (@$scope, @editor, @element, @cache, @$http) ->
constructor: (@$scope, @editor, @element, @cache, @$http, @$q) ->
$(document.body).append @element.find(".spell-check-menu")
@inProgressRequest = null
@updatedLines = []
@highlightedWordManager = new HighlightedWordManager(@editor)
@ -235,11 +235,18 @@ define [
apiRequest: (endpoint, data, callback = (error, result) ->)->
data.token = window.user.id
data._csrf = window.csrfToken
@$http.post("/spelling" + endpoint, data)
# use angular timeout option to cancel request if doc is changed
requestHandler = @$q.defer()
options = {timeout: requestHandler.promise}
httpRequest = @$http.post("/spelling" + endpoint, data, options)
.then (response) =>
callback(null, response.data)
.catch (response) =>
callback(new Error('api failure'))
# provide a method to cancel the request
abortRequest = () ->
requestHandler.resolve()
return { abort: abortRequest }
blacklistedCommandRegex: ///
\\ # initial backslash

View file

@ -13,15 +13,15 @@ define [
@loadRootFolder()
@loadDeletedDocs()
@$scope.$emit "file-tree:initialized"
@$scope.$watch "rootFolder", (rootFolder) =>
if rootFolder?
@recalculateDocList()
@_bindToSocketEvents()
@$scope.multiSelectedCount = 0
$(document).on "click", =>
@clearMultiSelectedEntities()
$scope.$digest()
@ -46,7 +46,7 @@ define [
type: "file"
}
@recalculateDocList()
@ide.socket.on "reciveNewFolder", (parent_folder_id, folder) =>
parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
@$scope.$apply () =>
@ -85,25 +85,25 @@ define [
@ide.fileTreeManager.forEachEntity (entity) ->
entity.selected = false
entity.selected = true
toggleMultiSelectEntity: (entity) ->
entity.multiSelected = !entity.multiSelected
@$scope.multiSelectedCount = @multiSelectedCount()
multiSelectedCount: () ->
count = 0
@forEachEntity (entity) ->
if entity.multiSelected
count++
return count
getMultiSelectedEntities: () ->
entities = []
@forEachEntity (e) ->
if e.multiSelected
entities.push e
return entities
getMultiSelectedEntityChildNodes: () ->
entities = @getMultiSelectedEntities()
paths = {}
@ -125,13 +125,13 @@ define [
if !prefixes[path]?
child_entities.push entity
return child_entities
clearMultiSelectedEntities: () ->
return if @$scope.multiSelectedCount == 0 # Be efficient, this is called a lot on 'click'
@forEachEntity (entity) ->
entity.multiSelected = false
@$scope.multiSelectedCount = 0
multiSelectSelectedEntity: () ->
@findSelectedEntity()?.multiSelected = true
@ -140,7 +140,7 @@ define [
return false if !folder?
entity = @_findEntityByPathInFolder(folder, name)
return entity?
findSelectedEntity: () ->
selected = null
@forEachEntity (entity) ->
@ -178,7 +178,7 @@ define [
parts = path.split("/")
name = parts.shift()
rest = parts.join("/")
if name == "."
return @_findEntityByPathInFolder(folder, rest)
@ -268,7 +268,7 @@ define [
type: "doc"
deleted: true
}
recalculateDocList: () ->
@$scope.docs = []
@forEachEntity (entity, parentFolder, path) =>
@ -287,7 +287,7 @@ define [
return -1
else
return 1
getEntityPath: (entity) ->
@_getEntityPathInFolder @$scope.rootFolder, entity
@ -349,7 +349,7 @@ define [
}
deleteEntity: (entity, callback = (error) ->) ->
# We'll wait for the socket.io notification to
# We'll wait for the socket.io notification to
# delete from scope.
return @ide.queuedHttp {
method: "DELETE"
@ -367,7 +367,7 @@ define [
folder_id: parent_folder.id
_csrf: window.csrfToken
}
_isChildFolder: (parent_folder, child_folder) ->
parent_path = @getEntityPath(parent_folder) or "" # null if root folder
child_path = @getEntityPath(child_folder) or "" # null if root folder

View file

@ -0,0 +1,17 @@
define [
"base"
], (App) ->
App.factory 'files', (ide) ->
Files =
getTeXFiles: () ->
texFiles = []
ide.fileTreeManager.forEachEntity (entity, folder, path) ->
if entity.type == 'doc' && entity?.name?.match?(/.*\.(tex|txt|md)/)
cloned = _.clone(entity)
cloned.path = path
texFiles.push cloned
return texFiles
return Files

View file

@ -1,5 +1,4 @@
define [
], () ->
define [], () ->
class LabelsManager

View file

@ -0,0 +1,13 @@
define [], () ->
class MetadataManager
constructor: (@ide, @$scope, @metadata) ->
@ide.socket.on 'broadcastDocMeta', (data) =>
@metadata.onBroadcastDocMeta data
@$scope.$on 'entity:deleted', @metadata.onEntityDeleted
@$scope.$on 'file:upload:complete', @metadata.fileUploadComplete
loadProjectMetaFromServer: () ->
@metadata.loadProjectMetaFromServer()

View file

@ -0,0 +1,49 @@
define [
"base"
], (App) ->
App.factory 'metadata', ($http, ide) ->
state = {documents: {}}
metadata = {state: state}
metadata.onBroadcastDocMeta = (data) ->
if data.docId? and data.meta?
state.documents[data.docId] = data.meta
metadata.onEntityDeleted = (e, entity) ->
if entity.type == 'doc'
delete state.documents[entity.id]
metadata.onFileUploadComplete = (e, upload) ->
if upload.entity_type == 'doc'
metadata.loadDocMetaFromServer upload.entity_id
metadata.getAllLabels = () ->
_.flatten(meta.labels for docId, meta of state.documents)
metadata.getAllPackages = () ->
packageCommandMapping = {}
for _docId, meta of state.documents
for packageName, commandSnippets of meta.packages
packageCommandMapping[packageName] = commandSnippets
return packageCommandMapping
metadata.loadProjectMetaFromServer = () ->
$http
.get("/project/#{window.project_id}/metadata")
.then (response) ->
{ data } = response
if data.projectMeta
for docId, docMeta of data.projectMeta
state.documents[docId] = docMeta
metadata.loadDocMetaFromServer = (docId) ->
$http
.post(
"/project/#{window.project_id}/doc/#{docId}/metadata",
{_csrf: window.csrfToken}
)
return metadata

View file

@ -85,7 +85,11 @@ define [
isTimeNonMonotonic = timeSinceLastCompile < 0
if isTimeNonMonotonic || timeSinceLastCompile >= AUTO_COMPILE_TIMEOUT
if (!ide.$scope.hasLintingError)
# If user has code check disabled, it is likely because they have
# linting errors that they are ignoring. Therefore it doesn't make sense
# to block auto compiles. It also causes problems where server-provided
# linting errors aren't cleared after typing
if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError)
$scope.recompile(isAutoCompileOnChange: true)
else
# Extend remainder of timeout
@ -109,7 +113,7 @@ define [
toggleAutoCompile(newValue)
event_tracking.sendMB "autocompile-setting-changed", { value: newValue }
if (window.user?.betaProgram or window.showAutoCompileOnboarding) and $scope.autocompile_enabled
if (window.user?.betaProgram or window.autoCompileEnabled) and $scope.autocompile_enabled
toggleAutoCompile(true)
# abort compile if syntax checks fail

View file

@ -43,7 +43,6 @@ define [
# A count of user-facing selected changes. An aggregated change (insertion + deletion) will count
# as only one.
nVisibleSelectedChanges: 0
showPerUserTCNotice: window.showPerUserTCNotice
window.addEventListener "beforeunload", () ->
collapsedStates = {}
@ -598,8 +597,6 @@ define [
$scope.toggleFullTCStateCollapse = () ->
if $scope.project.features.trackChanges
if $scope.reviewPanel.showPerUserTCNotice
$scope.openPerUserTCNoticeModal()
$scope.reviewPanel.fullTCStateCollapsed = !$scope.reviewPanel.fullTCStateCollapsed
else
$scope.openTrackChangesUpgradeModal()
@ -802,11 +799,3 @@ define [
controller: "TrackChangesUpgradeModalController"
scope: $scope.$new()
}
$scope.openPerUserTCNoticeModal = () ->
$scope.reviewPanel.showPerUserTCNotice = false
$modal.open({
templateUrl: "perUserTCNoticeModalTemplate"
}).result.finally () ->
event_tracking.sendMB "shown-per-user-tc-notice"

View file

@ -178,13 +178,6 @@ define [
$scope.state.error = "Sorry, something went wrong resending the invite :("
event.target.blur()
$scope.openMakePublicModal = () ->
$modal.open {
templateUrl: "makePublicModalTemplate"
controller: "MakePublicModalController"
scope: $scope
}
$scope.openMakePrivateModal = () ->
$modal.open {
templateUrl: "makePrivateModalTemplate"

View file

@ -1,12 +1,10 @@
define [
"moment"
"libs/angular-autocomplete/angular-autocomplete"
"libs/ui-bootstrap"
"libs/ng-context-menu-0.1.4"
"libs/underscore-1.3.3"
"libs/algolia-2.5.2"
"libs/jquery.storage"
"libs/fineuploader"
"libs/angular-sanitize-1.2.17"
"libs/angular-cookie"
"libs/passfield"

View file

@ -116,6 +116,10 @@ define [
if $scope.filter == "shared" and project.accessLevel == "owner"
visible = false
# Hide projects from V1 if we only want to see shared projects
if $scope.filter == "shared" and project.isV1Project
visible = false
# Hide projects we don't own if we only want to see owned projects
if $scope.filter == "owned" and project.accessLevel != "owner"
visible = false
@ -129,6 +133,9 @@ define [
if project.archived
visible = false
if $scope.filter == "v1" and !project.isV1Project
visible = false
if visible
$scope.visibleProjects.push project
else

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -4,7 +4,7 @@ ace.define("ace/mode/latex_highlight_rules",["require","exports","module","ace/l
var oop = require("../lib/oop");
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
var LatexHighlightRules = function() {
var LatexHighlightRules = function() {
this.$rules = {
"start" : [{
@ -47,9 +47,9 @@ var LatexHighlightRules = function() {
token : "constant.character.escape",
regex : "\\\\(?:[^a-zA-Z]|[a-zA-Z]+)"
}, {
token : "error",
regex : "^\\s*$",
next : "start"
token : "error",
regex : "^\\s*$",
next : "start"
}, {
defaultToken : "string"
}]
@ -76,16 +76,16 @@ oop.inherits(FoldMode, BaseFoldMode);
(function() {
this.foldingStartMarker = /^\s*\\(begin)|(section|subsection|paragraph)\b|{\s*$/;
this.foldingStartMarker = /^\s*\\(begin|section|subsection|subsubsection|paragraph|part|chapter)\b|{\s*$/;
this.foldingStopMarker = /^\s*\\(end)\b|^\s*}/;
this.getFoldWidgetRange = function(session, foldStyle, row) {
var line = session.doc.getLine(row);
var match = this.foldingStartMarker.exec(line);
if (match) {
if (match[1])
if (match[1] === "begin")
return this.latexBlock(session, row, match[0].length - 1);
if (match[2])
else if (match[1])
return this.latexSection(session, row, match[0].length - 1);
return this.openingBracketBlock(session, "{", row, match.index);
@ -153,7 +153,7 @@ oop.inherits(FoldMode, BaseFoldMode);
};
this.latexSection = function(session, row, column) {
var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph"];
var keywords = ["\\subsection", "\\section", "\\begin", "\\end", "\\paragraph", "\\part", "\\chapter"];
var stream = new TokenIterator(session, row, column);
var token = stream.getCurrentToken();

File diff suppressed because it is too large Load diff

View file

@ -78,6 +78,7 @@
@import "app/invite.less";
@import "app/review-features-page.less";
@import "app/error-pages.less";
@import "app/v1-badge.less";
@import "../js/libs/pdfListView/TextLayer.css";
@import "../js/libs/pdfListView/AnnotationsLayer.css";

View file

@ -1,105 +1,3 @@
@feat-onboard-width: 900px;
.feat-onboard {
position: fixed;
top: 50px;
bottom: 50px;
left: 50%;
width: @feat-onboard-width;
margin-left: -(@feat-onboard-width / 2);
display: flex;
justify-content: center;
align-items: baseline;
background-color: rgba(0, 0, 0, .85);
background-repeat: no-repeat;
background-position-x: 0;
color: #FFF;
text-align: center;
border-radius: 1em;
z-index: 102;
overflow: auto;
}
.feat-onboard-wrapper {
padding: 30px 0;
}
.feat-onboard-title {
color: #FFF;
margin-bottom: 30px;
}
.feat-onboard-description {
max-width: 35em;
margin: 0 auto 5px;
}
.feat-onboard-highlight {
font-weight: bold;
white-space: nowrap;
}
.feat-onboard-adv-title {
font-weight: bold;
white-space: nowrap;
color: #FFF;
font-size: 23px;
margin-top: 0;
}
.feat-onboard-tutorial-wrapper {
display: flex;
align-items: center;
padding: 30px 0 15px;
}
.feat-onboard-video {
width: 616px;
margin: 0 30px;
box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3);
}
.feat-onboard-nav-btn {
border-radius: 1em;
width: 2em;
height: 2em;
text-align: center;
padding: 0;
font-size: 1.3em;
line-height: 1em;
box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3);
&[disabled] {
opacity: 0.2;
}
&:focus,
&:active:focus {
outline: 0;
box-shadow: 0 0 70px 0 rgba(255, 255, 255, 0.3);
}
}
a.feat-onboard-dismiss {
position: absolute;
top: 10px;
right: 10px;
width: 1em;
height: 1em;
line-height: 1em;
font-size: 2.5em;
color: #FFF;
background-color: rgba(0,0,0, .25);
opacity: 0.7;
border-radius: 0.5em;
transition: opacity .15s ease-in-out;
&:hover,
&:focus {
text-decoration: none;
color: #FFF;
opacity: 1;
}
}
.onboarding-autocompile {
display: block;
top: 10px;
@ -143,3 +41,42 @@ a.feat-onboard-dismiss {
right: -9.5px;
}
}
.onboarding-linksharing {
display: block;
top: 10px;
&.popover {
left: auto !important;
}
img {
margin-bottom: 10px;
border: 1px solid @gray-lighter;
}
&::before, &::after {
content: '';
border-width: 11px;
border-style: solid;
border-color: transparent;
top: 7px;
display: block;
position: absolute;
}
// Bottom
&.bottom::before {
border-top-width: 0;
border-bottom-color: rgba(0, 0, 0, .3);
top: -10px;
right: 38px;
}
&.bottom::after {
border-top-width: 0;
border-bottom-color: #f7f7f7;
top: -9.5px;
right: 38px;
}
}

View file

@ -366,6 +366,11 @@ ul.project-list {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.v1-badge {
margin-right: 9px;
margin-left: 7px;
}
}
i.tablesort {
padding-left: 8px;

View file

@ -0,0 +1,10 @@
.v1-badge {
&:extend(.label);
&:extend(.label-default);
vertical-align: 11%;
padding: 1px 3px;
margin: 0 6px;
&:before {
content: "V1";
}
}

View file

@ -4,11 +4,11 @@
*
* Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt.
*/
.qq-uploader {
.qq-uploader-selector {
position: relative;
width: 100%;
}
.qq-uploader {
.qq-uploader-selector {
text-align: center;
.drag-here {
border: 1px dashed #666;
@ -18,7 +18,7 @@
margin-top: 6px;
}
}
/*.qq-upload-button {
/*.qq-upload-button-selector {
display: block;
width: 105px;
padding: 7px 0;
@ -27,13 +27,13 @@
border-bottom: 1px solid #DDD;
color: #FFF;
}
.qq-upload-button-hover {
.qq-upload-button-hover-selector {
background: #CC0000;
}
.qq-upload-button-focus {
.qq-upload-button-focus-selector {
outline: 1px dotted #000000;
}*/
.qq-upload-drop-area, .qq-upload-extra-drop-area {
.qq-upload-drop-area-selector, .qq-upload-extra-drop-area-selector {
position: absolute;
top: 0;
left: 0;
@ -44,7 +44,7 @@
background: @orange;
text-align: center;
}
.qq-upload-drop-area span {
.qq-upload-drop-area-selector span {
display: block;
position: absolute;
top: 50%;
@ -53,7 +53,7 @@
font-size: 16px;
color: white;
}
.qq-upload-extra-drop-area {
.qq-upload-extra-drop-area-selector {
position: relative;
margin-top: 50px;
font-size: 16px;
@ -61,15 +61,15 @@
height: 20px;
min-height: 40px;
}
.qq-upload-drop-area-active {
.qq-upload-drop-area-active-selector {
background: darken(@orange, 15%);
}
.qq-upload-list {
.qq-upload-list-selector {
margin: 0;
padding: 0;
list-style: none;
}
.qq-upload-list li {
.qq-upload-list-selector li {
margin: 0;
margin-top: 10px;
padding: 9px;
@ -77,74 +77,74 @@
font-size: 16px;
background-color: @gray-lightest;
}
.qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-failed-text, .qq-upload-finished, .qq-upload-delete {
.qq-upload-file-selector, .qq-upload-spinner-selector, .qq-upload-size-selector, .qq-upload-cancel-selector, .qq-upload-retry-selector, .qq-upload-failed-text-selector, .qq-upload-finished-selector, .qq-upload-delete-selector {
margin-right: 12px;
}
.qq-upload-file {
.qq-upload-file-selector {
}
.qq-upload-spinner {
.qq-upload-spinner-selector {
display: inline-block;
width: 15px;
height: 15px;
vertical-align: text-bottom;
}
.qq-drop-processing {
.qq-drop-processing-selector {
display: none;
}
.qq-drop-processing-spinner {
.qq-drop-processing-spinner-selector {
display: inline-block;
background: url("processing.gif");
width: 24px;
height: 24px;
vertical-align: text-bottom;
}
.qq-upload-finished {
.qq-upload-finished-selector {
display:none;
width:15px;
height:15px;
vertical-align:text-bottom;
}
.qq-upload-retry, .qq-upload-delete {
.qq-upload-retry-selector, .qq-upload-delete-selector {
display: none;
// color: #000000;
}
.qq-upload-cancel, .qq-upload-delete {
.qq-upload-cancel-selector, .qq-upload-delete-selector {
// color: #000000;
}
.qq-upload-retryable .qq-upload-retry {
.qq-upload-retryable-selector .qq-upload-retry-selector {
display: inline;
}
.qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-delete {
.qq-upload-size-selector, .qq-upload-cancel-selector, .qq-upload-retry-selector, .qq-upload-delete-selector {
font-size: 12px;
font-weight: normal;
}
.qq-upload-failed-text {
.qq-upload-failed-text-selector {
display: none;
font-style: italic;
font-weight: bold;
}
.qq-upload-failed-icon {
.qq-upload-failed-icon-selector {
display:none;
width:15px;
height:15px;
vertical-align:text-bottom;
}
.qq-upload-fail .qq-upload-failed-text {
.qq-upload-fail-selector .qq-upload-failed-text-selector {
display: inline;
}
.qq-upload-retrying .qq-upload-failed-text {
.qq-upload-retrying-selector .qq-upload-failed-text-selector {
display: inline;
color: #D60000;
}
.qq-upload-list li.qq-upload-success {
.qq-upload-list-selector li.qq-upload-success-selector {
background-color: @green;
color: #FFFFFF;
}
.qq-upload-list li.qq-upload-fail {
.qq-upload-list-selector li.qq-upload-fail-selector {
background-color: @red;
color: #FFFFFF;
}
.qq-progress-bar {
.qq-progress-bar-selector {
width: 0%;
height: @line-height-computed;
margin-bottom: @line-height-computed / 2;
@ -156,5 +156,9 @@
.box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
.transition(width .6s ease);
border-radius: @border-radius-base 0 0 @border-radius-base;
display: none;
}
a.qq-btn {
&:hover {
cursor: pointer !important;
}
}

View file

@ -1,119 +0,0 @@
chai = require('chai')
chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Labels/LabelsController"
SandboxedModule = require('sandboxed-module')
describe 'LabelsController', ->
beforeEach ->
@projectId = 'somekindofid'
@EditorRealTimeController = {
emitToRoom: sinon.stub()
}
@LabelsHandler = {
getAllLabelsForProject: sinon.stub()
getLabelsForDoc: sinon.stub()
}
@LabelsController = SandboxedModule.require modulePath, requires:
'logger-sharelatex': {log: sinon.stub(), err: sinon.stub()}
'../Editor/EditorRealTimeController': @EditorRealTimeController
'./LabelsHandler': @LabelsHandler
describe 'getAllLabels', ->
beforeEach ->
@fakeLabels = {'somedoc': ['a_label']}
@LabelsHandler.getAllLabelsForProject = sinon.stub().callsArgWith(1, null, @fakeLabels)
@req = {params: {project_id: @projectId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getAllLabelsForProject', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@LabelsHandler.getAllLabelsForProject.callCount.should.equal 1
@LabelsHandler.getAllLabelsForProject.calledWith(@projectId).should.equal true
it 'should call not call next with an error', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@next.callCount.should.equal 0
it 'should send a json response', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@res.json.callCount.should.equal 1
expect(@res.json.lastCall.args[0]).to.have.all.keys ['projectId', 'projectLabels']
describe 'when LabelsHandler.getAllLabelsForProject produces an error', ->
beforeEach ->
@LabelsHandler.getAllLabelsForProject = sinon.stub().callsArgWith(1, new Error('woops'))
@req = {params: {project_id: @projectId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getAllLabelsForProject', () ->
@LabelsController.getAllLabels(@req, @res, @next)
@LabelsHandler.getAllLabelsForProject.callCount.should.equal 1
@LabelsHandler.getAllLabelsForProject.calledWith(@projectId).should.equal true
it 'should call next with an error', ->
@LabelsController.getAllLabels(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not send a json response', ->
@LabelsController.getAllLabels(@req, @res, @next)
@res.json.callCount.should.equal 0
describe 'broadcastLabelsForDoc', ->
beforeEach ->
@LabelsHandler.getLabelsForDoc = sinon.stub().callsArgWith(2, null, @fakeLabels)
@EditorRealTimeController.emitToRoom = sinon.stub()
@docId = 'somedoc'
@req = {params: {project_id: @projectId, doc_id: @docId}}
@res = {sendStatus: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getLabelsForDoc', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@LabelsHandler.getLabelsForDoc.callCount.should.equal 1
@LabelsHandler.getLabelsForDoc.calledWith(@projectId).should.equal true
it 'should call not call next with an error', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@next.callCount.should.equal 0
it 'should send a success response', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@res.sendStatus.callCount.should.equal 1
@res.sendStatus.calledWith(200).should.equal true
it 'should emit a message to room', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@EditorRealTimeController.emitToRoom.callCount.should.equal 1
lastCall = @EditorRealTimeController.emitToRoom.lastCall
expect(lastCall.args[0]).to.equal @projectId
expect(lastCall.args[1]).to.equal 'broadcastDocLabels'
expect(lastCall.args[2]).to.have.all.keys ['docId', 'labels']
describe 'when LabelsHandler.getLabelsForDoc produces an error', ->
beforeEach ->
@LabelsHandler.getLabelsForDoc = sinon.stub().callsArgWith(2, new Error('woops'))
@EditorRealTimeController.emitToRoom = sinon.stub()
@docId = 'somedoc'
@req = {params: {project_id: @projectId, doc_id: @docId}}
@res = {json: sinon.stub()}
@next = sinon.stub()
it 'should call LabelsHandler.getLabelsForDoc', () ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@LabelsHandler.getLabelsForDoc.callCount.should.equal 1
@LabelsHandler.getLabelsForDoc.calledWith(@projectId).should.equal true
it 'should call next with an error', ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@next.callCount.should.equal 1
expect(@next.lastCall.args[0]).to.be.instanceof Error
it 'should not send a json response', ->
@LabelsController.broadcastLabelsForDoc(@req, @res, @next)
@res.json.callCount.should.equal 0

View file

@ -1,134 +0,0 @@
chai = require('chai')
chai.should()
expect = chai.expect
sinon = require("sinon")
modulePath = "../../../../app/js/Features/Labels/LabelsHandler"
SandboxedModule = require('sandboxed-module')
describe 'LabelsHandler', ->
beforeEach ->
@projectId = 'someprojectid'
@docId = 'somedocid'
@ProjectEntityHandler = {
getAllDocs: sinon.stub()
getDoc: sinon.stub()
}
@DocumentUpdaterHandler = {
flushDocToMongo: sinon.stub()
}
@LabelsHandler = SandboxedModule.require modulePath, requires:
'../Project/ProjectEntityHandler': @ProjectEntityHandler
'../DocumentUpdater/DocumentUpdaterHandler': @DocumentUpdaterHandler
describe 'extractLabelsFromDoc', ->
beforeEach ->
@lines = [
'one',
'two',
'three \\label{aaa}',
'four five',
'\\label{bbb}',
'six seven'
]
it 'should extract all the labels', ->
docLabels = @LabelsHandler.extractLabelsFromDoc @lines
expect(docLabels).to.deep.equal ['aaa', 'bbb']
describe 'extractLabelsFromProjectDocs', ->
beforeEach ->
@docs = {
'doc_one': {
_id: 'id_one',
lines: ['one', '\\label{aaa} two', 'three']
},
'doc_two': {
_id: 'id_two',
lines: ['four']
},
'doc_three': {
_id: 'id_three',
lines: ['\\label{bbb}', 'five six', 'seven eight \\label{ccc} nine']
}
}
it 'should extract all the labels', ->
projectLabels = @LabelsHandler.extractLabelsFromProjectDocs @docs
expect(projectLabels).to.deep.equal {
'id_one': ['aaa'],
'id_two': [],
'id_three': ['bbb', 'ccc']
}
describe 'getLabelsForDoc', ->
beforeEach ->
@fakeLines = ['one', '\\label{aaa}', 'two']
@fakeLabels = ['aaa']
@DocumentUpdaterHandler.flushDocToMongo = sinon.stub().callsArgWith(2, null)
@ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @fakeLines)
@LabelsHandler.extractLabelsFromDoc = sinon.stub().returns(@fakeLabels)
@call = (callback) =>
@LabelsHandler.getLabelsForDoc @projectId, @docId, callback
it 'should not produce an error', (done) ->
@call (err, docLabels) =>
expect(err).to.equal null
done()
it 'should produce docLabels', (done) ->
@call (err, docLabels) =>
expect(docLabels).to.equal @fakeLabels
done()
it 'should call flushDocToMongo', (done) ->
@call (err, docLabels) =>
@DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal 1
@DocumentUpdaterHandler.flushDocToMongo.calledWith(@projectId, @docId).should.equal true
done()
it 'should call getDoc', (done) ->
@call (err, docLabels) =>
@ProjectEntityHandler.getDoc.callCount.should.equal 1
@ProjectEntityHandler.getDoc.calledWith(@projectId, @docId).should.equal true
done()
it 'should call extractLabelsFromDoc', (done) ->
@call (err, docLabels) =>
@LabelsHandler.extractLabelsFromDoc.callCount.should.equal 1
@LabelsHandler.extractLabelsFromDoc.calledWith(@fakeLines).should.equal true
done()
describe 'getAllLabelsForProject', ->
beforeEach ->
@fakeDocs = {
'doc_one': {lines: ['\\label{aaa}']}
}
@fakeLabels = ['aaa']
@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
@ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @fakeDocs)
@LabelsHandler.extractLabelsFromProjectDocs = sinon.stub().returns(@fakeLabels)
@call = (callback) =>
@LabelsHandler.getAllLabelsForProject @projectId, callback
it 'should not produce an error', (done) ->
@call (err, projectLabels) =>
expect(err).to.equal null
done()
it 'should produce projectLabels', (done) ->
@call (err, projectLabels) =>
expect(projectLabels).to.equal @fakeLabels
done()
it 'should call getAllDocs', (done) ->
@call (err, projectLabels) =>
@ProjectEntityHandler.getAllDocs.callCount.should.equal 1
@ProjectEntityHandler.getAllDocs.calledWith(@projectId).should.equal true
done()
it 'should call extractLabelsFromDoc', (done) ->
@call (err, docLabels) =>
@LabelsHandler.extractLabelsFromProjectDocs.callCount.should.equal 1
@LabelsHandler.extractLabelsFromProjectDocs.calledWith(@fakeDocs).should.equal true
done()

View file

@ -163,7 +163,7 @@ class User
@request.get {
url: "/register"
}, (err, response, body) =>
return callback(error) if error?
return callback(err) if err?
csrfMatches = body.match("window.csrfToken = \"(.*?)\";")
if !csrfMatches?
return callback(new Error("no csrf token found"))

View file

@ -1,5 +1,4 @@
Settings = require('settings-sharelatex')
redis = require('redis-sharelatex')
logger = require("logger-sharelatex")
Async = require('async')

View file

@ -1,4 +1,4 @@
BASE_URL = "http://localhost:3000"
BASE_URL = "http://#{process.env["HTTP_TEST_HOST"] or "localhost"}:3000"
module.exports = require("request").defaults({
baseUrl: BASE_URL,
followRedirect: false

View file

@ -1,26 +1,40 @@
#! /usr/bin/env bash
# If you're running on OS X, you probably need to manually
# 'rm -r node_modules/bcrypt; npm install bcrypt' inside
# the docker container, before it will start.
# npm rebuild bcrypt
# If you're running on OS X, you probably need to rebuild
# some dependencies in the docker container, before it will start.
#
# npm rebuild --update-binary
echo ">> Starting server..."
grunt --no-color forever:app:start
echo ">> Server started"
echo ">> Waiting for Server"
sleep 5
count=1
max_wait=60
echo ">> Running acceptance tests..."
grunt --no-color mochaTest:acceptance
_test_exit_code=$?
while [ $count -le $max_wait ]
do
if nc -z localhost 3000
then
echo ">> Server Started"
echo ">> Killing server"
echo ">> Running acceptance tests..."
grunt --no-color mochaTest:acceptance
_test_exit_code=$?
grunt --no-color forever:app:stop
echo ">> Killing server"
echo ">> Done"
grunt --no-color forever:app:stop
exit $_test_exit_code
echo ">> Done"
exit $_test_exit_code
fi
sleep 1
echo -n "."
count=$((count+1))
done
exit 1

View file

@ -33,23 +33,19 @@ describe "Opening", ->
return done(err)
logger.log "smoke test: clearing rate limit "
require("../../../app/js/infrastructure/RateLimiter.js").clearRateLimit "open-project", "#{Settings.smokeTest.projectId}:#{Settings.smokeTest.userId}", ->
logger.log "smoke test: hitting /register"
logger.log "smoke test: hitting dev/csrf"
command = """
curl -H "X-Forwarded-Proto: https" -c #{cookeFilePath} #{buildUrl('register')}
curl -H "X-Forwarded-Proto: https" -c #{cookeFilePath} #{buildUrl('dev/csrf')}
"""
child.exec command, (err, stdout, stderr)->
if err? then done(err)
csrfMatches = stdout.match("<input name=\"_csrf\" type=\"hidden\" value=\"(.*?)\">")
if !csrfMatches?
logger.err stdout:stdout, "smoke test: does not have csrf token"
return done("smoke test: does not have csrf token")
csrf = csrfMatches[1]
csrf = stdout
logger.log "smoke test: converting cookie file 1"
convertCookieFile (err) ->
return done(err) if err?
logger.log "smoke test: hitting /register with csrf"
logger.log "smoke test: hitting /login with csrf"
command = """
curl -c #{cookeFilePath} -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"#{csrf}", "email":"#{Settings.smokeTest.user}", "password":"#{Settings.smokeTest.password}"}' #{buildUrl('register')}
curl -c #{cookeFilePath} -H "Content-Type: application/json" -H "X-Forwarded-Proto: https" -d '{"_csrf":"#{csrf}", "email":"#{Settings.smokeTest.user}", "password":"#{Settings.smokeTest.password}"}' #{buildUrl('login')}
"""
child.exec command, (err) ->
return done(err) if err?

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