mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-14 22:25:27 +00:00
Merge branch 'master' into pr-ol-beta-editor-styling
This commit is contained in:
commit
f21870aac2
217 changed files with 10366 additions and 1351 deletions
3
services/web/.gitignore
vendored
3
services/web/.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
36
services/web/Jenkinsfile
vendored
36
services/web/Jenkinsfile
vendored
|
@ -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
77
services/web/Makefile
Normal 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
|
|
@ -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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
66
services/web/app/coffee/Features/Metadata/MetaHandler.coffee
Normal file
66
services/web/app/coffee/Features/Metadata/MetaHandler.coffee
Normal 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
|
@ -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
|
||||
|
|
|
@ -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)->
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)->
|
||||
|
|
16
services/web/app/coffee/infrastructure/Features.coffee
Normal file
16
services/web/app/coffee/infrastructure/Features.coffee
Normal 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}")
|
|
@ -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'
|
||||
|
|
|
@ -2,6 +2,7 @@ version = {
|
|||
"pdfjs": "1.7.225"
|
||||
"moment": "2.9.0"
|
||||
"ace": "1.2.5"
|
||||
"fineuploader": "5.15.4"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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')}
|
||||
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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();"
|
||||
) ×
|
||||
.feat-onboard-wrapper
|
||||
h1.feat-onboard-title
|
||||
| Introducing
|
||||
span.feat-onboard-highlight Commenting
|
||||
| &
|
||||
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 “Add comment”
|
||||
| .
|
||||
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 “Track Changes”
|
||||
| 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
|
||||
span.feat-onboard-highlight “Accept”
|
||||
| or
|
||||
span.feat-onboard-highlight “Reject”
|
||||
| to incorporate or discard an individual change.
|
|
@ -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 }}
|
||||
|
|
|
@ -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();",
|
||||
|
|
|
@ -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()"
|
||||
) ×
|
||||
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
|
||||
| #{translate("turn_tc_on_individuals")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{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(
|
||||
|
|
|
@ -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")}
|
||||
|
|
||||
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')}.
|
||||
|
|
||||
|
@ -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()"
|
||||
) ×
|
||||
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(
|
||||
|
|
|
@ -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 = {
|
||||
|
|
40
services/web/app/views/project/list/item.pug
Normal file
40
services/web/app/views/project/list/item.pug
Normal 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)"
|
||||
) ×
|
||||
|
||||
.col-xs-2
|
||||
span.owner {{ownerName()}}
|
||||
span(ng-if="isLinkSharingProject(project)")
|
||||
|
|
||||
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}}
|
|
@ -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")}
|
||||
|
||||
|
|
|
@ -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)"
|
||||
) ×
|
||||
|
||||
.col-xs-2
|
||||
span.owner {{ownerName()}}
|
||||
span(ng-if="isLinkSharingProject(project)")
|
||||
|
|
||||
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
|
||||
|
|
|
@ -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="#",
|
||||
|
|
18
services/web/app/views/project/list/v1-item.pug
Normal file
18
services/web/app/views/project/list/v1-item.pug
Normal 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}}
|
|
@ -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")}
|
||||
|
|
||||
a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
|
||||
when "canceled"
|
||||
|
|
|
@ -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}"
|
||||
});
|
||||
|
||||
|
||||
|
6
services/web/app/views/v1-tooltip.pug
Normal file
6
services/web/app/views/v1-tooltip.pug
Normal 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
|
4
services/web/bin/acceptance_test
Executable file
4
services/web/bin/acceptance_test
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
set -e;
|
||||
MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000"
|
||||
$MOCHA "$@"
|
16
services/web/bin/compile_acceptance_tests
Executable file
16
services/web/bin/compile_acceptance_tests
Executable 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
23
services/web/bin/compile_app
Executable 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
|
15
services/web/bin/compile_unit_tests
Executable file
15
services/web/bin/compile_unit_tests
Executable 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
|
26
services/web/bin/generate_volumes_file
Executable file
26
services/web/bin/generate_volumes_file
Executable 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
14
services/web/bin/unit_test
Executable 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
|
||||
|
|
@ -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"
|
||||
|
|
36
services/web/docker-compose.yml
Normal file
36
services/web/docker-compose.yml
Normal 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
|
30
services/web/docker-shared.template.yml
Normal file
30
services/web/docker-shared.template.yml
Normal 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
|
18
services/web/modules/.gitignore
vendored
18
services/web/modules/.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -23,4 +23,4 @@ define [
|
|||
|
||||
$scope.dismiss = () ->
|
||||
$scope.onboarding.autoCompile = 'dismissed'
|
||||
event_tracking.sendMB "shown-autocompile-onboarding"
|
||||
event_tracking.sendMB "shown-autocompile-onboarding-2"
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}]
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
17
services/web/public/coffee/ide/files/services/files.coffee
Normal file
17
services/web/public/coffee/ide/files/services/files.coffee
Normal 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
|
|
@ -1,5 +1,4 @@
|
|||
define [
|
||||
], () ->
|
||||
define [], () ->
|
||||
|
||||
class LabelsManager
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 |
BIN
services/web/public/img/onboarding/linksharing/link-sharing.png
Normal file
BIN
services/web/public/img/onboarding/linksharing/link-sharing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -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();
|
||||
|
|
7455
services/web/public/js/libs/fineuploader-5.15.4.js
Normal file
7455
services/web/public/js/libs/fineuploader-5.15.4.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
10
services/web/public/stylesheets/app/v1-badge.less
Normal file
10
services/web/public/stylesheets/app/v1-badge.less
Normal file
|
@ -0,0 +1,10 @@
|
|||
.v1-badge {
|
||||
&:extend(.label);
|
||||
&:extend(.label-default);
|
||||
vertical-align: 11%;
|
||||
padding: 1px 3px;
|
||||
margin: 0 6px;
|
||||
&:before {
|
||||
content: "V1";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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"))
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require('redis-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue