mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge remote-tracking branch 'origin/master' into afc-update-user-references
This commit is contained in:
commit
57775e60b1
130 changed files with 4315 additions and 1011 deletions
|
@ -180,7 +180,7 @@ clean_css:
|
|||
rm -f public/stylesheets/*.css*
|
||||
|
||||
clean_ci:
|
||||
docker-compose down -v
|
||||
docker-compose down -v -t 0
|
||||
|
||||
test: test_unit test_frontend test_acceptance
|
||||
|
||||
|
@ -204,7 +204,7 @@ test_acceptance_app_start_service: test_clean # stop service and clear dbs
|
|||
docker-compose ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance
|
||||
|
||||
test_acceptance_app_stop_service:
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} stop -t 0 test_acceptance redis mongo
|
||||
|
||||
test_acceptance_app_run:
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS}
|
||||
|
@ -224,7 +224,7 @@ test_acceptance_module: $(MODULE_MAKEFILES)
|
|||
fi
|
||||
|
||||
test_clean:
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} down -v
|
||||
docker-compose ${DOCKER_COMPOSE_FLAGS} down -v -t 0
|
||||
|
||||
ci:
|
||||
MOCHA_ARGS="--reporter tap" \
|
||||
|
|
|
@ -62,7 +62,7 @@ test_acceptance_start_service: test_acceptance_stop_service
|
|||
$(DOCKER_COMPOSE) up -d test_acceptance
|
||||
|
||||
test_acceptance_stop_service:
|
||||
$(DOCKER_COMPOSE) stop test_acceptance redis mongo
|
||||
$(DOCKER_COMPOSE) stop -t 0 test_acceptance redis mongo
|
||||
|
||||
test_acceptance_run:
|
||||
$(DOCKER_COMPOSE) exec -T test_acceptance npm -q run test:acceptance:dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/js
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
BetaProgramHandler = require './BetaProgramHandler'
|
||||
UserLocator = require "../User/UserLocator"
|
||||
UserGetter = require "../User/UserGetter"
|
||||
Settings = require "settings-sharelatex"
|
||||
logger = require 'logger-sharelatex'
|
||||
AuthenticationController = require '../Authentication/AuthenticationController'
|
||||
|
@ -30,7 +30,7 @@ module.exports = BetaProgramController =
|
|||
optInPage: (req, res, next)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log {user_id}, "showing beta participation page for user"
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
if err
|
||||
logger.err {err, user_id}, "error fetching user"
|
||||
return next(err)
|
||||
|
|
|
@ -27,7 +27,7 @@ module.exports = CollaboratorsInviteController =
|
|||
_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
|
||||
if Settings.restrictInvitesToExistingAccounts == true
|
||||
logger.log {email}, "checking if user exists with this email"
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
|
||||
UserGetter.getUserByMainEmail email, {_id: 1}, (err, user) ->
|
||||
return callback(err) if err?
|
||||
userExists = user? and user?._id?
|
||||
callback(null, userExists)
|
||||
|
|
|
@ -32,7 +32,7 @@ module.exports = CollaboratorsInviteHandler =
|
|||
|
||||
_trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
|
||||
email = invite.email
|
||||
UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
|
||||
UserGetter.getUserByMainEmail email, {_id: 1}, (err, existingUser) ->
|
||||
if err?
|
||||
logger.err {projectId, email}, "error checking if user exists"
|
||||
return callback(err)
|
||||
|
|
|
@ -7,7 +7,17 @@ module.exports =
|
|||
exportProject: (req, res) ->
|
||||
{project_id, brand_variation_id} = req.params
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
ExportsHandler.exportProject project_id, user_id, brand_variation_id, (err, export_data) ->
|
||||
export_params = {
|
||||
project_id: project_id,
|
||||
brand_variation_id: brand_variation_id,
|
||||
user_id: user_id
|
||||
}
|
||||
|
||||
if req.body && req.body.firstName && req.body.lastName
|
||||
export_params.first_name = req.body.firstName.trim()
|
||||
export_params.last_name = req.body.lastName.trim()
|
||||
|
||||
ExportsHandler.exportProject export_params, (err, export_data) ->
|
||||
return next(err) if err?
|
||||
logger.log
|
||||
user_id:user_id
|
||||
|
|
|
@ -10,8 +10,8 @@ settings = require 'settings-sharelatex'
|
|||
|
||||
module.exports = ExportsHandler = self =
|
||||
|
||||
exportProject: (project_id, user_id, brand_variation_id, callback=(error, export_data) ->) ->
|
||||
self._buildExport project_id, user_id, brand_variation_id, (err, export_data) ->
|
||||
exportProject: (export_params, callback=(error, export_data) ->) ->
|
||||
self._buildExport export_params, (err, export_data) ->
|
||||
return callback(err) if err?
|
||||
self._requestExport export_data, (err, export_v1_id) ->
|
||||
return callback(err) if err?
|
||||
|
@ -19,7 +19,10 @@ module.exports = ExportsHandler = self =
|
|||
# TODO: possibly store the export data in Mongo
|
||||
callback null, export_data
|
||||
|
||||
_buildExport: (project_id, user_id, brand_variation_id, callback=(err, export_data) ->) ->
|
||||
_buildExport: (export_params, callback=(err, export_data) ->) ->
|
||||
project_id = export_params.project_id
|
||||
user_id = export_params.user_id
|
||||
brand_variation_id = export_params.brand_variation_id
|
||||
jobs =
|
||||
project: (cb) ->
|
||||
ProjectGetter.getProject project_id, cb
|
||||
|
@ -43,6 +46,10 @@ module.exports = ExportsHandler = self =
|
|||
logger.err err:err, project_id: project_id
|
||||
return callback(err)
|
||||
|
||||
if export_params.first_name && export_params.last_name
|
||||
user.first_name = export_params.first_name
|
||||
user.last_name = export_params.last_name
|
||||
|
||||
export_data =
|
||||
project:
|
||||
id: project_id
|
||||
|
|
|
@ -5,7 +5,8 @@ logger = require 'logger-sharelatex'
|
|||
|
||||
module.exports = LinkedFilesController = {
|
||||
Agents: {
|
||||
url: require('./UrlAgent')
|
||||
url: require('./UrlAgent'),
|
||||
project_file: require('./ProjectFileAgent')
|
||||
}
|
||||
|
||||
createLinkedFile: (req, res, next) ->
|
||||
|
@ -22,11 +23,17 @@ module.exports = LinkedFilesController = {
|
|||
|
||||
linkedFileData = Agent.sanitizeData(data)
|
||||
linkedFileData.provider = provider
|
||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.send(204) # created
|
||||
}
|
||||
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
|
||||
return Agent.handleError(err, req, res, next) if err?
|
||||
return res.sendStatus(403) if !allowed
|
||||
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
|
||||
return Agent.handleError(err) if err?
|
||||
linkedFileData = newLinkedFileData
|
||||
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
|
||||
if error?
|
||||
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
|
||||
return Agent.handleError(error, req, res, next)
|
||||
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) ->
|
||||
return next(error) if error?
|
||||
res.json(new_file_id: file._id) # created
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
ProjectLocator = require('../Project/ProjectLocator')
|
||||
ProjectGetter = require('../Project/ProjectGetter')
|
||||
DocstoreManager = require('../Docstore/DocstoreManager')
|
||||
FileStoreHandler = require('../FileStore/FileStoreHandler')
|
||||
FileWriter = require('../../infrastructure/FileWriter')
|
||||
_ = require "underscore"
|
||||
Settings = require 'settings-sharelatex'
|
||||
|
||||
|
||||
AccessDeniedError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'AccessDenied'
|
||||
error.__proto__ = AccessDeniedError.prototype
|
||||
return error
|
||||
AccessDeniedError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadEntityTypeError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadEntityType'
|
||||
error.__proto__ = BadEntityTypeError.prototype
|
||||
return error
|
||||
BadEntityTypeError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
BadDataError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = BadDataError.prototype
|
||||
return error
|
||||
BadDataError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
ProjectNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'ProjectNotFound'
|
||||
error.__proto__ = ProjectNotFoundError.prototype
|
||||
return error
|
||||
ProjectNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
SourceFileNotFoundError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = 'BadData'
|
||||
error.__proto__ = SourceFileNotFoundError.prototype
|
||||
return error
|
||||
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
|
||||
|
||||
|
||||
module.exports = ProjectFileAgent =
|
||||
|
||||
sanitizeData: (data) ->
|
||||
return _.pick(
|
||||
data,
|
||||
'source_project_id',
|
||||
'source_entity_path'
|
||||
)
|
||||
|
||||
_validate: (data) ->
|
||||
return (
|
||||
data.source_project_id? &&
|
||||
data.source_entity_path?
|
||||
)
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
callback = _.once(callback)
|
||||
{ source_project_id } = data
|
||||
return callback(new BadDataError()) if !source_project_id?
|
||||
ProjectGetter.getProject source_project_id, (err, project) ->
|
||||
return callback(err) if err?
|
||||
return callback(new ProjectNotFoundError()) if !project?
|
||||
callback(err, _.extend(data, {source_project_display_name: project.name}))
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{source_project_id, source_entity_path} = data
|
||||
AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) ->
|
||||
return callback(err) if err?
|
||||
callback(null, canRead)
|
||||
|
||||
writeIncomingFileToDisk:
|
||||
(project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
if !ProjectFileAgent._validate(data)
|
||||
return callback(new BadDataError())
|
||||
{source_project_id, source_entity_path} = data
|
||||
ProjectLocator.findElementByPath {
|
||||
project_id: source_project_id,
|
||||
path: source_entity_path
|
||||
}, (err, entity, type) ->
|
||||
if err?
|
||||
if err.toString().match(/^not found.*/)
|
||||
err = new SourceFileNotFoundError()
|
||||
return callback(err)
|
||||
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
|
||||
|
||||
_writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) ->
|
||||
callback = _.once(callback)
|
||||
if type == 'doc'
|
||||
DocstoreManager.getDoc project_id, entity_id, (err, lines) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeLinesToDisk entity_id, lines, callback
|
||||
else if type == 'file'
|
||||
FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) ->
|
||||
return callback(err) if err?
|
||||
FileWriter.writeStreamToDisk entity_id, fileStream, callback
|
||||
else
|
||||
callback(new BadEntityTypeError())
|
||||
|
||||
handleError: (error, req, res, next) ->
|
||||
if error instanceof AccessDeniedError
|
||||
res.status(403).send("You do not have access to this project")
|
||||
else if error instanceof BadDataError
|
||||
res.status(400).send("The submitted data is not valid")
|
||||
else if error instanceof BadEntityTypeError
|
||||
res.status(400).send("The file is the wrong type")
|
||||
else if error instanceof SourceFileNotFoundError
|
||||
res.status(404).send("Source file not found")
|
||||
else if error instanceof ProjectNotFoundError
|
||||
res.status(404).send("Project not found")
|
||||
else
|
||||
next(error)
|
||||
next()
|
|
@ -27,6 +27,12 @@ module.exports = UrlAgent = {
|
|||
url: @._prependHttpIfNeeded(data.url)
|
||||
}
|
||||
|
||||
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
|
||||
return callback(null, data)
|
||||
|
||||
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
|
||||
callback(null, true)
|
||||
|
||||
writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
url = data.url
|
||||
|
@ -65,4 +71,4 @@ module.exports = UrlAgent = {
|
|||
if !Settings.apis?.linkedUrlProxy?.url?
|
||||
throw new Error('no linked url proxy configured')
|
||||
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ logger = require("logger-sharelatex")
|
|||
module.exports =
|
||||
|
||||
generateAndEmailResetToken:(email, callback = (error, exists) ->)->
|
||||
UserGetter.getUser email:email, (err, user)->
|
||||
UserGetter.getUserByMainEmail email, (err, user)->
|
||||
if err then return callback(err)
|
||||
if !user? or user.holdingAccount
|
||||
logger.err email:email, "user could not be found for password reset"
|
||||
|
|
|
@ -25,6 +25,7 @@ Sources = require "../Authorization/Sources"
|
|||
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
|
||||
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
|
||||
Modules = require '../../infrastructure/Modules'
|
||||
ProjectEntityHandler = require './ProjectEntityHandler'
|
||||
crypto = require 'crypto'
|
||||
|
||||
module.exports = ProjectController =
|
||||
|
@ -138,6 +139,33 @@ module.exports = ProjectController =
|
|||
return next(err) if err?
|
||||
res.sendStatus 200
|
||||
|
||||
userProjectsJson: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
ProjectGetter.findAllUsersProjects user_id,
|
||||
'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) ->
|
||||
return next(err) if err?
|
||||
projects = ProjectController._buildProjectList(projects)
|
||||
.filter((p) -> !p.archived)
|
||||
.filter((p) -> !p.isV1Project)
|
||||
.map((p) -> {_id: p.id, name: p.name, accessLevel: p.accessLevel})
|
||||
|
||||
res.json({projects: projects})
|
||||
|
||||
projectEntitiesJson: (req, res, next) ->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
project_id = req.params.Project_id
|
||||
ProjectGetter.getProject project_id, (err, project) ->
|
||||
return next(err) if err?
|
||||
ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) ->
|
||||
return next(err) if err?
|
||||
entities = docs.concat(files)
|
||||
.sort (a, b) -> a.path > b.path # Sort by path ascending
|
||||
.map (e) -> {
|
||||
path: e.path,
|
||||
type: if e.doc? then 'doc' else 'file'
|
||||
}
|
||||
res.json({project_id: project_id, entities: entities})
|
||||
|
||||
projectListPage: (req, res, next)->
|
||||
timer = new metrics.Timer("project-list")
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
|
@ -313,6 +341,7 @@ module.exports = ProjectController =
|
|||
maxDocLength: Settings.max_doc_length
|
||||
useV2History: !!project.overleaf?.history?.display
|
||||
showRichText: req.query?.rt == 'true'
|
||||
showTestControls: req.query?.tc == 'true' || user.isAdmin
|
||||
showPublishModal: req.query?.pm == 'true'
|
||||
timer.done()
|
||||
|
||||
|
|
|
@ -45,37 +45,40 @@ wrapWithLock = (methodWithoutLock) ->
|
|||
methodWithLock
|
||||
|
||||
module.exports = ProjectEntityUpdateHandler = self =
|
||||
# this doesn't need any locking because it's only called by ProjectDuplicator
|
||||
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
projectHistoryId = project.overleaf?.history?.id
|
||||
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
|
||||
return callback(err) if err?
|
||||
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=>
|
||||
if !origonalFileRef?
|
||||
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
|
||||
return callback()
|
||||
# convert any invalid characters in original file to '_'
|
||||
fileRef = new File name : SafePath.clean(origonalFileRef.name)
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
|
||||
return callback(err)
|
||||
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
|
||||
return callback(err)
|
||||
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
|
||||
copyFileFromExistingProjectWithProject: wrapWithLock
|
||||
beforeLock: (next) ->
|
||||
(project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
|
||||
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) ->
|
||||
if !origonalFileRef?
|
||||
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
|
||||
return callback()
|
||||
# convert any invalid characters in original file to '_'
|
||||
fileRef = new File name : SafePath.clean(origonalFileRef.name)
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
|
||||
newFiles = [
|
||||
file: fileRef
|
||||
path: result?.path?.fileSystem
|
||||
url: fileStoreUrl
|
||||
]
|
||||
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, fileRef, folder_id
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
|
||||
return callback(err)
|
||||
next(project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback)
|
||||
withLock: (project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
projectHistoryId = project.overleaf?.history?.id
|
||||
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) ->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
|
||||
return callback(err)
|
||||
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
|
||||
if err?
|
||||
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
|
||||
newFiles = [
|
||||
file: fileRef
|
||||
path: result?.path?.fileSystem
|
||||
url: fileStoreUrl
|
||||
]
|
||||
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, fileRef, folder_id
|
||||
|
||||
updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
||||
|
|
|
@ -10,6 +10,8 @@ Async = require('async')
|
|||
oneMinInMs = 60 * 1000
|
||||
fiveMinsInMs = oneMinInMs * 5
|
||||
|
||||
if !settings.apis?.references?.url?
|
||||
logger.log "references search not enabled"
|
||||
|
||||
module.exports = ReferencesHandler =
|
||||
|
||||
|
|
|
@ -11,7 +11,16 @@ V1SubscriptionManager = require("./V1SubscriptionManager")
|
|||
oneMonthInSeconds = 60 * 60 * 24 * 30
|
||||
|
||||
module.exports = FeaturesUpdater =
|
||||
refreshFeatures: (user_id, callback)->
|
||||
refreshFeatures: (user_id, notifyV1 = true, callback = () ->)->
|
||||
if typeof notifyV1 == 'function'
|
||||
callback = notifyV1
|
||||
notifyV1 = true
|
||||
|
||||
if notifyV1
|
||||
V1SubscriptionManager.notifyV1OfFeaturesChange user_id, (error) ->
|
||||
if error?
|
||||
logger.err {err: error, user_id}, "error notifying v1 about updated features"
|
||||
|
||||
jobs =
|
||||
individualFeatures: (cb) -> FeaturesUpdater._getIndividualFeatures user_id, cb
|
||||
groupFeatureSets: (cb) -> FeaturesUpdater._getGroupFeatureSets user_id, cb
|
||||
|
@ -80,4 +89,4 @@ module.exports = FeaturesUpdater =
|
|||
if !plan?
|
||||
return {}
|
||||
else
|
||||
return plan.features
|
||||
return plan.features
|
||||
|
|
|
@ -10,6 +10,7 @@ GeoIpLookup = require("../../infrastructure/GeoIpLookup")
|
|||
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
||||
UserGetter = require "../User/UserGetter"
|
||||
FeaturesUpdater = require './FeaturesUpdater'
|
||||
planFeatures = require './planFeatures'
|
||||
|
||||
module.exports = SubscriptionController =
|
||||
|
||||
|
@ -20,6 +21,7 @@ module.exports = SubscriptionController =
|
|||
viewName = "#{viewName}_#{req.query.v}"
|
||||
logger.log viewName:viewName, "showing plans page"
|
||||
currentUser = null
|
||||
|
||||
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
|
||||
return next(err) if err?
|
||||
render = () ->
|
||||
|
@ -29,6 +31,7 @@ module.exports = SubscriptionController =
|
|||
gaExperiments: Settings.gaExperiments.plansPage
|
||||
recomendedCurrency:recomendedCurrency
|
||||
shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
|
||||
planFeatures: planFeatures
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if user_id?
|
||||
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
|
||||
|
|
|
@ -2,8 +2,8 @@ async = require("async")
|
|||
_ = require("underscore")
|
||||
SubscriptionUpdater = require("./SubscriptionUpdater")
|
||||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
UserGetter = require("../User/UserGetter")
|
||||
Subscription = require("../../models/Subscription").Subscription
|
||||
UserLocator = require("../User/UserLocator")
|
||||
LimitationsManager = require("./LimitationsManager")
|
||||
logger = require("logger-sharelatex")
|
||||
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
|
||||
|
@ -22,7 +22,7 @@ module.exports = SubscriptionGroupHandler =
|
|||
if limitReached
|
||||
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
|
||||
return callback(limitReached:limitReached)
|
||||
UserLocator.findByEmail newEmail, (err, user)->
|
||||
UserGetter.getUserByMainEmail newEmail, (err, user)->
|
||||
return callback(err) if err?
|
||||
if user?
|
||||
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
|
||||
|
@ -66,7 +66,7 @@ module.exports = SubscriptionGroupHandler =
|
|||
users.push buildEmailInviteViewModel(email)
|
||||
jobs = _.map subscription.member_ids, (user_id)->
|
||||
return (cb)->
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
if err? or !user?
|
||||
users.push _id:user_id
|
||||
return cb()
|
||||
|
|
|
@ -12,39 +12,49 @@ module.exports = V1SubscriptionManager =
|
|||
# - 'v1_free'
|
||||
getPlanCodeFromV1: (userId, callback=(err, planCode)->) ->
|
||||
logger.log {userId}, "[V1SubscriptionManager] fetching v1 plan for user"
|
||||
V1SubscriptionManager._v1Request userId, {
|
||||
method: 'GET',
|
||||
url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/plan_code"
|
||||
}, (error, body) ->
|
||||
return callback(error) if error?
|
||||
planName = body?.plan_name
|
||||
logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user"
|
||||
if planName in ['pro', 'pro_plus', 'student', 'free']
|
||||
planName = "v1_#{planName}"
|
||||
else
|
||||
# Throw away 'anonymous', etc as being equivalent to null
|
||||
planName = null
|
||||
return callback(null, planName)
|
||||
|
||||
notifyV1OfFeaturesChange: (userId, callback = (error) ->) ->
|
||||
V1SubscriptionManager._v1Request userId, {
|
||||
method: 'POST',
|
||||
url: (v1Id) -> "/api/v1/sharelatex/users/#{v1Id}/sync"
|
||||
}, callback
|
||||
|
||||
_v1Request: (userId, options, callback=(err, body)->) ->
|
||||
if !settings?.apis?.v1
|
||||
return callback null, null
|
||||
UserGetter.getUser userId, {'overleaf.id': 1}, (err, user) ->
|
||||
return callback(err) if err?
|
||||
v1Id = user?.overleaf?.id
|
||||
if !v1Id?
|
||||
logger.log {userId}, "[V1SubscriptionManager] no v1 id found for user"
|
||||
return callback(null, null)
|
||||
V1SubscriptionManager._v1PlanRequest v1Id, (err, body) ->
|
||||
return callback(err) if err?
|
||||
planName = body?.plan_name
|
||||
logger.log {userId, planName, body}, "[V1SubscriptionManager] fetched v1 plan for user"
|
||||
if planName in ['pro', 'pro_plus', 'student', 'free']
|
||||
planName = "v1_#{planName}"
|
||||
request {
|
||||
baseUrl: settings.apis.v1.url
|
||||
url: options.url(v1Id)
|
||||
method: options.method
|
||||
auth:
|
||||
user: settings.apis.v1.user
|
||||
pass: settings.apis.v1.pass
|
||||
sendImmediately: true
|
||||
json: true,
|
||||
timeout: 5 * 1000
|
||||
}, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
return callback null, body
|
||||
else
|
||||
# Throw away 'anonymous', etc as being equivalent to null
|
||||
planName = null
|
||||
return callback(null, planName)
|
||||
return callback new Error("non-success code from v1: #{response.statusCode}")
|
||||
|
||||
_v1PlanRequest: (v1Id, callback=(err, body)->) ->
|
||||
if !settings?.apis?.v1
|
||||
return callback null, null
|
||||
request {
|
||||
method: 'GET',
|
||||
url: settings.apis.v1.url +
|
||||
"/api/v1/sharelatex/users/#{v1Id}/plan_code"
|
||||
auth:
|
||||
user: settings.apis.v1.user
|
||||
pass: settings.apis.v1.pass
|
||||
sendImmediately: true
|
||||
json: true,
|
||||
timeout: 5 * 1000
|
||||
}, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
return callback null, body
|
||||
else
|
||||
return callback new Error("non-success code from v1: #{response.statusCode}")
|
|
@ -0,0 +1,133 @@
|
|||
module.exports =
|
||||
[
|
||||
{
|
||||
feature: 'number_collab'
|
||||
value: 'str'
|
||||
plans: {
|
||||
free: '1'
|
||||
coll: '10'
|
||||
prof: 'unlimited'
|
||||
}
|
||||
student: '6'
|
||||
}
|
||||
{
|
||||
feature: 'unlimited_private'
|
||||
value: 'bool'
|
||||
info: 'unlimited_private_info'
|
||||
plans: {
|
||||
free: true
|
||||
coll: true
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'realtime_collab'
|
||||
value: 'bool'
|
||||
info: 'realtime_collab_info'
|
||||
plans: {
|
||||
free: true
|
||||
coll: true
|
||||
prof: true
|
||||
}
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'hundreds_templates'
|
||||
value: 'bool'
|
||||
info: 'hundreds_templates_info'
|
||||
plans: {
|
||||
free: true
|
||||
coll: true
|
||||
prof: true
|
||||
}
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'powerful_latex_editor'
|
||||
value: 'bool'
|
||||
info: 'latex_editor_info'
|
||||
plans: {
|
||||
free: true
|
||||
coll: true
|
||||
prof: true
|
||||
}
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'realtime_track_changes'
|
||||
value: 'bool'
|
||||
info: 'realtime_track_changes_info'
|
||||
plans: {
|
||||
free: false
|
||||
coll: true
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'reference_search'
|
||||
value: 'bool'
|
||||
info: 'reference_search_info'
|
||||
plans: {
|
||||
free: false
|
||||
coll: true
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
},
|
||||
{
|
||||
feature: 'reference_sync'
|
||||
info: 'reference_sync_info'
|
||||
value: 'bool'
|
||||
plans: {
|
||||
free: false
|
||||
coll: true
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'full_doc_history'
|
||||
value: 'bool'
|
||||
info: 'full_doc_history_info'
|
||||
plans: {
|
||||
free: false,
|
||||
coll: true,
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
}
|
||||
{
|
||||
feature: 'dropbox_integration_lowercase'
|
||||
value: 'bool'
|
||||
info: 'dropbox_integration_info'
|
||||
plans: {
|
||||
free: false,
|
||||
coll: true,
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
},
|
||||
{
|
||||
feature: 'github_integration_lowercase'
|
||||
value: 'bool'
|
||||
info: 'github_integration_info'
|
||||
plans: {
|
||||
free: false,
|
||||
coll: true,
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
},
|
||||
{
|
||||
feature: 'priority_support',
|
||||
value: 'bool',
|
||||
plans: {
|
||||
free: false,
|
||||
coll: true,
|
||||
prof: true
|
||||
},
|
||||
student: true
|
||||
},
|
||||
]
|
|
@ -0,0 +1,80 @@
|
|||
path = require('path')
|
||||
Project = require('../../../js/models/Project').Project
|
||||
ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager')
|
||||
ProjectOptionsHandler = require('../../../js/Features/Project/ProjectOptionsHandler')
|
||||
AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController')
|
||||
settings = require('settings-sharelatex')
|
||||
fs = require('fs')
|
||||
request = require('request')
|
||||
uuid = require('uuid')
|
||||
logger = require('logger-sharelatex')
|
||||
async = require("async")
|
||||
|
||||
|
||||
module.exports = TemplatesController =
|
||||
|
||||
getV1Template: (req, res)->
|
||||
templateVersionId = req.params.Template_version_id
|
||||
templateId = req.query.id
|
||||
if !/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId)
|
||||
logger.err templateVersionId:templateVersionId, templateId: templateId, "invalid template id or version"
|
||||
return res.sendStatus 400
|
||||
data = {}
|
||||
data.templateVersionId = templateVersionId
|
||||
data.templateId = templateId
|
||||
data.name = req.query.templateName
|
||||
data.compiler = req.query.latexEngine
|
||||
res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data
|
||||
|
||||
createProjectFromV1Template: (req, res)->
|
||||
currentUserId = AuthenticationController.getLoggedInUserId(req)
|
||||
zipUrl = "#{settings.apis.v1.url}/api/v1/sharelatex/templates/#{req.body.templateVersionId}"
|
||||
zipReq = request(zipUrl, {
|
||||
'auth': {
|
||||
'user': settings.apis.v1.user,
|
||||
'pass': settings.apis.v1.pass
|
||||
}
|
||||
})
|
||||
|
||||
TemplatesController.createFromZip(
|
||||
zipReq,
|
||||
{
|
||||
templateName: req.body.templateName,
|
||||
currentUserId: currentUserId,
|
||||
compiler: req.body.compiler
|
||||
docId: req.body.docId
|
||||
templateId: req.body.templateId
|
||||
templateVersionId: req.body.templateVersionId
|
||||
},
|
||||
req,
|
||||
res
|
||||
)
|
||||
|
||||
createFromZip: (zipReq, options, req, res)->
|
||||
dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}"
|
||||
writeStream = fs.createWriteStream(dumpPath)
|
||||
|
||||
zipReq.on "error", (error) ->
|
||||
logger.error err: error, "error getting zip from template API"
|
||||
zipReq.pipe(writeStream)
|
||||
writeStream.on 'close', ->
|
||||
ProjectUploadManager.createProjectFromZipArchive options.currentUserId, options.templateName, dumpPath, (err, project)->
|
||||
if err?
|
||||
logger.err err:err, zipReq:zipReq, "problem building project from zip"
|
||||
return res.sendStatus 500
|
||||
setCompiler project._id, options.compiler, ->
|
||||
fs.unlink dumpPath, ->
|
||||
delete req.session.templateData
|
||||
conditions = {_id:project._id}
|
||||
update = {
|
||||
fromV1TemplateId:options.templateId,
|
||||
fromV1TemplateVersionId:options.templateVersionId
|
||||
}
|
||||
Project.update conditions, update, {}, (err)->
|
||||
res.redirect "/project/#{project._id}"
|
||||
|
||||
setCompiler = (project_id, compiler, callback)->
|
||||
if compiler?
|
||||
ProjectOptionsHandler.setCompiler project_id, compiler, callback
|
||||
else
|
||||
callback()
|
|
@ -0,0 +1,8 @@
|
|||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
saveTemplateDataInSession: (req, res, next)->
|
||||
if req.query.templateName
|
||||
req.session.templateData = req.query
|
||||
next()
|
|
@ -0,0 +1,9 @@
|
|||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
|
||||
module.exports =
|
||||
saveTemplateDataInSession: (req, res, next)->
|
||||
if req.query.templateName
|
||||
req.session.templateData = req.query
|
||||
next()
|
|
@ -0,0 +1,10 @@
|
|||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
TemplatesController = require("./TemplatesController")
|
||||
TemplatesMiddlewear = require('./TemplatesMiddlewear')
|
||||
|
||||
module.exports =
|
||||
apply: (app)->
|
||||
|
||||
app.get '/project/new/template/:Template_version_id', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.getV1Template
|
||||
|
||||
app.post '/project/new/template', AuthenticationController.requireLogin(), TemplatesController.createProjectFromV1Template
|
|
@ -1,6 +1,6 @@
|
|||
UserHandler = require("./UserHandler")
|
||||
UserDeleter = require("./UserDeleter")
|
||||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
User = require("../../models/User").User
|
||||
newsLetterManager = require('../Newsletter/NewsletterManager')
|
||||
UserRegistrationHandler = require("./UserRegistrationHandler")
|
||||
|
@ -45,7 +45,7 @@ module.exports = UserController =
|
|||
|
||||
unsubscribe: (req, res)->
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
newsLetterManager.unsubscribe user, ->
|
||||
res.send()
|
||||
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
User = require("../../models/User").User
|
||||
UserLocator = require("./UserLocator")
|
||||
logger = require("logger-sharelatex")
|
||||
metrics = require('metrics-sharelatex')
|
||||
|
||||
|
||||
module.exports = UserCreator =
|
||||
|
||||
getUserOrCreateHoldingAccount: (email, callback = (err, user)->)->
|
||||
self = @
|
||||
UserLocator.findByEmail email, (err, user)->
|
||||
if user?
|
||||
callback(err, user)
|
||||
else
|
||||
self.createNewUser email:email, holdingAccount:true, callback
|
||||
|
||||
createNewUser: (opts, callback)->
|
||||
logger.log opts:opts, "creating new user"
|
||||
user = new User()
|
||||
|
|
|
@ -6,6 +6,8 @@ ObjectId = mongojs.ObjectId
|
|||
|
||||
module.exports = UserGetter =
|
||||
getUser: (query, projection, callback = (error, user) ->) ->
|
||||
if query?.email?
|
||||
return callback(new Error("Don't use getUser to find user by email"), null)
|
||||
if arguments.length == 2
|
||||
callback = projection
|
||||
projection = {}
|
||||
|
@ -19,6 +21,31 @@ module.exports = UserGetter =
|
|||
|
||||
db.users.findOne query, projection, callback
|
||||
|
||||
getUserEmail: (userId, callback = (error, email) ->) ->
|
||||
@getUser userId, { email: 1 }, (error, user) ->
|
||||
callback(error, user?.email)
|
||||
|
||||
getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
|
||||
email = email.trim()
|
||||
if arguments.length == 2
|
||||
callback = projection
|
||||
projection = {}
|
||||
db.users.findOne email: email, projection, callback
|
||||
|
||||
getUserByAnyEmail: (email, projection, callback = (error, user) ->) ->
|
||||
email = email.trim()
|
||||
if arguments.length == 2
|
||||
callback = projection
|
||||
projection = {}
|
||||
# $exists: true MUST be set to use the partial index
|
||||
query = emails: { $exists: true }, 'emails.email': email
|
||||
db.users.findOne query, projection, (error, user) =>
|
||||
return callback(error, user) if error? or user?
|
||||
|
||||
# While multiple emails are being rolled out, check for the main email as
|
||||
# well
|
||||
@getUserByMainEmail email, projection, callback
|
||||
|
||||
getUsers: (user_ids, projection, callback = (error, users) ->) ->
|
||||
try
|
||||
user_ids = user_ids.map (u) -> ObjectId(u.toString())
|
||||
|
@ -39,6 +66,9 @@ module.exports = UserGetter =
|
|||
|
||||
[
|
||||
'getUser',
|
||||
'getUserEmail',
|
||||
'getUserByMainEmail',
|
||||
'getUserByAnyEmail',
|
||||
'getUsers',
|
||||
'getUserOrUserStubById'
|
||||
].map (method) ->
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
logger = require('logger-sharelatex')
|
||||
|
||||
module.exports = UserLocator =
|
||||
|
||||
findByEmail: (email, callback)->
|
||||
email = email.trim()
|
||||
db.users.findOne email:email, (err, user)->
|
||||
callback(err, user)
|
||||
|
||||
findById: (_id, callback)->
|
||||
db.users.findOne _id:ObjectId(_id+""), callback
|
||||
|
||||
[
|
||||
'findById',
|
||||
'findByEmail'
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod UserLocator, method, 'mongo.UserLocator', logger
|
|
@ -1,4 +1,3 @@
|
|||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
UserSessionsManager = require("./UserSessionsManager")
|
||||
ErrorController = require("../Errors/ErrorController")
|
||||
|
@ -61,7 +60,7 @@ module.exports =
|
|||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
logger.log user: user_id, "loading settings page"
|
||||
shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin)
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
UserGetter.getUser user_id, (err, user)->
|
||||
return next(err) if err?
|
||||
res.render 'user/settings',
|
||||
title:'account_settings'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
sanitize = require('sanitizer')
|
||||
User = require("../../models/User").User
|
||||
UserCreator = require("./UserCreator")
|
||||
UserGetter = require("./UserGetter")
|
||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||
NewsLetterManager = require("../Newsletter/NewsletterManager")
|
||||
async = require("async")
|
||||
|
@ -47,7 +48,7 @@ module.exports = UserRegistrationHandler =
|
|||
if !requestIsValid
|
||||
return callback(new Error("request is not valid"))
|
||||
userDetails.email = userDetails.email?.trim()?.toLowerCase()
|
||||
User.findOne email:userDetails.email, (err, user)->
|
||||
UserGetter.getUserByMainEmail userDetails.email, (err, user) =>
|
||||
if err?
|
||||
return callback err
|
||||
if user?.holdingAccount == false
|
||||
|
|
|
@ -2,8 +2,9 @@ logger = require("logger-sharelatex")
|
|||
mongojs = require("../../infrastructure/mongojs")
|
||||
metrics = require("metrics-sharelatex")
|
||||
db = mongojs.db
|
||||
async = require("async")
|
||||
ObjectId = mongojs.ObjectId
|
||||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
|
||||
module.exports = UserUpdater =
|
||||
updateUser: (query, update, callback = (error) ->) ->
|
||||
|
@ -11,23 +12,89 @@ module.exports = UserUpdater =
|
|||
query = _id: ObjectId(query)
|
||||
else if query instanceof ObjectId
|
||||
query = _id: query
|
||||
else if typeof query._id == "string"
|
||||
query._id = ObjectId(query._id)
|
||||
|
||||
db.users.update query, update, callback
|
||||
|
||||
|
||||
changeEmailAddress: (user_id, newEmail, callback)->
|
||||
self = @
|
||||
logger.log user_id:user_id, newEmail:newEmail, "updaing email address of user"
|
||||
UserLocator.findByEmail newEmail, (error, user) ->
|
||||
if user?
|
||||
return callback({message:"alread_exists"})
|
||||
self.updateUser user_id.toString(), {
|
||||
$set: { "email": newEmail},
|
||||
}, (err) ->
|
||||
if err?
|
||||
logger.err err:err, "problem updating users email"
|
||||
return callback(err)
|
||||
#
|
||||
# DEPRECATED
|
||||
#
|
||||
# Change the user's main email address by adding a new email, switching the
|
||||
# default email and removing the old email. Prefer manipulating multiple
|
||||
# emails and the default rather than calling this method directly
|
||||
#
|
||||
changeEmailAddress: (userId, newEmail, callback)->
|
||||
logger.log userId: userId, newEmail: newEmail, "updaing email address of user"
|
||||
|
||||
oldEmail = null
|
||||
async.series [
|
||||
(cb) ->
|
||||
UserGetter.getUserEmail userId, (error, email) ->
|
||||
oldEmail = email
|
||||
cb(error)
|
||||
(cb) -> UserUpdater.addEmailAddress userId, newEmail, cb
|
||||
(cb) -> UserUpdater.setDefaultEmailAddress userId, newEmail, cb
|
||||
(cb) -> UserUpdater.removeEmailAddress userId, oldEmail, cb
|
||||
], callback
|
||||
|
||||
|
||||
# Add a new email address for the user. Email cannot be already used by this
|
||||
# or any other user
|
||||
addEmailAddress: (userId, newEmail, callback) ->
|
||||
@_ensureUniqueEmailAddress newEmail, (error) =>
|
||||
return callback(error) if error?
|
||||
|
||||
update = $push: emails: email: newEmail, createdAt: new Date()
|
||||
@updateUser userId, update, (error) ->
|
||||
if error?
|
||||
logger.err error: error, 'problem updating users emails'
|
||||
return callback(error)
|
||||
callback()
|
||||
|
||||
|
||||
metrics.timeAsyncMethod UserUpdater, 'updateUser', 'mongo.UserUpdater', logger
|
||||
# remove one of the user's email addresses. The email cannot be the user's
|
||||
# default email address
|
||||
removeEmailAddress: (userId, email, callback) ->
|
||||
query = _id: userId, email: $ne: email
|
||||
update = $pull: emails: email: email
|
||||
@updateUser query, update, (error, res) ->
|
||||
if error?
|
||||
logger.err error:error, 'problem removing users email'
|
||||
return callback(error)
|
||||
if res.nMatched == 0
|
||||
return callback(new Error('Cannot remove default email'))
|
||||
callback()
|
||||
|
||||
|
||||
# set the default email address by setting the `email` attribute. The email
|
||||
# must be one of the user's multiple emails (`emails` attribute)
|
||||
setDefaultEmailAddress: (userId, email, callback) ->
|
||||
query = _id: userId, 'emails.email': email
|
||||
update = $set: email: email
|
||||
@updateUser query, update, (error, res) ->
|
||||
if error?
|
||||
logger.err error:error, 'problem setting default emails'
|
||||
return callback(error)
|
||||
if res.nMatched == 0
|
||||
return callback(new Error('Default email does not belong to user'))
|
||||
callback()
|
||||
|
||||
|
||||
# check for duplicate email address. This is also enforced at the DB level
|
||||
_ensureUniqueEmailAddress: (newEmail, callback) ->
|
||||
UserGetter.getUserByAnyEmail newEmail, (error, user) ->
|
||||
return callback(message: 'alread_exists') if user?
|
||||
callback()
|
||||
|
||||
|
||||
[
|
||||
'updateUser'
|
||||
'changeEmailAddress'
|
||||
'setDefaultEmailAddress'
|
||||
'addEmailAddress'
|
||||
'removeEmailAddress'
|
||||
'_ensureUniqueEmailAddress'
|
||||
].map (method) ->
|
||||
metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger)
|
||||
|
|
|
@ -6,16 +6,31 @@ Settings = require 'settings-sharelatex'
|
|||
request = require 'request'
|
||||
|
||||
module.exports = FileWriter =
|
||||
|
||||
_ensureDumpFolderExists: (callback=(error)->) ->
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
callback(null)
|
||||
|
||||
writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
fs.writeFile fsPath, lines.join('\n'), (error) ->
|
||||
return callback(error) if error?
|
||||
callback(null, fsPath)
|
||||
|
||||
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
|
||||
callback = _.once(callback)
|
||||
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
|
||||
|
||||
stream.pause()
|
||||
fs.mkdir Settings.path.dumpFolder, (error) ->
|
||||
FileWriter._ensureDumpFolderExists (error) ->
|
||||
return callback(error) if error?
|
||||
stream.resume()
|
||||
if error? and error.code != 'EEXIST'
|
||||
# Ignore error about already existing
|
||||
return callback(error)
|
||||
|
||||
writeStream = fs.createWriteStream(fsPath)
|
||||
stream.pipe(writeStream)
|
||||
|
@ -39,4 +54,4 @@ module.exports = FileWriter =
|
|||
else
|
||||
err = new Error("bad response from url: #{response.statusCode}")
|
||||
logger.err {err, identifier, url}, err.message
|
||||
callback(err)
|
||||
callback(err)
|
||||
|
|
|
@ -30,7 +30,8 @@ module.exports = Modules =
|
|||
for module in @modules
|
||||
for view, partial of module.viewIncludes or {}
|
||||
@viewIncludes[view] ||= []
|
||||
@viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
|
||||
filePath = Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")
|
||||
@viewIncludes[view].push pug.compileFile(filePath, doctype: "html")
|
||||
|
||||
moduleIncludes: (view, locals) ->
|
||||
compiledPartials = Modules.viewIncludes[view] or []
|
||||
|
|
|
@ -48,6 +48,7 @@ MetaController = require('./Features/Metadata/MetaController')
|
|||
TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
|
||||
Features = require('./infrastructure/Features')
|
||||
LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter'
|
||||
TemplatesRouter = require './Features/Templates/TemplatesRouter'
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -80,10 +81,10 @@ module.exports = class Router
|
|||
ContactRouter.apply(webRouter, privateApiRouter)
|
||||
AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
|
||||
TemplatesRouter.apply(webRouter)
|
||||
|
||||
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
|
||||
|
||||
|
||||
if Settings.enableSubscriptions
|
||||
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
|
||||
|
||||
|
@ -119,6 +120,11 @@ module.exports = class Router
|
|||
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
|
||||
privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
|
||||
|
||||
webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson
|
||||
webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(),
|
||||
AuthorizationMiddlewear.ensureUserCanReadProject,
|
||||
ProjectController.projectEntitiesJson
|
||||
|
||||
webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
|
||||
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
|
||||
|
||||
|
@ -201,7 +207,8 @@ module.exports = class Router
|
|||
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
|
||||
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.get "/project/:Project_id/filetree/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
|
||||
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
|
||||
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
|
||||
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
script(type='text/ng-template', id='supportModalTemplate')
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="close()"
|
||||
) ×
|
||||
h3 #{translate("contact_us")}
|
||||
.modal-body.contact-us-modal
|
||||
form(name="contactForm")
|
||||
span(ng-show="sent == false")
|
||||
.alert.alert-danger(ng-show="error") Something went wrong sending your request :(
|
||||
label
|
||||
| #{translate("subject")}
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(
|
||||
name="subject",
|
||||
required
|
||||
ng-model="form.subject",
|
||||
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
|
||||
maxlength='255',
|
||||
tabindex='1',
|
||||
onkeyup='')
|
||||
.contact-suggestions(ng-show="suggestions.length")
|
||||
p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>" + translate("knowledge_base") + "</a>" })}
|
||||
ul.contact-suggestion-list
|
||||
li(ng-repeat="suggestion in suggestions")
|
||||
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
|
||||
span(ng-bind-html="suggestion.name")
|
||||
i.fa.fa-angle-right
|
||||
label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||
| #{translate("email")}
|
||||
.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
|
||||
input.field.text.medium.span8.form-control(
|
||||
name="email",
|
||||
required
|
||||
ng-model="form.email",
|
||||
ng-init="form.email = '"+getUserEmail()+"'",
|
||||
type='email', spellcheck='false',
|
||||
value='',
|
||||
maxlength='255',
|
||||
tabindex='2')
|
||||
label#title12.desc
|
||||
| #{translate("project_url")} (#{translate("optional")})
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
|
||||
label.desc
|
||||
| #{translate("contact_message_label")}
|
||||
.form-group
|
||||
textarea.field.text.medium.span8.form-control(
|
||||
name="body",
|
||||
required
|
||||
ng-model="form.message",
|
||||
type='text',
|
||||
value='',
|
||||
tabindex='4',
|
||||
onkeyup=''
|
||||
)
|
||||
.form-group.text-center
|
||||
input.btn-success.btn.btn-lg(
|
||||
type='submit',
|
||||
ng-disabled="contactForm.$invalid || sending",
|
||||
ng-click="contactUs()"
|
||||
value=translate("contact_us")
|
||||
)
|
||||
span(ng-show="sent")
|
||||
p #{translate("request_sent_thank_you")}
|
|
@ -147,7 +147,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
src=buildJsPath('libs/require.js', {hashedPath:true})
|
||||
)
|
||||
|
||||
include contact-us-modal
|
||||
!= moduleIncludes("contactModal", locals)
|
||||
include v1-tooltip
|
||||
include sentry
|
||||
|
||||
|
|
|
@ -57,9 +57,12 @@ block content
|
|||
include ./editor/share
|
||||
!= moduleIncludes("publish:body", locals)
|
||||
|
||||
include ./editor/history/toolbarV2.pug
|
||||
|
||||
main#ide-body(
|
||||
ng-cloak,
|
||||
role="main",
|
||||
ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME) }",
|
||||
layout="main",
|
||||
ng-hide="state.loading",
|
||||
resize-on="layout:chat:resize",
|
||||
|
@ -70,7 +73,7 @@ block content
|
|||
)
|
||||
.ui-layout-west
|
||||
include ./editor/file-tree
|
||||
include ./editor/history-file-tree
|
||||
include ./editor/history/fileTreeV2
|
||||
|
||||
.ui-layout-center
|
||||
include ./editor/editor
|
||||
|
|
|
@ -47,7 +47,18 @@ div.binary-file.full-size(
|
|||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url'")
|
||||
div(ng-if="openFile.linkedFileData.provider == 'project_file'")
|
||||
p
|
||||
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
|
||||
| Imported from
|
||||
|
|
||||
a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
|
||||
| {{ openFile.linkedFileData.source_project_display_name }}
|
||||
| /{{ openFile.linkedFileData.source_entity_path.slice(1) }},
|
||||
|
|
||||
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
|
||||
|
||||
span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'")
|
||||
button.btn.btn-success(
|
||||
href, ng-click="refreshFile(openFile)",
|
||||
ng-disabled="refreshing"
|
||||
|
@ -63,3 +74,7 @@ div.binary-file.full-size(
|
|||
i.fa.fa-fw.fa-download
|
||||
|
|
||||
| #{translate("download")}
|
||||
div(ng-if="refreshError")
|
||||
br
|
||||
.alert.alert-danger.col-md-6.col-md-offset-3
|
||||
| Error: {{ refreshError}}
|
||||
|
|
|
@ -33,11 +33,11 @@ div.full-size(
|
|||
i.fa.fa-arrow-left
|
||||
| #{translate("open_a_file_on_the_left")}
|
||||
|
||||
!= moduleIncludes('editor:toolbar', locals)
|
||||
!= moduleIncludes('editor:main', locals)
|
||||
|
||||
#editor(
|
||||
ace-editor="editor",
|
||||
ng-if="!editor.richText",
|
||||
ng-if="!editor.showRichText",
|
||||
ng-show="!!editor.sharejs_doc && !editor.opening",
|
||||
style=showRichText ? "top: 32px" : "",
|
||||
theme="settings.theme",
|
||||
|
@ -73,8 +73,6 @@ div.full-size(
|
|||
line-height="settings.lineHeight || ui.defaultLineHeight"
|
||||
)
|
||||
|
||||
!= moduleIncludes('editor:body', locals)
|
||||
|
||||
include ./review-panel
|
||||
|
||||
.ui-layout-east
|
||||
|
|
|
@ -342,6 +342,77 @@ script(type='text/ng-template', id='newDocModalTemplate')
|
|||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
// Project Linked Files Modal
|
||||
script(type='text/ng-template', id='projectLinkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from Project
|
||||
|
||||
.modal-body
|
||||
div
|
||||
div.alert.alert-danger(ng-if="state.error") Error, something went wrong!
|
||||
div
|
||||
form
|
||||
.form-controls
|
||||
label(for="project-select") Select a Project
|
||||
span(ng-show="state.inFlight.projects")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-select"
|
||||
ng-model="data.selectedProjectId"
|
||||
ng-disabled="!shouldEnableProjectSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a Project
|
||||
option(
|
||||
ng-repeat="project in data.projects"
|
||||
value="{{ project._id }}"
|
||||
) {{ project.name }}
|
||||
|
||||
br
|
||||
.form-controls
|
||||
label(for="project-entity-select") Select a File
|
||||
span(ng-show="state.inFlight.entities")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-entity-select"
|
||||
ng-model="data.selectedProjectEntity"
|
||||
ng-disabled="!shouldEnableProjectEntitySelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a File
|
||||
option(
|
||||
ng-repeat="projectEntity in data.projectEntities"
|
||||
value="{{ projectEntity.path }}"
|
||||
) {{ projectEntity.path.slice(1) }}
|
||||
br
|
||||
|
||||
.form-controls
|
||||
label(for="name") File Name In This Project
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="example.tex"
|
||||
required
|
||||
ng-model="data.name"
|
||||
name="name"
|
||||
)
|
||||
br
|
||||
|
||||
.modal-footer
|
||||
span(ng-show="state.inFlight.create")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
|
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="!shouldEnableCreateButton()"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='linkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from URL
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
aside.file-tree.file-tree-history(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }", ng-show="ui.view == 'history' && history.isV2").full-size
|
||||
.toolbar.toolbar-filetree
|
||||
span Modified files
|
||||
|
||||
.file-tree-inner
|
||||
ul.list-unstyled.file-tree-list
|
||||
li(
|
||||
ng-repeat="(pathname, doc) in history.selection.docs"
|
||||
ng-class="{ 'selected': history.selection.pathname == pathname }"
|
||||
)
|
||||
.entity
|
||||
.entity-name.entity-name-history(
|
||||
ng-click="history.selection.pathname = pathname",
|
||||
ng-class="{ 'deleted': !!doc.deletedAtV }"
|
||||
)
|
||||
i.fa.fa-fw.fa-pencil
|
||||
span {{ pathname }}
|
|
@ -40,90 +40,11 @@ div#history(ng-show="ui.view == 'history'")
|
|||
p
|
||||
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
|
||||
|
||||
aside.change-list(
|
||||
ng-controller="HistoryListController"
|
||||
infinite-scroll="loadMore()"
|
||||
infinite-scroll-disabled="history.loading || history.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'history'"
|
||||
)
|
||||
.infinite-scroll-inner
|
||||
ul.list-unstyled(
|
||||
ng-class="{\
|
||||
'hover-state': history.hoveringOverListSelectors\
|
||||
}"
|
||||
)
|
||||
li.change(
|
||||
ng-repeat="update in history.updates"
|
||||
ng-class="{\
|
||||
'first-in-day': update.meta.first_in_day,\
|
||||
'selected': update.inSelection,\
|
||||
'selected-to': update.selectedTo,\
|
||||
'selected-from': update.selectedFrom,\
|
||||
'hover-selected': update.inHoverSelection,\
|
||||
'hover-selected-to': update.hoverSelectedTo,\
|
||||
'hover-selected-from': update.hoverSelectedFrom,\
|
||||
}"
|
||||
ng-controller="HistoryListItemController"
|
||||
)
|
||||
|
||||
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
|
||||
|
||||
div.selectors
|
||||
div.range
|
||||
form
|
||||
input.selector-from(
|
||||
type="radio"
|
||||
name="fromVersion"
|
||||
ng-model="update.selectedFrom"
|
||||
ng-value="true"
|
||||
ng-mouseover="mouseOverSelectedFrom()"
|
||||
ng-mouseout="mouseOutSelectedFrom()"
|
||||
ng-show="update.afterSelection || update.inSelection"
|
||||
)
|
||||
form
|
||||
input.selector-to(
|
||||
type="radio"
|
||||
name="toVersion"
|
||||
ng-model="update.selectedTo"
|
||||
ng-value="true"
|
||||
ng-mouseover="mouseOverSelectedTo()"
|
||||
ng-mouseout="mouseOutSelectedTo()"
|
||||
ng-show="update.beforeSelection || update.inSelection"
|
||||
)
|
||||
|
||||
div.description(ng-click="select()")
|
||||
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
|
||||
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
|
||||
| Edited
|
||||
div.docs(ng-repeat="pathname in update.pathnames")
|
||||
.doc {{ pathname }}
|
||||
div.docs(ng-repeat="project_op in update.project_ops")
|
||||
div(ng-if="project_op.rename")
|
||||
.action Renamed
|
||||
.doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }}
|
||||
div(ng-if="project_op.add")
|
||||
.action Created
|
||||
.doc {{ project_op.add.pathname }}
|
||||
div(ng-if="project_op.remove")
|
||||
.action Deleted
|
||||
.doc {{ project_op.remove.pathname }}
|
||||
div.users
|
||||
div.user(ng-repeat="update_user in update.meta.users")
|
||||
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
|
||||
.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
|
||||
.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)")
|
||||
.name(ng-if="update_user && update_user.id == user.id") You
|
||||
.name(ng-if="update_user == null") #{translate("anonymous")}
|
||||
div.user(ng-if="update.meta.users.length == 0")
|
||||
.color-square(style="background-color: hsl(100, 100%, 50%)")
|
||||
span #{translate("anonymous")}
|
||||
|
||||
.loading(ng-show="history.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
include ./history/entriesListV1
|
||||
include ./history/entriesListV2
|
||||
|
||||
include ./history/diffPanelV1
|
||||
include ./history/diffPanelV2
|
||||
include ./history/previewPanelV2
|
||||
|
||||
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
|
||||
.modal-header
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
}"
|
||||
)
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.toolbar-right
|
||||
a.btn.btn-danger.btn-sm(
|
||||
.toolbar-right(ng-if="permissions.write")
|
||||
a.btn.btn-danger.btn-xs(
|
||||
href,
|
||||
ng-click="openRestoreDiffModal()"
|
||||
) #{translate("restore_to_before_these_changes")}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
aside.change-list(
|
||||
ng-if="!history.isV2"
|
||||
ng-controller="HistoryListController"
|
||||
infinite-scroll="loadMore()"
|
||||
infinite-scroll-disabled="history.loading || history.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'history'"
|
||||
)
|
||||
.infinite-scroll-inner
|
||||
ul.list-unstyled(
|
||||
ng-class="{\
|
||||
'hover-state': history.hoveringOverListSelectors\
|
||||
}"
|
||||
)
|
||||
li.change(
|
||||
ng-repeat="update in history.updates"
|
||||
ng-class="{\
|
||||
'first-in-day': update.meta.first_in_day,\
|
||||
'selected': update.inSelection,\
|
||||
'selected-to': update.selectedTo,\
|
||||
'selected-from': update.selectedFrom,\
|
||||
'hover-selected': update.inHoverSelection,\
|
||||
'hover-selected-to': update.hoverSelectedTo,\
|
||||
'hover-selected-from': update.hoverSelectedFrom,\
|
||||
}"
|
||||
ng-controller="HistoryListItemController"
|
||||
)
|
||||
|
||||
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
|
||||
|
||||
div.selectors
|
||||
div.range
|
||||
form
|
||||
input.selector-from(
|
||||
type="radio"
|
||||
name="fromVersion"
|
||||
ng-model="update.selectedFrom"
|
||||
ng-value="true"
|
||||
ng-mouseover="mouseOverSelectedFrom()"
|
||||
ng-mouseout="mouseOutSelectedFrom()"
|
||||
ng-show="update.afterSelection || update.inSelection"
|
||||
)
|
||||
form
|
||||
input.selector-to(
|
||||
type="radio"
|
||||
name="toVersion"
|
||||
ng-model="update.selectedTo"
|
||||
ng-value="true"
|
||||
ng-mouseover="mouseOverSelectedTo()"
|
||||
ng-mouseout="mouseOutSelectedTo()"
|
||||
ng-show="update.beforeSelection || update.inSelection"
|
||||
)
|
||||
|
||||
div.description(ng-click="select()")
|
||||
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
|
||||
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
|
||||
| #{translate("file_action_edited")}
|
||||
div.docs(ng-repeat="pathname in update.pathnames")
|
||||
.doc {{ pathname }}
|
||||
div.docs(ng-repeat="project_op in update.project_ops")
|
||||
div(ng-if="project_op.rename")
|
||||
.action #{translate("file_action_renamed")}
|
||||
.doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }}
|
||||
div(ng-if="project_op.add")
|
||||
.action #{translate("file_action_created")}
|
||||
.doc {{ project_op.add.pathname }}
|
||||
div(ng-if="project_op.remove")
|
||||
.action #{translate("file_action_deleted")}
|
||||
.doc {{ project_op.remove.pathname }}
|
||||
div.users
|
||||
div.user(ng-repeat="update_user in update.meta.users")
|
||||
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
|
||||
.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
|
||||
.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)")
|
||||
.name(ng-if="update_user && update_user.id == user.id") You
|
||||
.name(ng-if="update_user == null") #{translate("anonymous")}
|
||||
div.user(ng-if="update.meta.users.length == 0")
|
||||
.color-square(style="background-color: hsl(100, 100%, 50%)")
|
||||
span #{translate("anonymous")}
|
||||
|
||||
.loading(ng-show="history.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
174
services/web/app/views/project/editor/history/entriesListV2.pug
Normal file
174
services/web/app/views/project/editor/history/entriesListV2.pug
Normal file
|
@ -0,0 +1,174 @@
|
|||
aside.change-list(
|
||||
ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
|
||||
ng-controller="HistoryV2ListController"
|
||||
)
|
||||
history-entries-list(
|
||||
entries="history.updates"
|
||||
current-user="user"
|
||||
load-entries="loadMore()"
|
||||
load-disabled="history.loading || history.atEnd"
|
||||
load-initialize="ui.view == 'history'"
|
||||
is-loading="history.loading"
|
||||
on-entry-select="handleEntrySelect(selectedEntry)"
|
||||
)
|
||||
|
||||
aside.change-list(
|
||||
ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE"
|
||||
ng-controller="HistoryListController"
|
||||
infinite-scroll="loadMore()"
|
||||
infinite-scroll-disabled="history.loading || history.atEnd"
|
||||
infinite-scroll-initialize="ui.view == 'history'"
|
||||
)
|
||||
.infinite-scroll-inner
|
||||
ul.list-unstyled(
|
||||
ng-class="{\
|
||||
'hover-state': history.hoveringOverListSelectors\
|
||||
}"
|
||||
)
|
||||
li.change(
|
||||
ng-repeat="update in history.updates"
|
||||
ng-class="{\
|
||||
'first-in-day': update.meta.first_in_day,\
|
||||
'selected': update.inSelection,\
|
||||
'selected-to': update.selectedTo,\
|
||||
'selected-from': update.selectedFrom,\
|
||||
'hover-selected': update.inHoverSelection,\
|
||||
'hover-selected-to': update.hoverSelectedTo,\
|
||||
'hover-selected-from': update.hoverSelectedFrom,\
|
||||
}"
|
||||
ng-controller="HistoryListItemController"
|
||||
)
|
||||
|
||||
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
|
||||
|
||||
div.selectors
|
||||
div.range
|
||||
form
|
||||
input.selector-from(
|
||||
type="radio"
|
||||
name="fromVersion"
|
||||
ng-model="update.selectedFrom"
|
||||
ng-value="true"
|
||||
ng-mouseover="mouseOverSelectedFrom()"
|
||||
ng-mouseout="mouseOutSelectedFrom()"
|
||||
ng-show="update.afterSelection || update.inSelection"
|
||||
)
|
||||
form
|
||||
input.selector-to(
|
||||
type="radio"
|
||||
name="toVersion"
|
||||
ng-model="update.selectedTo"
|
||||
ng-value="true"
|
||||
ng-mouseover="mouseOverSelectedTo()"
|
||||
ng-mouseout="mouseOutSelectedTo()"
|
||||
ng-show="update.beforeSelection || update.inSelection"
|
||||
)
|
||||
|
||||
div.description(ng-click="select()")
|
||||
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
|
||||
div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
|
||||
| #{translate("file_action_edited")}
|
||||
div.docs(ng-repeat="pathname in update.pathnames")
|
||||
.doc {{ pathname }}
|
||||
div.docs(ng-repeat="project_op in update.project_ops")
|
||||
div(ng-if="project_op.rename")
|
||||
.action #{translate("file_action_renamed")}
|
||||
.doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }}
|
||||
div(ng-if="project_op.add")
|
||||
.action #{translate("file_action_created")}
|
||||
.doc {{ project_op.add.pathname }}
|
||||
div(ng-if="project_op.remove")
|
||||
.action #{translate("file_action_deleted")}
|
||||
.doc {{ project_op.remove.pathname }}
|
||||
div.users
|
||||
div.user(ng-repeat="update_user in update.meta.users")
|
||||
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
|
||||
.color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}")
|
||||
.name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)")
|
||||
.name(ng-if="update_user && update_user.id == user.id") You
|
||||
.name(ng-if="update_user == null") #{translate("anonymous")}
|
||||
div.user(ng-if="update.meta.users.length == 0")
|
||||
.color-square(style="background-color: hsl(100, 100%, 50%)")
|
||||
span #{translate("anonymous")}
|
||||
|
||||
.loading(ng-show="history.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
|
||||
script(type="text/ng-template", id="historyEntriesListTpl")
|
||||
.history-entries(
|
||||
infinite-scroll="$ctrl.loadEntries()"
|
||||
infinite-scroll-disabled="$ctrl.loadDisabled"
|
||||
infinite-scroll-initialize="$ctrl.loadInitialize"
|
||||
)
|
||||
.infinite-scroll-inner
|
||||
history-entry(
|
||||
ng-repeat="entry in $ctrl.entries"
|
||||
entry="entry"
|
||||
current-user="$ctrl.currentUser"
|
||||
on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })"
|
||||
ng-show="!$ctrl.isLoading"
|
||||
)
|
||||
.loading(ng-show="$ctrl.isLoading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
|
||||
script(type="text/ng-template", id="historyEntryTpl")
|
||||
.history-entry(
|
||||
ng-class="{\
|
||||
'history-entry-first-in-day': $ctrl.entry.meta.first_in_day,\
|
||||
'history-entry-selected': $ctrl.entry.inSelection,\
|
||||
'history-entry-selected-to': $ctrl.entry.selectedTo,\
|
||||
'history-entry-selected-from': $ctrl.entry.selectedFrom,\
|
||||
'history-entry-hover-selected': $ctrl.entry.inHoverSelection,\
|
||||
'history-entry-hover-selected-to': $ctrl.entry.hoverSelectedTo,\
|
||||
'history-entry-hover-selected-from': $ctrl.entry.hoverSelectedFrom,\
|
||||
}"
|
||||
)
|
||||
|
||||
time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }}
|
||||
|
||||
.history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })")
|
||||
ol.history-entry-changes
|
||||
li.history-entry-change(
|
||||
ng-repeat="pathname in ::$ctrl.entry.pathnames"
|
||||
)
|
||||
span.history-entry-change-action #{translate("file_action_edited")}
|
||||
span.history-entry-change-doc {{ ::pathname }}
|
||||
li.history-entry-change(
|
||||
ng-repeat="project_op in ::$ctrl.entry.project_ops"
|
||||
)
|
||||
span.history-entry-change-action(
|
||||
ng-if="::project_op.rename"
|
||||
) #{translate("file_action_renamed")}
|
||||
span.history-entry-change-action(
|
||||
ng-if="::project_op.add"
|
||||
) #{translate("file_action_created")}
|
||||
span.history-entry-change-action(
|
||||
ng-if="::project_op.remove"
|
||||
) #{translate("file_action_deleted")}
|
||||
span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
|
||||
.history-entry-metadata
|
||||
time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
|
||||
span
|
||||
|
|
||||
| •
|
||||
|
|
||||
ol.history-entry-metadata-users
|
||||
li.history-entry-metadata-user(ng-repeat="update_user in ::$ctrl.entry.meta.users")
|
||||
span.name(
|
||||
ng-if="::update_user && update_user.id != $ctrl.currentUser.id"
|
||||
ng-style="$ctrl.getUserCSSStyle(update_user);"
|
||||
) {{ ::$ctrl.displayName(update_user) }}
|
||||
span.name(
|
||||
ng-if="::update_user && update_user.id == $ctrl.currentUser.id"
|
||||
ng-style="$ctrl.getUserCSSStyle(update_user);"
|
||||
) You
|
||||
span.name(
|
||||
ng-if="::update_user == null"
|
||||
ng-style="$ctrl.getUserCSSStyle(update_user);"
|
||||
) #{translate("anonymous")}
|
||||
li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0")
|
||||
span.name(
|
||||
ng-style="$ctrl.getUserCSSStyle();"
|
||||
) #{translate("anonymous")}
|
70
services/web/app/views/project/editor/history/fileTreeV2.pug
Normal file
70
services/web/app/views/project/editor/history/fileTreeV2.pug
Normal file
|
@ -0,0 +1,70 @@
|
|||
aside.file-tree.full-size(
|
||||
ng-controller="HistoryV2FileTreeController"
|
||||
ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
|
||||
)
|
||||
.history-file-tree-inner
|
||||
history-file-tree(
|
||||
file-tree="currentFileTree"
|
||||
selected-pathname="history.selection.pathname"
|
||||
on-selected-file-change="handleFileSelection(file)"
|
||||
is-loading="history.loadingFileTree"
|
||||
)
|
||||
|
||||
aside.file-tree.file-tree-history.full-size(
|
||||
ng-controller="FileTreeController"
|
||||
ng-class="{ 'multi-selected': multiSelectedCount > 0 }"
|
||||
ng-show="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.COMPARE")
|
||||
.toolbar.toolbar-filetree
|
||||
span Modified files
|
||||
|
||||
.file-tree-inner
|
||||
ul.list-unstyled.file-tree-list
|
||||
li(
|
||||
ng-repeat="(pathname, doc) in history.selection.docs"
|
||||
ng-class="{ 'selected': history.selection.pathname == pathname }"
|
||||
)
|
||||
.entity
|
||||
.entity-name.entity-name-history(
|
||||
ng-click="history.selection.pathname = pathname",
|
||||
ng-class="{ 'deleted': !!doc.deletedAtV }"
|
||||
)
|
||||
i.fa.fa-fw.fa-pencil
|
||||
span {{ pathname }}
|
||||
|
||||
|
||||
|
||||
|
||||
script(type="text/ng-template", id="historyFileTreeTpl")
|
||||
.history-file-tree
|
||||
history-file-entity(
|
||||
ng-repeat="fileEntity in $ctrl.fileTree"
|
||||
file-entity="fileEntity"
|
||||
ng-show="!$ctrl.isLoading"
|
||||
)
|
||||
|
||||
|
||||
script(type="text/ng-template", id="historyFileEntityTpl")
|
||||
.history-file-entity-wrapper
|
||||
a.history-file-entity-link(
|
||||
href
|
||||
ng-click="$ctrl.handleClick()"
|
||||
ng-class="{ 'history-file-entity-link-selected': $ctrl.isSelected }"
|
||||
)
|
||||
span.history-file-entity-name
|
||||
i.history-file-entity-icon.history-file-entity-icon-folder-state.fa.fa-fw(
|
||||
ng-class="{\
|
||||
'fa-chevron-down': ($ctrl.fileEntity.type === 'folder' && $ctrl.isOpen),\
|
||||
'fa-chevron-right': ($ctrl.fileEntity.type === 'folder' && !$ctrl.isOpen)\
|
||||
}"
|
||||
)
|
||||
i.history-file-entity-icon.fa(
|
||||
ng-class="$ctrl.iconClass"
|
||||
)
|
||||
| {{ ::$ctrl.fileEntity.name }}
|
||||
div(
|
||||
ng-show="$ctrl.isOpen"
|
||||
)
|
||||
history-file-entity(
|
||||
ng-repeat="childEntity in $ctrl.fileEntity.children"
|
||||
file-entity="childEntity"
|
||||
)
|
|
@ -1,4 +1,7 @@
|
|||
.diff-panel.full-size(ng-if="history.isV2", ng-controller="HistoryV2DiffController")
|
||||
.diff-panel.full-size(
|
||||
ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE"
|
||||
ng-controller="HistoryV2DiffController"
|
||||
)
|
||||
.diff(
|
||||
ng-if="!!history.diff && !history.diff.loading && !history.diff.error",
|
||||
ng-class="{ 'diff-binary': history.diff.binary }"
|
||||
|
@ -16,8 +19,13 @@
|
|||
}"
|
||||
)
|
||||
| in <strong>{{history.diff.pathname}}</strong>
|
||||
.history-toolbar-btn(
|
||||
ng-click="toggleHistoryViewMode();"
|
||||
)
|
||||
i.fa
|
||||
| #{translate("view_single_version")}
|
||||
.toolbar-right(ng-if="history.selection.docs[history.selection.pathname].deletedAtV")
|
||||
button.btn.btn-danger.btn-sm(
|
||||
button.btn.btn-danger.btn-xs(
|
||||
ng-click="restoreDeletedFile()"
|
||||
ng-show="!restoreState.error"
|
||||
ng-disabled="restoreState.inflight"
|
||||
|
@ -47,4 +55,27 @@
|
|||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.diff.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
||||
|
||||
.point-in-time-panel.full-size(
|
||||
ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
|
||||
)
|
||||
.point-in-time-editor-container(
|
||||
ng-if="!!history.selectedFile && !history.selectedFile.loading && !history.selectedFile.error"
|
||||
)
|
||||
.hide-ace-cursor(
|
||||
ng-if="!history.selectedFile.binary"
|
||||
ace-editor="history-pointintime",
|
||||
theme="settings.theme",
|
||||
font-size="settings.fontSize",
|
||||
text="history.selectedFile.text",
|
||||
read-only="true",
|
||||
resize-on="layout:main:resize",
|
||||
)
|
||||
.alert.alert-info(ng-if="history.selectedFile.binary")
|
||||
| We're still working on showing image and binary changes, sorry. Stay tuned!
|
||||
.loading-panel(ng-show="history.selectedFile.loading")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
.error-panel(ng-show="history.selectedFile.error")
|
||||
.alert.alert-danger #{translate("generic_something_went_wrong")}
|
13
services/web/app/views/project/editor/history/toolbarV2.pug
Normal file
13
services/web/app/views/project/editor/history/toolbarV2.pug
Normal file
|
@ -0,0 +1,13 @@
|
|||
.history-toolbar(
|
||||
ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
|
||||
)
|
||||
span(ng-show="history.loadingFileTree")
|
||||
i.fa.fa-spin.fa-refresh
|
||||
| #{translate("loading")}...
|
||||
span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}
|
||||
time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
|
||||
.history-toolbar-btn(
|
||||
ng-click="toggleHistoryViewMode();"
|
||||
)
|
||||
i.fa
|
||||
| #{translate("compare_to_another_version")}
|
|
@ -62,6 +62,23 @@ aside#left-menu.full-size(
|
|||
!= moduleIncludes("editorLeftMenu:editing_services", locals)
|
||||
|
||||
|
||||
if showTestControls
|
||||
h4 Test Controls
|
||||
ul.list-unstyled.nav(ng-controller="TestControlsController")
|
||||
li
|
||||
a(href="#" ng-click="richText()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Rich Text
|
||||
li
|
||||
a(href="#" ng-click="openProjectLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| Project-Linked-File Modal
|
||||
li
|
||||
a(href="#" ng-click="openLinkedFileModal()")
|
||||
i.fa.fa-exclamation.fa-fw
|
||||
| URL-Linked-File Modal
|
||||
|
||||
|
||||
h4(ng-show="!anonymous") #{translate("settings")}
|
||||
form.settings(ng-controller="SettingsController", ng-show="!anonymous")
|
||||
.containter-fluid
|
||||
|
@ -179,6 +196,7 @@ aside#left-menu.full-size(
|
|||
option(value="pdfjs") #{translate("built_in")}
|
||||
option(value="native") #{translate("native")}
|
||||
|
||||
|
||||
h4 #{translate("hotkeys")}
|
||||
ul.list-unstyled.nav
|
||||
li(ng-controller="HotkeysController")
|
||||
|
|
26
services/web/app/views/project/editor/new_from_template.pug
Normal file
26
services/web/app/views/project/editor/new_from_template.pug
Normal file
|
@ -0,0 +1,26 @@
|
|||
extends ../../layout
|
||||
|
||||
block content
|
||||
script.
|
||||
$(document).ready(function(){
|
||||
$('#create_form').submit();
|
||||
});
|
||||
|
||||
.editor.full-size
|
||||
.loading-screen()
|
||||
.loading-screen-brand-container
|
||||
.loading-screen-brand(
|
||||
style="height: 20%;"
|
||||
)
|
||||
|
||||
h3.loading-screen-label() #{translate("Opening template")}
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
span.loading-screen-ellip .
|
||||
|
||||
form(id='create_form' method='POST' action='/project/new/template/')
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
input(type="hidden" name="templateId" value=templateId)
|
||||
input(type="hidden" name="templateVersionId" value=templateVersionId)
|
||||
input(type="hidden" name="templateName" value=name)
|
||||
input(type="hidden" name="compiler" value=compiler)
|
|
@ -1,4 +1,7 @@
|
|||
.col-xs-6
|
||||
- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
|
||||
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
|
||||
|
||||
div(class=titleClasses)
|
||||
input.select-item(
|
||||
select-individual,
|
||||
type="checkbox",
|
||||
|
@ -37,8 +40,50 @@
|
|||
tooltip-placement="right"
|
||||
tooltip-append-to-body="true"
|
||||
)
|
||||
.col-xs-4
|
||||
|
||||
div(class=lastUpdatedClasses)
|
||||
if settings.overleaf
|
||||
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
|
||||
else
|
||||
span.last-modified {{project.lastUpdated | formatDate}}
|
||||
|
||||
if settings.overleaf
|
||||
.hidden-xs.col-sm-3.col-md-2.action-btn-row
|
||||
button.btn.btn-link.action-btn(
|
||||
tooltip=translate('copy'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="clone($event)"
|
||||
)
|
||||
i.icon.fa.fa-files-o
|
||||
button.btn.btn-link.action-btn(
|
||||
tooltip=translate('download'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="download($event)"
|
||||
)
|
||||
i.icon.fa.fa-cloud-download
|
||||
button.btn.btn-link.action-btn(
|
||||
ng-if="!project.archived && isOwner()"
|
||||
tooltip=translate('archive'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="archiveOrLeave($event)"
|
||||
)
|
||||
i.icon.fa.fa-inbox
|
||||
button.btn.btn-link.action-btn(
|
||||
ng-if="!project.archived && !isOwner()"
|
||||
tooltip=translate('leave'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="archiveOrLeave($event)"
|
||||
)
|
||||
i.icon.fa.fa-sign-out
|
||||
button.btn.btn-link.action-btn(
|
||||
ng-if="project.archived"
|
||||
tooltip=translate('unarchive'),
|
||||
tooltip-placement="top",
|
||||
tooltip-append-to-body="true",
|
||||
ng-click="restore($event)"
|
||||
)
|
||||
i.icon.fa.fa-reply
|
|
@ -131,7 +131,10 @@
|
|||
)
|
||||
li.container-fluid
|
||||
.row
|
||||
.col-xs-6
|
||||
- var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
|
||||
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
|
||||
|
||||
div(class=titleClasses)
|
||||
input.select-all(
|
||||
select-all,
|
||||
type="checkbox"
|
||||
|
@ -142,9 +145,12 @@
|
|||
.col-xs-2
|
||||
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
|
||||
.col-xs-4
|
||||
div(class=lastUpdatedClasses)
|
||||
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
|
||||
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
|
||||
if settings.overleaf
|
||||
.hidden-xs.col-sm-3.col-md-2.action-btn-row-header
|
||||
span.header #{translate("actions")}
|
||||
li.project_entry.container-fluid(
|
||||
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
|
||||
ng-controller="ProjectListItemController"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.col-xs-6
|
||||
.col-xs-6.col-sm-4.col-md-6
|
||||
.select-item
|
||||
span.v1-badge(
|
||||
aria-label=translate("v1_badge")
|
||||
|
@ -21,5 +21,5 @@
|
|||
.col-xs-2
|
||||
span.owner {{ownerName()}}
|
||||
|
||||
.col-xs-4
|
||||
.col-xs-4.col-sm-3.col-md-2
|
||||
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
|
|
@ -0,0 +1,118 @@
|
|||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered
|
||||
h1 #{translate("start_x_day_trial", {len:'{{trial_len}}'})}
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
p.text-centered #{translate("sl_benefits_plans")}
|
||||
|
||||
.row.top-switch
|
||||
.col-md-6.col-md-offset-3
|
||||
+plan_switch('card')
|
||||
.col-md-2.text-right
|
||||
+currency_dropdown
|
||||
|
||||
div(ng-show="showPlans")
|
||||
.row
|
||||
.col-md-10.col-md-offset-1
|
||||
.row
|
||||
.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
.circle #{translate("free")}
|
||||
+features_free
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
.card-header
|
||||
h2 #{translate("collaborator")}
|
||||
.circle
|
||||
+price_collaborator
|
||||
+features_collaborator
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
.card-header
|
||||
h2 #{translate("professional")}
|
||||
.circle
|
||||
+price_professional
|
||||
+features_professional
|
||||
|
||||
.card-group.text-centered(ng-if="ui.view == 'student'")
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
.circle #{translate("free")}
|
||||
+features_free
|
||||
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
+card_student_monthly
|
||||
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
+card_student_annual
|
||||
|
||||
.row.row-spaced
|
||||
p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
|
||||
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
.alert.alert-info.text-centered
|
||||
| #{translate("interested_in_group_licence")}
|
||||
br
|
||||
a(href, ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
|
||||
|
||||
script(type="text/ng-template", id="groupPlanModalTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("group_plan_enquiry")}
|
||||
.modal-body
|
||||
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()")
|
||||
span(ng-show="sent == false && error == false")
|
||||
.form-group
|
||||
label#title9(for='Field9')
|
||||
| Name
|
||||
input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
|
||||
label#title11.desc(for='Field11')
|
||||
| Email
|
||||
.form-group
|
||||
input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||
label#title12.desc(for='Field12')
|
||||
| University / Company
|
||||
.form-group
|
||||
input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
|
||||
label#title13.desc(for='Field13')
|
||||
| Position
|
||||
.form-group
|
||||
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
.form-group
|
||||
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
|
||||
.form-group.text-center
|
||||
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
|
||||
span(ng-show="sent == true && error == false")
|
||||
p Request Sent, Thank you.
|
||||
span(ng-show="error")
|
||||
p Error sending request.
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate("enjoy_these_features")}
|
||||
.col-md-4
|
||||
.card.features.text-centered
|
||||
i.fa.fa-file-text-o.fa-5x
|
||||
h4 #{translate("unlimited_projects")}
|
||||
p #{translate("create_unlimited_projects")}
|
||||
.col-md-4
|
||||
.card.features.text-centered
|
||||
i.fa.fa-clock-o.fa-5x
|
||||
h4 #{translate("full_doc_history")}
|
||||
p #{translate("never_loose_work")}
|
||||
.col-md-4
|
||||
.card.features.text-centered
|
||||
i.fa.fa-dropbox.fa-5x
|
||||
|
|
||||
i.fa.fa-github.fa-5x
|
||||
h4 #{translate("sync_to_dropbox_and_github")}
|
||||
p #{translate("access_projects_anywhere")}
|
|
@ -0,0 +1,160 @@
|
|||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered
|
||||
h1.text-capitalize #{translate('instant_access')}
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
p.text-centered #{translate("sl_benefits_plans")}
|
||||
|
||||
.row.top-switch
|
||||
.col-md-6.col-md-offset-3
|
||||
+plan_switch('card')
|
||||
.col-md-2.text-right
|
||||
+currency_dropdown
|
||||
|
||||
div(ng-show="showPlans")
|
||||
.row
|
||||
.col-md-10.col-md-offset-1
|
||||
.row
|
||||
.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
h5.tagline #{translate("tagline_personal")}
|
||||
.circle #{translate("free")}
|
||||
+features_free
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
.best-value
|
||||
strong #{translate('best_value')}
|
||||
.card-header
|
||||
h2 #{translate("collaborator")}
|
||||
h5.tagline #{translate("tagline_collaborator")}
|
||||
.circle
|
||||
+price_collaborator
|
||||
+features_collaborator
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
.card-header
|
||||
h2 #{translate("professional")}
|
||||
h5.tagline #{translate("tagline_professional")}
|
||||
.circle
|
||||
+price_professional
|
||||
+features_professional
|
||||
|
||||
.card-group.text-centered(ng-if="ui.view == 'student'")
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
h5.tagline #{translate("tagline_personal")}
|
||||
.circle #{translate("free")}
|
||||
+features_free
|
||||
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
+card_student_annual
|
||||
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
+card_student_monthly
|
||||
|
||||
.row.row-spaced-large.text-centered
|
||||
i.fa.fa-cc-mastercard.fa-2x
|
||||
i.fa.fa-cc-visa.fa-2x
|
||||
i.fa.fa-cc-amex.fa-2x
|
||||
i.fa.fa-cc-paypal.fa-2x
|
||||
div.text-centered #{translate('change_plans_any_time')}<br/> #{translate('billed_after_x_days', {len:'{{trial_len}}'})}
|
||||
|
||||
.row.row-spaced-large
|
||||
.col-md-8.col-md-offset-2
|
||||
.card.text-centered
|
||||
.card-header
|
||||
h2 #{translate('looking_multiple_licenses')}
|
||||
span #{translate('reduce_costs_group_licenses')}
|
||||
br
|
||||
br
|
||||
a.btn.btn-info(href="/i/university/groups") #{translate('find_out_more')}
|
||||
|
||||
div
|
||||
.row.row-spaced-large
|
||||
.col-sm-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate('compare_plan_features')}
|
||||
.row
|
||||
.col-md-6.col-md-offset-3
|
||||
+plan_switch('table')
|
||||
.col-md-3.text-right
|
||||
+currency_dropdown
|
||||
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true")
|
||||
.col-sm-12(ng-if="ui.view != 'student'")
|
||||
+table_premium
|
||||
.col-sm-12(ng-if="ui.view == 'student'")
|
||||
+table_student
|
||||
|
||||
.row.row-spaced-large
|
||||
.col-md-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate('in_good_company')}
|
||||
.row
|
||||
.col-md-6
|
||||
div
|
||||
.row
|
||||
.col-md-3
|
||||
.circle-img
|
||||
img(src=buildImgPath('advocates/erdogmus.jpg') alt="Professor Erdogmus")
|
||||
.col-md-9
|
||||
blockquote
|
||||
p The ability to track changes and the real-time collaborative nature is what sets ShareLaTeX apart.
|
||||
footer Professor Erdogmus, Northeastern University
|
||||
.col-md-6
|
||||
div
|
||||
.row
|
||||
.col-md-3
|
||||
.circle-img
|
||||
img(src=buildImgPath('advocates/henderson.jpg') alt="Rob Henderson")
|
||||
.col-md-9
|
||||
blockquote
|
||||
p ShareLaTeX has proven to be a powerful and robust collaboration tool that is widely used in our School.
|
||||
footer Rob Henderson, School Of Informatics And Computing - Indiana University
|
||||
|
||||
.faq
|
||||
.row.row-spaced-large
|
||||
.col-md-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 FAQ
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate("faq_how_free_trial_works_question")}
|
||||
p #{translate('faq_how_free_trial_works_answer', { len:'{{trial_len}}' })}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_change_plans_question')}
|
||||
p #{translate('faq_change_plans_answer')}
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate('faq_do_collab_need_premium_question')}
|
||||
p #{translate('faq_do_collab_need_premium_answer')}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_need_more_collab_question')}
|
||||
p !{translate('faq_need_more_collab_answer', { referFriendsLink: '<a href="/user/bonus">' + translate('referring_your_friends') + '</a>'})}
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate('faq_purchase_more_licenses_question')}
|
||||
p !{translate('faq_purchase_more_licenses_answer', { groupLink: '<a href="/i/university/groups">' + translate('discounted_group_accounts') + '</a>' })}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_monthly_or_annual_question')}
|
||||
p #{translate('faq_monthly_or_annual_answer')}
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate('faq_how_to_pay_question')}
|
||||
p #{translate('faq_how_to_pay_answer')}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_pay_by_invoice_question')}
|
||||
p !{translate('faq_pay_by_invoice_answer', { groupLink: '<a href="/i/university/groups">' + translate('discounted_group_accounts') + '</a>' })}
|
||||
.row.row-spaced-large.text-centery
|
||||
.col-md-12
|
||||
.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate('still_have_questions')}
|
||||
button.btn.btn-info.btn-header.text-capitalize(ng-controller="ContactGeneralModal" ng-click="openModal()") #{translate('get_in_touch')}
|
||||
!= moduleIncludes("contactModalGeneral", locals)
|
162
services/web/app/views/subscriptions/_plans_page_mixins.pug
Normal file
162
services/web/app/views/subscriptions/_plans_page_mixins.pug
Normal file
|
@ -0,0 +1,162 @@
|
|||
//- Buy Buttons
|
||||
mixin btn_buy_collaborator(location)
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{currencyCode}}",
|
||||
ng-click="signUpNowClicked('collaborator','" + location + "')"
|
||||
)
|
||||
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
|
||||
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
|
||||
mixin btn_buy_free(location)
|
||||
a.btn.btn-info(
|
||||
href="/register"
|
||||
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
|
||||
ng-click="signUpNowClicked('free','" + location + "')"
|
||||
)
|
||||
span(ng-if="plansVariant !== 'more-details'") #{translate('sign_up_now')}
|
||||
span.text-capitalize(ng-if="plansVariant === 'more-details'") #{translate('get_started_now')}
|
||||
mixin btn_buy_professional(location)
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}"
|
||||
ng-click="signUpNowClicked('professional','" + location + "')"
|
||||
)
|
||||
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
|
||||
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
|
||||
mixin btn_buy_student(location, plan)
|
||||
if plan == 'annual'
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode=student-annual¤cy={{currencyCode}}",
|
||||
ng-click="signUpNowClicked('student-annual','" + location + "')"
|
||||
) #{translate("buy_now")}
|
||||
else
|
||||
//- planQueryString will contain _free_trial_7_days
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode=student{{planQueryString}}¤cy={{currencyCode}}",
|
||||
ng-click="signUpNowClicked('student-monthly','" + location + "')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
//- Cards
|
||||
mixin card_student_annual
|
||||
.best-value(ng-if="plansVariant == 'more-details'")
|
||||
strong #{translate('best_value')}
|
||||
.card-header
|
||||
h2 #{translate("student")} (#{translate("annual")})
|
||||
h5.tagline(ng-if="plansVariant == 'more-details'") #{translate('tagline_student_annual')}
|
||||
.circle
|
||||
span
|
||||
+price_student_annual
|
||||
+features_student('card', 'annual')
|
||||
mixin card_student_monthly
|
||||
.card-header
|
||||
h2 #{translate("student")}
|
||||
h5.tagline(ng-if="plansVariant == 'more-details'") #{translate('tagline_student_monthly')}
|
||||
.circle
|
||||
span
|
||||
+price_student_monthly
|
||||
+features_student('card', 'monthly')
|
||||
|
||||
//- Features Lists
|
||||
mixin features_collaborator
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:10})}
|
||||
+features_premium
|
||||
li
|
||||
br
|
||||
+btn_buy_collaborator('card')
|
||||
mixin features_free
|
||||
ul.list-unstyled
|
||||
li #{translate("one_collaborator")}
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'")
|
||||
li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'")
|
||||
li(class="hidden-xs hidden-sm" ng-if="plansVariant === 'more-details'")
|
||||
li
|
||||
br
|
||||
+btn_buy_free('card')
|
||||
mixin features_premium
|
||||
li(ng-if="plansVariant != 'more-details'") #{translate("full_doc_history")}
|
||||
li(ng-if="plansVariant != 'more-details'") #{translate("sync_to_dropbox")}
|
||||
li(ng-if="plansVariant != 'more-details'") #{translate("sync_to_github")}
|
||||
li(ng-if="plansVariant === 'more-details'")
|
||||
li(ng-if="plansVariant === 'more-details'")
|
||||
strong #{translate('all_premium_features')}
|
||||
li(ng-if="plansVariant === 'more-details'") #{translate('sync_dropbox_github')}
|
||||
li(ng-if="plansVariant === 'more-details'") #{translate('full_doc_history')}
|
||||
li(ng-if="plansVariant === 'more-details'") #{translate('track_changes')}
|
||||
li(ng-if="plansVariant === 'more-details'") + #{translate('more').toLowerCase()}
|
||||
mixin features_professional
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("unlimited_collabs")}
|
||||
+features_premium
|
||||
li
|
||||
br
|
||||
+btn_buy_professional('card')
|
||||
mixin features_student(location, plan)
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:6})}
|
||||
+features_premium
|
||||
li
|
||||
br
|
||||
+btn_buy_student(location, plan)
|
||||
|
||||
//- Prices
|
||||
mixin price_collaborator
|
||||
span(ng-if="ui.view == 'monthly'")
|
||||
| {{plans[currencyCode]['collaborator']['monthly']}}
|
||||
span.small /mo
|
||||
span(ng-if="ui.view == 'annual'")
|
||||
| {{plans[currencyCode]['collaborator']['annual']}}
|
||||
span.small /yr
|
||||
mixin price_professional
|
||||
span(ng-if="ui.view == 'monthly'")
|
||||
| {{plans[currencyCode]['professional']['monthly']}}
|
||||
span.small /mo
|
||||
span(ng-if="ui.view == 'annual'")
|
||||
| {{plans[currencyCode]['professional']['annual']}}
|
||||
span.small /yr
|
||||
mixin price_student_annual
|
||||
| {{plans[currencyCode]['student']['annual']}}
|
||||
span.small /yr
|
||||
mixin price_student_monthly
|
||||
| {{plans[currencyCode]['student']['monthly']}}
|
||||
span.small /mo
|
||||
|
||||
//- UI Control
|
||||
mixin currency_dropdown
|
||||
.dropdown.currency-dropdown(dropdown)
|
||||
a.btn.btn-default.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
|
||||
span.caret
|
||||
|
||||
ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
|
||||
li(ng-repeat="(currency, value) in plans")
|
||||
a(
|
||||
href="#",
|
||||
ng-click="changeCurreny($event, currency)"
|
||||
) {{currency}} ({{value['symbol']}})
|
||||
mixin plan_switch(location)
|
||||
ul.nav.nav-pills
|
||||
li(ng-class="{'active': ui.view == 'monthly'}")
|
||||
a(
|
||||
href="#"
|
||||
ng-click="switchToMonthly($event,'" + location + "')"
|
||||
) #{translate("monthly")}
|
||||
li(ng-class="{'active': ui.view == 'annual'}")
|
||||
a(
|
||||
href="#"
|
||||
ng-click="switchToAnnual($event,'" + location + "')"
|
||||
) #{translate("annual")}
|
||||
li(ng-class="{'active': ui.view == 'student'}")
|
||||
a(
|
||||
href="#"
|
||||
ng-click="switchToStudent($event,'" + location + "')"
|
||||
) #{translate("half_price_student")}
|
||||
|
107
services/web/app/views/subscriptions/_plans_page_tables.pug
Normal file
107
services/web/app/views/subscriptions/_plans_page_tables.pug
Normal file
|
@ -0,0 +1,107 @@
|
|||
|
||||
//- Features Tables
|
||||
mixin table_premium
|
||||
table.card.plans-table
|
||||
tr
|
||||
th
|
||||
th #{translate("personal")}
|
||||
th #{translate("collaborator")}
|
||||
.outer.outer-top
|
||||
.outer-content
|
||||
.best-value
|
||||
strong #{translate('best_value')}
|
||||
th #{translate("professional")}
|
||||
|
||||
tr
|
||||
td #{translate("price")}
|
||||
td #{translate("free")}
|
||||
td
|
||||
+price_collaborator
|
||||
td
|
||||
+price_professional
|
||||
|
||||
for feature in planFeatures
|
||||
tr
|
||||
td(event-tracking="features-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}-exp-{{plansVariant}}`)
|
||||
if feature.info
|
||||
span(tooltip=translate(feature.info)) #{translate(feature.feature)}
|
||||
else
|
||||
| #{translate(feature.feature)}
|
||||
for plan in feature.plans
|
||||
td
|
||||
if feature.value == 'str'
|
||||
| #{plan}
|
||||
else if plan
|
||||
i.fa.fa-check
|
||||
else
|
||||
i.fa.fa-times
|
||||
|
||||
tr
|
||||
td
|
||||
td
|
||||
+btn_buy_free('table')
|
||||
td
|
||||
+btn_buy_collaborator('table')
|
||||
.outer.outer-btm
|
||||
.outer-content
|
||||
td
|
||||
+btn_buy_professional('table')
|
||||
|
||||
mixin table_cell_student(feature)
|
||||
if feature.value == 'str'
|
||||
| #{feature.student}
|
||||
else if feature.student
|
||||
i.fa.fa-check
|
||||
else
|
||||
i.fa.fa-times
|
||||
|
||||
mixin table_student
|
||||
table.card.plans-table
|
||||
tr
|
||||
th
|
||||
th #{translate("personal")}
|
||||
th #{translate("student")} (#{translate("annual")})
|
||||
.outer.outer-top
|
||||
.outer-content
|
||||
.best-value
|
||||
strong Best Value
|
||||
th #{translate("student")}
|
||||
|
||||
tr
|
||||
td #{translate("price")}
|
||||
td #{translate("free")}
|
||||
td
|
||||
+price_student_annual
|
||||
td
|
||||
+price_student_monthly
|
||||
|
||||
for feature in planFeatures
|
||||
tr
|
||||
td(event-tracking="plans-page-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}-exp-{{plansVariant}}`)
|
||||
if feature.info
|
||||
span(tooltip=translate(feature.info)) #{translate(feature.feature)}
|
||||
else
|
||||
| #{translate(feature.feature)}
|
||||
td
|
||||
if feature.value == 'str'
|
||||
| #{feature.plans.free}
|
||||
else if feature.plans.free
|
||||
i.fa.fa-check
|
||||
else
|
||||
i.fa.fa-times
|
||||
td
|
||||
+table_cell_student(feature)
|
||||
td
|
||||
+table_cell_student(feature)
|
||||
|
||||
tr
|
||||
td
|
||||
td
|
||||
+btn_buy_free('table')
|
||||
td
|
||||
+btn_buy_student('table', 'annual')
|
||||
.outer.outer-btm
|
||||
.outer-content
|
||||
td
|
||||
+btn_buy_student('table', 'monthly')
|
||||
|
|
@ -31,7 +31,10 @@ block content
|
|||
li(ng-repeat="(currency, value) in plans")
|
||||
a(
|
||||
ng-click="changeCurrency(currency)",
|
||||
) {{currency}} ({{value['symbol']}})
|
||||
) {{currency}} ({{value['symbol']}})
|
||||
.row(ng-if="plansVariant == 'more-details' && planCode == 'student-annual' || plansVariant == 'more-details' && planCode == 'student-monthly'")
|
||||
.col-xs-12
|
||||
p.student-disclaimer #{translate('student_disclaimer')}
|
||||
hr.thin
|
||||
.row
|
||||
.col-md-12.text-center
|
||||
|
|
|
@ -1,253 +1,18 @@
|
|||
extends ../layout
|
||||
|
||||
include _plans_page_mixins
|
||||
include _plans_page_tables
|
||||
|
||||
block scripts
|
||||
script(type='text/javascript').
|
||||
window.recomendedCurrency = '#{recomendedCurrency}'
|
||||
window.abCurrencyFlag = '#{abCurrencyFlag}'
|
||||
window.shouldABTestPlans = #{shouldABTestPlans || false}
|
||||
|
||||
script(type='text/javascript').
|
||||
(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true;
|
||||
s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js';
|
||||
var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})();
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.content.plans(ng-controller="PlansController")
|
||||
.container
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered
|
||||
h1(ng-cloak) #{translate("start_x_day_trial", {len:'{{trial_len}}'})}
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
p.text-centered #{translate("sl_benefits_plans")}
|
||||
|
||||
.row(ng-cloak)
|
||||
.col-md-6.col-md-offset-3
|
||||
ul.nav.nav-pills
|
||||
li(ng-class="{'active': ui.view == 'monthly'}")
|
||||
a(
|
||||
href,
|
||||
ng-click="switchToMonthly()"
|
||||
) #{translate("monthly")}
|
||||
li(ng-class="{'active': ui.view == 'annual'}")
|
||||
a(
|
||||
href
|
||||
ng-click="switchToAnnual()"
|
||||
) #{translate("annual")}
|
||||
li(ng-class="{'active': ui.view == 'student'}")
|
||||
a(
|
||||
href,
|
||||
ng-click="switchToStudent()"
|
||||
) #{translate("half_price_student")}
|
||||
.col-md-2.text-right
|
||||
.dropdown.currency-dropdown(dropdown)
|
||||
a.btn.btn-default.dropdown-toggle#currenyDropdown(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
|
||||
span.caret
|
||||
|
||||
ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
|
||||
li(ng-repeat="(currency, value) in plans")
|
||||
a(
|
||||
href,
|
||||
ng-click="changeCurreny(currency)"
|
||||
) {{currency}} ({{value['symbol']}})
|
||||
|
||||
div(ng-show="showPlans")
|
||||
.row(ng-cloak)
|
||||
.col-md-10.col-md-offset-1
|
||||
.row
|
||||
.card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
.circle #{translate("free")}
|
||||
ul.list-unstyled
|
||||
li #{translate("one_collaborator")}
|
||||
li
|
||||
li
|
||||
li
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(
|
||||
href="/register"
|
||||
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
|
||||
) #{translate("sign_up_now")}
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
.card-header
|
||||
h2 #{translate("collaborator")}
|
||||
.circle
|
||||
span(ng-if="ui.view == 'monthly'")
|
||||
| {{plans[currencyCode]['collaborator']['monthly']}}
|
||||
span.small /mo
|
||||
span(ng-if="ui.view == 'annual'")
|
||||
| {{plans[currencyCode]['collaborator']['annual']}}
|
||||
span.small /yr
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:10})}
|
||||
li #{translate("full_doc_history")}
|
||||
li #{translate("sync_to_dropbox")}
|
||||
li #{translate("sync_to_github")}
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
|
||||
)
|
||||
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
|
||||
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
.card-header
|
||||
h2 #{translate("professional")}
|
||||
.circle
|
||||
span(ng-if="ui.view == 'monthly'")
|
||||
| {{plans[currencyCode]['professional']['monthly']}}
|
||||
span.small /mo
|
||||
span(ng-if="ui.view == 'annual'")
|
||||
| {{plans[currencyCode]['professional']['annual']}}
|
||||
span.small /yr
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("unlimited_collabs")}
|
||||
li #{translate("full_doc_history")}
|
||||
li #{translate("sync_to_dropbox")}
|
||||
li #{translate("sync_to_github")}
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('professional')"
|
||||
)
|
||||
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
|
||||
span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
|
||||
|
||||
.card-group.text-centered(ng-if="ui.view == 'student'")
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
.circle #{translate("free")}
|
||||
ul.list-unstyled
|
||||
li #{translate("one_collaborator")}
|
||||
li
|
||||
li
|
||||
li
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(
|
||||
href="/register"
|
||||
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
|
||||
) #{translate("sign_up_now")}
|
||||
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
.card-header
|
||||
h2 #{translate("student")}
|
||||
.circle
|
||||
span
|
||||
| {{plans[currencyCode]['student']['monthly']}}
|
||||
span.small /mo
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:6})}
|
||||
li #{translate("full_doc_history")}
|
||||
li #{translate("sync_to_dropbox")}
|
||||
li #{translate("sync_to_github")}
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}¤cy={{currencyCode}}",
|
||||
ng-click="signUpNowClicked('student-monthly')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
.card-header
|
||||
h2 #{translate("student")} (#{translate("annual")})
|
||||
.circle
|
||||
span
|
||||
| {{plans[currencyCode]['student']['annual']}}
|
||||
span.small /yr
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:6})}
|
||||
li #{translate("full_doc_history")}
|
||||
li #{translate("sync_to_dropbox")}
|
||||
li #{translate("sync_to_github")}
|
||||
li
|
||||
br
|
||||
a.btn.btn-info(
|
||||
ng-href="/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}¤cy={{currencyCode}}",
|
||||
ng-click="signUpNowClicked('student-annual')"
|
||||
) #{translate("buy_now")}
|
||||
|
||||
|
||||
|
||||
.row.row-spaced(ng-cloak)
|
||||
p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
|
||||
|
||||
.row(ng-cloak)
|
||||
.col-md-8.col-md-offset-2
|
||||
.alert.alert-info.text-centered
|
||||
| #{translate("interested_in_group_licence")}
|
||||
br
|
||||
a(href, ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
|
||||
|
||||
script(type="text/ng-template", id="groupPlanModalTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("group_plan_enquiry")}
|
||||
.modal-body
|
||||
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
|
||||
span(ng-show="sent == false && error == false")
|
||||
.form-group
|
||||
label#title9(for='Field9')
|
||||
| Name
|
||||
input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
|
||||
label#title11.desc(for='Field11')
|
||||
| Email
|
||||
.form-group
|
||||
input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||
label#title12.desc(for='Field12')
|
||||
| University / Company
|
||||
.form-group
|
||||
input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
|
||||
label#title13.desc(for='Field13')
|
||||
| Position
|
||||
.form-group
|
||||
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
.form-group
|
||||
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';")
|
||||
.form-group.text-center
|
||||
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
|
||||
span(ng-show="sent == true && error == false")
|
||||
p Request Sent, Thank you.
|
||||
span(ng-show="error")
|
||||
p Error sending request.
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate("enjoy_these_features")}
|
||||
.col-md-4
|
||||
.card.features.text-centered
|
||||
i.fa.fa-file-text-o.fa-5x
|
||||
h4 #{translate("unlimited_projects")}
|
||||
p #{translate("create_unlimited_projects")}
|
||||
.col-md-4
|
||||
.card.features.text-centered
|
||||
i.fa.fa-clock-o.fa-5x
|
||||
h4 #{translate("full_doc_history")}
|
||||
p #{translate("never_loose_work")}
|
||||
.col-md-4
|
||||
.card.features.text-centered
|
||||
i.fa.fa-dropbox.fa-5x
|
||||
|
|
||||
i.fa.fa-github.fa-5x
|
||||
h4 #{translate("sync_to_dropbox_and_github")}
|
||||
p #{translate("access_projects_anywhere")}
|
||||
.container(class="more-details" ng-cloak ng-if="plansVariant === 'more-details'")
|
||||
include _plans_page_details_more
|
||||
.container(ng-cloak ng-if="plansVariant != 'more-details'")
|
||||
include _plans_page_details_less
|
||||
|
|
|
@ -146,8 +146,8 @@ module.exports = settings =
|
|||
url: "http://#{process.env['CONTACTS_HOST'] or 'localhost'}:3036"
|
||||
sixpack:
|
||||
url: ""
|
||||
# references:
|
||||
# url: "http://localhost:3040"
|
||||
references:
|
||||
url: "http://#{process.env['REFERENCES_HOST'] or 'localhost'}:3040"
|
||||
notifications:
|
||||
url: "http://#{process.env['NOTIFICATIONS_HOST'] or 'localhost'}:3042"
|
||||
analytics:
|
||||
|
|
|
@ -17,6 +17,7 @@ services:
|
|||
PROJECT_HISTORY_ENABLED: 'true'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url'
|
||||
LINKED_URL_PROXY: 'http://localhost:6543'
|
||||
ENABLED_LINKED_FILE_TYPES: 'url,project_file'
|
||||
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
|
||||
depends_on:
|
||||
- redis
|
||||
|
|
|
@ -4,11 +4,23 @@
|
|||
# event not sent to MB.
|
||||
# for MB, add event-tracking-mb='true'
|
||||
# by default, event sent to MB via sendMB
|
||||
# this can be changed to use sendMBOnce via event-tracking-send-once='true' attribute
|
||||
# event not sent to GA.
|
||||
# for GA, add event-tracking-ga attribute, where the value is the GA category
|
||||
# Either GA or MB can use the attribute event-tracking-send-once='true' to
|
||||
# send event just once
|
||||
# MB will use the key and GA will use the action to determine if the event
|
||||
# has been sent
|
||||
# event-tracking-trigger attribute is required to send event
|
||||
|
||||
isInViewport = (element) ->
|
||||
elTop = element.offset().top
|
||||
elBtm = elTop + element.outerHeight()
|
||||
|
||||
viewportTop = $(window).scrollTop()
|
||||
viewportBtm = viewportTop + $(window).height()
|
||||
|
||||
elBtm > viewportTop && elTop < viewportBtm
|
||||
|
||||
define [
|
||||
'base'
|
||||
], (App) ->
|
||||
|
@ -22,20 +34,42 @@ define [
|
|||
sendGA = attrs.eventTrackingGa || false
|
||||
sendMB = attrs.eventTrackingMb || false
|
||||
sendMBFunction = if attrs.eventTrackingSendOnce then 'sendMBOnce' else 'sendMB'
|
||||
sendGAFunction = if attrs.eventTrackingSendOnce then 'sendGAOnce' else 'send'
|
||||
segmentation = scope.eventSegmentation || {}
|
||||
|
||||
segmentation.page = window.location.pathname
|
||||
|
||||
sendEvent = () ->
|
||||
sendEvent = (scrollEvent) ->
|
||||
###
|
||||
@param {boolean} scrollEvent Use to unbind scroll event
|
||||
###
|
||||
if sendMB
|
||||
event_tracking[sendMBFunction] scope.eventTracking, segmentation
|
||||
if sendGA
|
||||
event_tracking.send attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
|
||||
event_tracking[sendGAFunction] attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
|
||||
if scrollEvent
|
||||
$(window).unbind('resize scroll')
|
||||
|
||||
if attrs.eventTrackingTrigger == 'load'
|
||||
sendEvent()
|
||||
else if attrs.eventTrackingTrigger == 'click'
|
||||
element.on 'click', (e) ->
|
||||
sendEvent()
|
||||
else if attrs.eventTrackingTrigger == 'hover'
|
||||
timer = null
|
||||
timeoutAmt = 500
|
||||
if attrs.eventHoverAmt
|
||||
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
|
||||
element.on 'mouseenter', () ->
|
||||
timer = setTimeout((-> sendEvent()), timeoutAmt)
|
||||
return
|
||||
.on 'mouseleave', () ->
|
||||
clearTimeout(timer)
|
||||
else if attrs.eventTrackingTrigger == 'scroll'
|
||||
if !event_tracking.eventInCache(scope.eventTracking)
|
||||
$(window).on 'resize scroll', () ->
|
||||
_.throttle(
|
||||
if isInViewport(element) && !event_tracking.eventInCache(scope.eventTracking)
|
||||
sendEvent(true)
|
||||
, 500)
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -18,6 +18,7 @@ define [
|
|||
"ide/chat/index"
|
||||
"ide/clone/index"
|
||||
"ide/hotkeys/index"
|
||||
"ide/test-controls/index"
|
||||
"ide/wordcount/index"
|
||||
"ide/directives/layout"
|
||||
"ide/directives/validFile"
|
||||
|
@ -34,6 +35,7 @@ define [
|
|||
"directives/videoPlayState"
|
||||
"services/queued-http"
|
||||
"services/validateCaptcha"
|
||||
"services/wait-for"
|
||||
"filters/formatDate"
|
||||
"main/event"
|
||||
"main/account-upgrade"
|
||||
|
@ -54,7 +56,7 @@ define [
|
|||
SafariScrollPatcher
|
||||
) ->
|
||||
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) ->
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata, $q) ->
|
||||
# Don't freak out if we're already in an apply callback
|
||||
$scope.$originalApply = $scope.$apply
|
||||
$scope.$apply = (fn = () ->) ->
|
||||
|
@ -211,11 +213,10 @@ define [
|
|||
try
|
||||
chromeVersion = parseFloat(navigator.userAgent.split(" Chrome/")[1]) || null;
|
||||
browserIsChrome61or62 = (
|
||||
chromeVersion? &&
|
||||
(chromeVersion == 61 || chromeVersion == 62)
|
||||
chromeVersion?
|
||||
)
|
||||
if browserIsChrome61or62
|
||||
document.styleSheets[0].insertRule(".ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; }", 1)
|
||||
document.styleSheets[0].insertRule(".ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; font-weight: bold; }", 1)
|
||||
catch err
|
||||
console.error err
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ define [
|
|||
"base"
|
||||
"moment"
|
||||
], (App, moment) ->
|
||||
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", ($scope, $rootScope, $http, $timeout, $element, ide) ->
|
||||
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", "waitFor", ($scope, $rootScope, $http, $timeout, $element, ide, waitFor) ->
|
||||
|
||||
TWO_MEGABYTES = 2 * 1024 * 1024
|
||||
|
||||
|
@ -31,6 +31,7 @@ define [
|
|||
data: null
|
||||
|
||||
$scope.refreshing = false
|
||||
$scope.refreshError = null
|
||||
|
||||
MAX_URL_LENGTH = 60
|
||||
FRONT_OF_URL_LENGTH = 35
|
||||
|
@ -48,9 +49,27 @@ define [
|
|||
|
||||
$scope.refreshFile = (file) ->
|
||||
$scope.refreshing = true
|
||||
$scope.refreshError = null
|
||||
ide.fileTreeManager.refreshLinkedFile(file)
|
||||
.then () ->
|
||||
loadTextFileFilePreview()
|
||||
.then (response) ->
|
||||
{ data } = response
|
||||
{ new_file_id } = data
|
||||
$timeout(
|
||||
() ->
|
||||
waitFor(
|
||||
() ->
|
||||
ide.fileTreeManager.findEntityById(new_file_id)
|
||||
5000
|
||||
)
|
||||
.then (newFile) ->
|
||||
ide.binaryFilesManager.openFile(newFile)
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
, 0
|
||||
)
|
||||
$scope.refreshError = null
|
||||
.catch (response) ->
|
||||
$scope.refreshError = response.data
|
||||
.finally () ->
|
||||
$scope.refreshing = false
|
||||
|
||||
|
@ -86,11 +105,9 @@ define [
|
|||
# show dots when payload is closs to cutoff
|
||||
if data.length >= (TWO_MEGABYTES - 200)
|
||||
$scope.textPreview.shouldShowDots = true
|
||||
try
|
||||
# remove last partial line
|
||||
data = data.replace(/\n.*$/, '')
|
||||
finally
|
||||
$scope.textPreview.data = data
|
||||
data = data?.replace?(/\n.*$/, '')
|
||||
$scope.textPreview.data = data
|
||||
$timeout(setHeight, 0)
|
||||
.catch (error) ->
|
||||
console.error(error)
|
||||
|
|
|
@ -14,7 +14,7 @@ define [
|
|||
opening: true
|
||||
trackChanges: false
|
||||
wantTrackChanges: false
|
||||
richText: false
|
||||
showRichText: false
|
||||
}
|
||||
|
||||
@$scope.$on "entity:selected", (event, entity) =>
|
||||
|
|
|
@ -335,6 +335,11 @@ define [
|
|||
|
||||
return null
|
||||
|
||||
projectContainsFolder: () ->
|
||||
for entity in @$scope.rootFolder.children
|
||||
return true if entity.type == 'folder'
|
||||
return false
|
||||
|
||||
existsInThisFolder: (folder, name) ->
|
||||
for entity in folder?.children or []
|
||||
return true if entity.name is name
|
||||
|
|
|
@ -43,6 +43,19 @@ define [
|
|||
}
|
||||
)
|
||||
|
||||
$scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () ->
|
||||
unless 'project_file' in window.data.enabledLinkedFileTypes
|
||||
console.warn("Project linked files are not enabled")
|
||||
return
|
||||
$modal.open(
|
||||
templateUrl: "projectLinkedFileModalTemplate"
|
||||
controller: "ProjectLinkedFileModalController"
|
||||
scope: $scope
|
||||
resolve: {
|
||||
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
|
||||
}
|
||||
)
|
||||
|
||||
$scope.orderByFoldersFirst = (entity) ->
|
||||
return '0' if entity?.type == "folder"
|
||||
return '1'
|
||||
|
@ -201,6 +214,117 @@ define [
|
|||
$modalInstance.dismiss('cancel')
|
||||
]
|
||||
|
||||
App.controller "ProjectLinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
$scope.data =
|
||||
projects: null # or []
|
||||
selectedProjectId: null
|
||||
projectEntities: null # or []
|
||||
selectedProjectEntity: null
|
||||
name: null
|
||||
$scope.state =
|
||||
inFlight:
|
||||
projects: false
|
||||
entities: false
|
||||
create: false
|
||||
error: false
|
||||
|
||||
$scope.$watch 'data.selectedProjectId', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
$scope.data.selectedProjectEntity = null
|
||||
$scope.getProjectEntities($scope.data.selectedProjectId)
|
||||
|
||||
# auto-set filename based on selected file
|
||||
$scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) ->
|
||||
return if !newVal
|
||||
fileName = newVal.split('/').reverse()[0]
|
||||
if fileName
|
||||
$scope.data.name = fileName
|
||||
|
||||
_setInFlight = (type) ->
|
||||
$scope.state.inFlight[type] = true
|
||||
|
||||
_reset = (opts) ->
|
||||
isError = opts.err == true
|
||||
inFlight = $scope.state.inFlight
|
||||
inFlight.projects = inFlight.entities = inFlight.create = false
|
||||
$scope.state.error = isError
|
||||
|
||||
$scope.shouldEnableProjectSelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && data.projects
|
||||
|
||||
$scope.shouldEnableProjectEntitySelect = () ->
|
||||
{ state, data } = $scope
|
||||
return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId
|
||||
|
||||
$scope.shouldEnableCreateButton = () ->
|
||||
state = $scope.state
|
||||
data = $scope.data
|
||||
return !state.inFlight.projects &&
|
||||
!state.inFlight.entities &&
|
||||
data.projects &&
|
||||
data.selectedProjectId &&
|
||||
data.projectEntities &&
|
||||
data.selectedProjectEntity &&
|
||||
data.name
|
||||
|
||||
$scope.getUserProjects = () ->
|
||||
_setInFlight('projects')
|
||||
ide.$http.get("/user/projects", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
$scope.data.projectEntities = null
|
||||
$scope.data.projects = resp.data.projects.filter (p) ->
|
||||
p._id != ide.project_id
|
||||
_reset(err: false)
|
||||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.getProjectEntities = (project_id) =>
|
||||
_setInFlight('entities')
|
||||
ide.$http.get("/project/#{project_id}/entities", {
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
.then (resp) ->
|
||||
if $scope.data.selectedProjectId == resp.data.project_id
|
||||
$scope.data.projectEntities = resp.data.entities
|
||||
_reset(err: false)
|
||||
.catch (err) ->
|
||||
_reset(err: true)
|
||||
|
||||
$scope.init = () ->
|
||||
$scope.getUserProjects()
|
||||
$timeout($scope.init, 0)
|
||||
|
||||
$scope.create = () ->
|
||||
projectId = $scope.data.selectedProjectId
|
||||
path = $scope.data.selectedProjectEntity
|
||||
name = $scope.data.name
|
||||
if !name || !path || !projectId
|
||||
_reset(err: true)
|
||||
return
|
||||
_setInFlight('create')
|
||||
ide.fileTreeManager
|
||||
.createLinkedFile(name, parent_folder, 'project_file', {
|
||||
source_project_id: projectId,
|
||||
source_entity_path: path
|
||||
})
|
||||
.then () ->
|
||||
_reset(err: false)
|
||||
$modalInstance.close()
|
||||
.catch (response)->
|
||||
{ data } = response
|
||||
_reset(err: true)
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
||||
]
|
||||
|
||||
# TODO: rename all this to UrlLinkedFilModalController
|
||||
App.controller "LinkedFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
"ide/file-tree/util/iconTypeFromName"
|
||||
], (App, iconTypeFromName) ->
|
||||
App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
|
||||
$scope.select = (e) ->
|
||||
if e.ctrlKey or e.metaKey
|
||||
|
@ -70,18 +71,7 @@ define [
|
|||
$scope.$on "delete:selected", () ->
|
||||
$scope.openDeleteModal() if $scope.entity.selected
|
||||
|
||||
$scope.iconTypeFromName = (name) ->
|
||||
ext = name.split(".").pop()?.toLowerCase()
|
||||
if ext in ["png", "pdf", "jpg", "jpeg", "gif"]
|
||||
return "image"
|
||||
else if ext in ["csv", "xls", "xlsx"]
|
||||
return "table"
|
||||
else if ext in ["py", "r"]
|
||||
return "file-text"
|
||||
else if ext in ['bib']
|
||||
return 'book'
|
||||
else
|
||||
return "file"
|
||||
$scope.iconTypeFromName = iconTypeFromName
|
||||
]
|
||||
|
||||
App.controller "DeleteEntityModalController", [
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
define [], () ->
|
||||
return iconTypeFromName = (name) ->
|
||||
ext = name.split(".").pop()?.toLowerCase()
|
||||
if ext in ["png", "pdf", "jpg", "jpeg", "gif"]
|
||||
return "image"
|
||||
else if ext in ["csv", "xls", "xlsx"]
|
||||
return "table"
|
||||
else if ext in ["py", "r"]
|
||||
return "file-text"
|
||||
else if ext in ['bib']
|
||||
return 'book'
|
||||
else
|
||||
return "file"
|
|
@ -4,7 +4,6 @@ define [
|
|||
"ide/history/util/displayNameForUser"
|
||||
"ide/history/controllers/HistoryListController"
|
||||
"ide/history/controllers/HistoryDiffController"
|
||||
"ide/history/controllers/HistoryV2DiffController"
|
||||
"ide/history/directives/infiniteScroll"
|
||||
], (moment, ColorManager, displayNameForUser) ->
|
||||
class HistoryManager
|
||||
|
|
|
@ -2,13 +2,20 @@ define [
|
|||
"moment"
|
||||
"ide/colors/ColorManager"
|
||||
"ide/history/util/displayNameForUser"
|
||||
"ide/history/controllers/HistoryListController"
|
||||
"ide/history/controllers/HistoryDiffController"
|
||||
"ide/history/util/HistoryViewModes"
|
||||
"ide/history/controllers/HistoryV2ListController"
|
||||
"ide/history/controllers/HistoryV2DiffController"
|
||||
"ide/history/controllers/HistoryV2FileTreeController"
|
||||
"ide/history/directives/infiniteScroll"
|
||||
], (moment, ColorManager, displayNameForUser) ->
|
||||
"ide/history/components/historyEntriesList"
|
||||
"ide/history/components/historyEntry"
|
||||
"ide/history/components/historyFileTree"
|
||||
"ide/history/components/historyFileEntity"
|
||||
], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
|
||||
class HistoryManager
|
||||
constructor: (@ide, @$scope) ->
|
||||
@reset()
|
||||
@$scope.HistoryViewModes = HistoryViewModes
|
||||
|
||||
@$scope.toggleHistory = () =>
|
||||
if @$scope.ui.view == "history"
|
||||
|
@ -16,17 +23,31 @@ define [
|
|||
else
|
||||
@show()
|
||||
|
||||
@$scope.$watch "history.selection.updates", (updates) =>
|
||||
if updates? and updates.length > 0
|
||||
@_selectDocFromUpdates()
|
||||
@reloadDiff()
|
||||
@$scope.toggleHistoryViewMode = () =>
|
||||
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
||||
@reset()
|
||||
@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
|
||||
else
|
||||
@reset()
|
||||
@$scope.history.viewMode = HistoryViewModes.COMPARE
|
||||
|
||||
@$scope.$watch "history.selection.pathname", () =>
|
||||
@reloadDiff()
|
||||
@$scope.$watch "history.selection.updates", (updates) =>
|
||||
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
||||
if updates? and updates.length > 0
|
||||
@_selectDocFromUpdates()
|
||||
@reloadDiff()
|
||||
|
||||
@$scope.$watch "history.selection.pathname", (pathname) =>
|
||||
if @$scope.history.viewMode == HistoryViewModes.POINT_IN_TIME
|
||||
if pathname?
|
||||
@loadFileAtPointInTime()
|
||||
else
|
||||
@reloadDiff()
|
||||
|
||||
show: () ->
|
||||
@$scope.ui.view = "history"
|
||||
@reset()
|
||||
@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
|
||||
|
||||
hide: () ->
|
||||
@$scope.ui.view = "editor"
|
||||
|
@ -35,6 +56,7 @@ define [
|
|||
@$scope.history = {
|
||||
isV2: true
|
||||
updates: []
|
||||
viewMode: null
|
||||
nextBeforeTimestamp: null
|
||||
atEnd: false
|
||||
selection: {
|
||||
|
@ -46,16 +68,33 @@ define [
|
|||
toV: null
|
||||
}
|
||||
}
|
||||
diff: null
|
||||
files: []
|
||||
diff: null # When history.viewMode == HistoryViewModes.COMPARE
|
||||
selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
|
||||
}
|
||||
|
||||
restoreFile: (version, pathname) ->
|
||||
url = "/project/#{@$scope.project_id}/restore_file"
|
||||
|
||||
@ide.$http.post(url, {
|
||||
version, pathname,
|
||||
_csrf: window.csrfToken
|
||||
})
|
||||
|
||||
loadFileTreeForUpdate: (update) ->
|
||||
{fromV, toV} = update
|
||||
url = "/project/#{@$scope.project_id}/filetree/diff"
|
||||
query = [ "from=#{toV}", "to=#{toV}" ]
|
||||
url += "?" + query.join("&")
|
||||
@$scope.history.loadingFileTree = true
|
||||
@$scope.history.selectedFile = null
|
||||
@$scope.history.selection.pathname = null
|
||||
@ide.$http
|
||||
.get(url)
|
||||
.then (response) =>
|
||||
@$scope.history.files = response.data.diff
|
||||
@$scope.history.loadingFileTree = false
|
||||
|
||||
MAX_RECENT_UPDATES_TO_SELECT: 5
|
||||
autoSelectRecentUpdates: () ->
|
||||
return if @$scope.history.updates.length == 0
|
||||
|
@ -70,12 +109,28 @@ define [
|
|||
|
||||
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
||||
|
||||
autoSelectLastUpdate: () ->
|
||||
return if @$scope.history.updates.length == 0
|
||||
@selectUpdate @$scope.history.updates[0]
|
||||
|
||||
selectUpdate: (update) ->
|
||||
selectedUpdateIndex = @$scope.history.updates.indexOf update
|
||||
if selectedUpdateIndex == -1
|
||||
selectedUpdateIndex = 0
|
||||
for update in @$scope.history.updates
|
||||
update.selectedTo = false
|
||||
update.selectedFrom = false
|
||||
@$scope.history.updates[selectedUpdateIndex].selectedTo = true
|
||||
@$scope.history.updates[selectedUpdateIndex].selectedFrom = true
|
||||
@loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex]
|
||||
|
||||
BATCH_SIZE: 10
|
||||
fetchNextBatchOfUpdates: () ->
|
||||
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
||||
if @$scope.history.nextBeforeTimestamp?
|
||||
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
||||
@$scope.history.loading = true
|
||||
@$scope.history.loadingFileTree = true
|
||||
@ide.$http
|
||||
.get(url)
|
||||
.then (response) =>
|
||||
|
@ -86,6 +141,23 @@ define [
|
|||
@$scope.history.atEnd = true
|
||||
@$scope.history.loading = false
|
||||
|
||||
loadFileAtPointInTime: () ->
|
||||
pathname = @$scope.history.selection.pathname
|
||||
toV = @$scope.history.selection.updates[0].toV
|
||||
url = "/project/#{@$scope.project_id}/diff"
|
||||
query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"]
|
||||
url += "?" + query.join("&")
|
||||
@$scope.history.selectedFile =
|
||||
loading: true
|
||||
@ide.$http
|
||||
.get(url)
|
||||
.then (response) =>
|
||||
{text, binary} = @_parseDiff(response.data.diff)
|
||||
@$scope.history.selectedFile.binary = binary
|
||||
@$scope.history.selectedFile.text = text
|
||||
@$scope.history.selectedFile.loading = false
|
||||
.catch () ->
|
||||
|
||||
reloadDiff: () ->
|
||||
diff = @$scope.history.diff
|
||||
{updates} = @$scope.history.selection
|
||||
|
@ -200,7 +272,11 @@ define [
|
|||
@$scope.history.updates =
|
||||
@$scope.history.updates.concat(updates)
|
||||
|
||||
@autoSelectRecentUpdates() if firstLoad
|
||||
if firstLoad
|
||||
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
||||
@autoSelectRecentUpdates()
|
||||
else
|
||||
@autoSelectLastUpdate()
|
||||
|
||||
_perDocSummaryOfUpdates: (updates) ->
|
||||
# Track current_pathname -> original_pathname
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
historyEntriesListController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
return
|
||||
|
||||
App.component "historyEntriesList", {
|
||||
bindings:
|
||||
entries: "<"
|
||||
loadEntries: "&"
|
||||
loadDisabled: "<"
|
||||
loadInitialize: "<"
|
||||
isLoading: "<"
|
||||
currentUser: "<"
|
||||
onEntrySelect: "&"
|
||||
controller: historyEntriesListController
|
||||
templateUrl: "historyEntriesListTpl"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
define [
|
||||
"base"
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
historyEntryController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
ctrl.displayName = displayNameForUser
|
||||
ctrl.getProjectOpDoc = (projectOp) ->
|
||||
if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }"
|
||||
else if projectOp.add? then "#{ projectOp.add.pathname}"
|
||||
else if projectOp.remove? then "#{ projectOp.remove.pathname}"
|
||||
ctrl.getUserCSSStyle = (user) ->
|
||||
hue = user?.hue or 100
|
||||
if ctrl.entry.inSelection
|
||||
color : "#FFF"
|
||||
else
|
||||
color: "hsl(#{ hue }, 70%, 50%)"
|
||||
return
|
||||
|
||||
App.component "historyEntry", {
|
||||
bindings:
|
||||
entry: "<"
|
||||
currentUser: "<"
|
||||
onSelect: "&"
|
||||
controller: historyEntryController
|
||||
templateUrl: "historyEntryTpl"
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
define [
|
||||
"base"
|
||||
"ide/file-tree/util/iconTypeFromName"
|
||||
], (App, iconTypeFromName) ->
|
||||
# TODO Add arrows in folders
|
||||
historyFileEntityController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
_handleFolderClick = () ->
|
||||
ctrl.isOpen = !ctrl.isOpen
|
||||
ctrl.iconClass = _getFolderIcon()
|
||||
_handleFileClick = () ->
|
||||
ctrl.historyFileTreeController.handleEntityClick ctrl.fileEntity
|
||||
_getFolderIcon = () ->
|
||||
if ctrl.isOpen then "fa-folder-open" else "fa-folder"
|
||||
ctrl.$onInit = () ->
|
||||
if ctrl.fileEntity.type == "folder"
|
||||
ctrl.isOpen = true
|
||||
ctrl.iconClass = _getFolderIcon()
|
||||
ctrl.handleClick = _handleFolderClick
|
||||
else
|
||||
ctrl.iconClass = "fa-#{ iconTypeFromName(ctrl.fileEntity.name) }"
|
||||
ctrl.handleClick = _handleFileClick
|
||||
$scope.$watch (() -> ctrl.historyFileTreeController.selectedPathname), (newPathname) ->
|
||||
ctrl.isSelected = ctrl.fileEntity.pathname == newPathname
|
||||
return
|
||||
|
||||
App.component "historyFileEntity", {
|
||||
require:
|
||||
historyFileTreeController: "^historyFileTree"
|
||||
bindings:
|
||||
fileEntity: "<"
|
||||
controller: historyFileEntityController
|
||||
templateUrl: "historyFileEntityTpl"
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
historyFileTreeController = ($scope, $element, $attrs) ->
|
||||
ctrl = @
|
||||
ctrl.handleEntityClick = (file) ->
|
||||
ctrl.onSelectedFileChange file: file
|
||||
return
|
||||
|
||||
App.component "historyFileTree", {
|
||||
bindings:
|
||||
fileTree: "<"
|
||||
selectedPathname: "<"
|
||||
onSelectedFileChange: "&"
|
||||
isLoading: "<"
|
||||
controller: historyFileTreeController
|
||||
templateUrl: "historyFileTreeTpl"
|
||||
}
|
|
@ -5,7 +5,7 @@ define [
|
|||
|
||||
App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
|
||||
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking, waitFor) ->
|
||||
$scope.restoreState =
|
||||
inflight: false
|
||||
error: false
|
||||
|
@ -24,17 +24,16 @@ define [
|
|||
$scope.restoreState.inflight = false
|
||||
|
||||
openEntity = (data) ->
|
||||
iterations = 0
|
||||
{id, type} = data
|
||||
do tryOpen = () ->
|
||||
if iterations > 5
|
||||
return
|
||||
iterations += 1
|
||||
entity = ide.fileTreeManager.findEntityById(id)
|
||||
if entity? and type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
else if entity? and type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
else
|
||||
setTimeout(tryOpen, 500)
|
||||
|
||||
waitFor(
|
||||
() ->
|
||||
ide.fileTreeManager.findEntityById(id)
|
||||
3000
|
||||
)
|
||||
.then (entity) ->
|
||||
if type == 'doc'
|
||||
ide.editorManager.openDoc(entity)
|
||||
else if type == 'file'
|
||||
ide.binaryFilesManager.openFile(entity)
|
||||
.catch (err) ->
|
||||
console.warn(err)
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
|
||||
App.controller "HistoryV2FileTreeController", ["$scope", "ide", "_", ($scope, ide, _) ->
|
||||
_previouslySelectedPathname = null
|
||||
$scope.currentFileTree = []
|
||||
|
||||
_pathnameExistsInFiles = (pathname, files) ->
|
||||
_.any files, (file) -> file.pathname == pathname
|
||||
|
||||
_getSelectedDefaultPathname = (files) ->
|
||||
selectedPathname = null
|
||||
if _previouslySelectedPathname? and _pathnameExistsInFiles _previouslySelectedPathname, files
|
||||
selectedPathname = _previouslySelectedPathname
|
||||
else
|
||||
mainFile = _.find files, (file) -> /main\.tex$/.test file.pathname
|
||||
if mainFile?
|
||||
selectedPathname = _previouslySelectedPathname = mainFile.pathname
|
||||
else
|
||||
selectedPathname = _previouslySelectedPathname = files[0].pathname
|
||||
return selectedPathname
|
||||
|
||||
$scope.handleFileSelection = (file) ->
|
||||
$scope.history.selection.pathname = _previouslySelectedPathname = file.pathname
|
||||
|
||||
$scope.$watch 'history.files', (files) ->
|
||||
if files? and files.length > 0
|
||||
$scope.currentFileTree = _.reduce files, _reducePathsToTree, []
|
||||
$scope.history.selection.pathname = _getSelectedDefaultPathname(files)
|
||||
|
||||
_reducePathsToTree = (currentFileTree, fileObject) ->
|
||||
filePathParts = fileObject.pathname.split "/"
|
||||
currentFileTreeLocation = currentFileTree
|
||||
for pathPart, index in filePathParts
|
||||
isFile = index == filePathParts.length - 1
|
||||
if isFile
|
||||
fileTreeEntity =
|
||||
name: pathPart
|
||||
pathname: fileObject.pathname
|
||||
type: "file"
|
||||
operation: fileObject.operation || "edited"
|
||||
currentFileTreeLocation.push fileTreeEntity
|
||||
else
|
||||
fileTreeEntity = _.find currentFileTreeLocation, (entity) => entity.name == pathPart
|
||||
if !fileTreeEntity?
|
||||
fileTreeEntity =
|
||||
name: pathPart
|
||||
type: "folder"
|
||||
children: []
|
||||
currentFileTreeLocation.push fileTreeEntity
|
||||
currentFileTreeLocation = fileTreeEntity.children
|
||||
return currentFileTree
|
||||
]
|
|
@ -0,0 +1,76 @@
|
|||
define [
|
||||
"base",
|
||||
"ide/history/util/displayNameForUser"
|
||||
], (App, displayNameForUser) ->
|
||||
|
||||
App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) ->
|
||||
$scope.hoveringOverListSelectors = false
|
||||
|
||||
$scope.loadMore = () =>
|
||||
ide.historyManager.fetchNextBatchOfUpdates()
|
||||
|
||||
$scope.handleEntrySelect = (entry) ->
|
||||
# $scope.$applyAsync () ->
|
||||
ide.historyManager.selectUpdate(entry)
|
||||
$scope.recalculateSelectedUpdates()
|
||||
|
||||
$scope.recalculateSelectedUpdates = () ->
|
||||
beforeSelection = true
|
||||
afterSelection = false
|
||||
$scope.history.selection.updates = []
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
inSelection = true
|
||||
beforeSelection = false
|
||||
|
||||
update.beforeSelection = beforeSelection
|
||||
update.inSelection = inSelection
|
||||
update.afterSelection = afterSelection
|
||||
|
||||
if inSelection
|
||||
$scope.history.selection.updates.push update
|
||||
|
||||
if update.selectedFrom
|
||||
inSelection = false
|
||||
afterSelection = true
|
||||
|
||||
$scope.recalculateHoveredUpdates = () ->
|
||||
hoverSelectedFrom = false
|
||||
hoverSelectedTo = false
|
||||
for update in $scope.history.updates
|
||||
# Figure out whether the to or from selector is hovered over
|
||||
if update.hoverSelectedFrom
|
||||
hoverSelectedFrom = true
|
||||
if update.hoverSelectedTo
|
||||
hoverSelectedTo = true
|
||||
|
||||
if hoverSelectedFrom
|
||||
# We want to 'hover select' everything between hoverSelectedFrom and selectedTo
|
||||
inHoverSelection = false
|
||||
for update in $scope.history.updates
|
||||
if update.selectedTo
|
||||
update.hoverSelectedTo = true
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
if update.hoverSelectedFrom
|
||||
inHoverSelection = false
|
||||
if hoverSelectedTo
|
||||
# We want to 'hover select' everything between hoverSelectedTo and selectedFrom
|
||||
inHoverSelection = false
|
||||
for update in $scope.history.updates
|
||||
if update.hoverSelectedTo
|
||||
inHoverSelection = true
|
||||
update.inHoverSelection = inHoverSelection
|
||||
if update.selectedFrom
|
||||
update.hoverSelectedFrom = true
|
||||
inHoverSelection = false
|
||||
|
||||
$scope.resetHoverState = () ->
|
||||
for update in $scope.history.updates
|
||||
delete update.hoverSelectedFrom
|
||||
delete update.hoverSelectedTo
|
||||
delete update.inHoverSelection
|
||||
|
||||
$scope.$watch "history.updates.length", () ->
|
||||
$scope.recalculateSelectedUpdates()
|
||||
]
|
|
@ -0,0 +1,4 @@
|
|||
define [], () ->
|
||||
HistoryViewModes =
|
||||
POINT_IN_TIME : 'point_in_time'
|
||||
COMPARE : 'compare'
|
|
@ -0,0 +1,16 @@
|
|||
define [
|
||||
"base"
|
||||
"ace/ace"
|
||||
], (App) ->
|
||||
App.controller "TestControlsController", ($scope) ->
|
||||
|
||||
$scope.openProjectLinkedFileModal = () ->
|
||||
window.openProjectLinkedFileModal()
|
||||
|
||||
$scope.openLinkedFileModal = () ->
|
||||
window.openLinkedFileModal()
|
||||
|
||||
$scope.richText = () ->
|
||||
current = window.location.toString()
|
||||
target = "#{current}#{if window.location.search then '&' else '?'}rt=true"
|
||||
window.location.href = target
|
|
@ -0,0 +1,3 @@
|
|||
define [
|
||||
"ide/test-controls/controllers/TestControlsController"
|
||||
], () ->
|
|
@ -1,79 +1,7 @@
|
|||
define [
|
||||
"base"
|
||||
"libs/platform"
|
||||
"services/algolia-search"
|
||||
], (App, platform) ->
|
||||
App.controller 'ContactModal', ($scope, $modal) ->
|
||||
$scope.contactUsModal = () ->
|
||||
modalInstance = $modal.open(
|
||||
templateUrl: "supportModalTemplate"
|
||||
controller: "SupportModalController"
|
||||
)
|
||||
|
||||
App.controller 'SupportModalController', ($scope, $modalInstance, algoliaSearch, event_tracking) ->
|
||||
$scope.form = {}
|
||||
$scope.sent = false
|
||||
$scope.sending = false
|
||||
$scope.suggestions = [];
|
||||
|
||||
_handleSearchResults = (success, results) ->
|
||||
suggestions = for hit in results.hits
|
||||
page_underscored = hit.pageName.replace(/\s/g,'_')
|
||||
|
||||
suggestion =
|
||||
url :"/learn/kb/#{page_underscored}"
|
||||
name : hit._highlightResult.pageName.value
|
||||
|
||||
event_tracking.sendMB "contact-form-suggestions-shown" if results.hits.length
|
||||
|
||||
$scope.$applyAsync () ->
|
||||
$scope.suggestions = suggestions
|
||||
|
||||
$scope.contactUs = ->
|
||||
if !$scope.form.email? or $scope.form.email == ""
|
||||
console.log "email not set"
|
||||
return
|
||||
$scope.sending = true
|
||||
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
|
||||
message = $scope.form.message
|
||||
if $scope.form.project_url?
|
||||
message = "#{message}\n\n project_url = #{$scope.form.project_url}"
|
||||
params =
|
||||
email: $scope.form.email
|
||||
message: message or ""
|
||||
subject: $scope.form.subject + " - [#{ticketNumber}]"
|
||||
labels: "support"
|
||||
about: "<div>browser: #{platform?.name} #{platform?.version}</div>
|
||||
<div>os: #{platform?.os?.family} #{platform?.os?.version}</div>"
|
||||
|
||||
Groove.createTicket params, (response)->
|
||||
$scope.sending = false
|
||||
if response.responseText == "" # Blocked request or similar
|
||||
$scope.error = true
|
||||
else
|
||||
data = JSON.parse(response.responseText)
|
||||
if data.errors?
|
||||
$scope.error = true
|
||||
else
|
||||
$scope.sent = true
|
||||
$scope.$apply()
|
||||
|
||||
$scope.$watch "form.subject", (newVal, oldVal) ->
|
||||
if newVal and newVal != oldVal and newVal.length > 3
|
||||
algoliaSearch.searchKB newVal, _handleSearchResults, {
|
||||
hitsPerPage: 3
|
||||
typoTolerance: 'strict'
|
||||
}
|
||||
else
|
||||
$scope.suggestions = [];
|
||||
|
||||
$scope.clickSuggestionLink = (url) ->
|
||||
event_tracking.sendMB "contact-form-suggestions-clicked", { url }
|
||||
|
||||
$scope.close = () ->
|
||||
$modalInstance.close()
|
||||
|
||||
|
||||
App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
|
||||
|
||||
$scope.form = {}
|
||||
|
|
|
@ -50,6 +50,10 @@ define [
|
|||
send: (category, action, label, value)->
|
||||
ga('send', 'event', category, action, label, value)
|
||||
|
||||
sendGAOnce: (category, action, label, value) ->
|
||||
if ! _eventInCache(action)
|
||||
_addEventToCache(action)
|
||||
@send category, action, label, value
|
||||
|
||||
editingSessionHeartbeat: () ->
|
||||
return unless nextHeartbeat <= new Date()
|
||||
|
@ -86,6 +90,9 @@ define [
|
|||
if ! _eventInCache(key)
|
||||
_addEventToCache(key)
|
||||
@sendMB key, segmentation
|
||||
|
||||
eventInCache: (key) ->
|
||||
_eventInCache(key)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ define [
|
|||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
$scope.plans = MultiCurrencyPricing.plans
|
||||
$scope.planCode = window.plan_code
|
||||
|
||||
$scope.switchToStudent = ()->
|
||||
currentPlanCode = window.plan_code
|
||||
|
@ -234,3 +235,6 @@ define [
|
|||
{code:'WK',name:'Wake Island'},{code:'WF',name:'Wallis and Futuna'},{code:'EH',name:'Western Sahara'},{code:'YE',name:'Yemen'},
|
||||
{code:'ZM',name:'Zambia'},{code:'AX',name:'Åland Islandscode:'}
|
||||
]
|
||||
|
||||
sixpack.participate 'plans', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
|
@ -3,7 +3,6 @@ define [
|
|||
"libs/recurly-4.8.5"
|
||||
], (App, recurly) ->
|
||||
|
||||
|
||||
App.factory "MultiCurrencyPricing", () ->
|
||||
|
||||
currencyCode = window.recomendedCurrency
|
||||
|
@ -146,17 +145,16 @@ define [
|
|||
}
|
||||
|
||||
|
||||
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack) ->
|
||||
App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack, $filter) ->
|
||||
|
||||
$scope.showPlans = false
|
||||
|
||||
$scope.plansVariant = 'default'
|
||||
$scope.shouldABTestPlans = window.shouldABTestPlans
|
||||
|
||||
if $scope.shouldABTestPlans
|
||||
$scope.showPlans = true
|
||||
else
|
||||
$scope.showPlans = true
|
||||
sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
|
||||
$scope.plansVariant = chosenVariation
|
||||
|
||||
$scope.showPlans = true
|
||||
|
||||
$scope.plans = MultiCurrencyPricing.plans
|
||||
|
||||
|
@ -169,44 +167,57 @@ define [
|
|||
$scope.ui =
|
||||
view: "monthly"
|
||||
|
||||
$scope.changeCurreny = (newCurrency)->
|
||||
$scope.changeCurreny = (e, newCurrency)->
|
||||
e.preventDefault()
|
||||
$scope.currencyCode = newCurrency
|
||||
|
||||
# because ternary logic in angular bindings is hard
|
||||
$scope.getCollaboratorPlanCode = () ->
|
||||
view = $scope.ui.view
|
||||
variant = $scope.plansVariant
|
||||
if view == "annual"
|
||||
if variant == "default"
|
||||
return "collaborator-annual"
|
||||
else
|
||||
return "collaborator-annual_#{variant}"
|
||||
return "collaborator-annual"
|
||||
else
|
||||
if variant == "default"
|
||||
return "collaborator#{$scope.planQueryString}"
|
||||
else
|
||||
return "collaborator_#{variant}"
|
||||
return "collaborator#{$scope.planQueryString}"
|
||||
|
||||
$scope.signUpNowClicked = (plan, annual)->
|
||||
event_tracking.sendMB 'plans-page-start-trial', {plan}
|
||||
$scope.signUpNowClicked = (plan, location)->
|
||||
if $scope.ui.view == "annual"
|
||||
plan = "#{plan}_annual"
|
||||
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
|
||||
plan = eventLabel(plan, location)
|
||||
event_tracking.sendMB 'plans-page-start-trial', {plan}
|
||||
event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
|
||||
if $scope.shouldABTestPlans
|
||||
sixpack.convert 'plans-details'
|
||||
|
||||
$scope.switchToMonthly = ->
|
||||
$scope.ui.view = "monthly"
|
||||
event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
|
||||
$scope.switchToMonthly = (e, location) ->
|
||||
uiView = 'monthly'
|
||||
switchEvent(e, uiView + '-prices', location)
|
||||
$scope.ui.view = uiView
|
||||
|
||||
$scope.switchToStudent = ->
|
||||
$scope.ui.view = "student"
|
||||
event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
|
||||
$scope.switchToStudent = (e, location) ->
|
||||
uiView = 'student'
|
||||
switchEvent(e, uiView + '-prices', location)
|
||||
$scope.ui.view = uiView
|
||||
|
||||
$scope.switchToAnnual = ->
|
||||
$scope.ui.view = "annual"
|
||||
event_tracking.send 'subscription-funnel', 'plans-page', 'annual-prices'
|
||||
$scope.switchToAnnual = (e, location) ->
|
||||
uiView = 'annual'
|
||||
switchEvent(e, uiView + '-prices', location)
|
||||
$scope.ui.view = uiView
|
||||
|
||||
$scope.openGroupPlanModal = () ->
|
||||
$modal.open {
|
||||
templateUrl: "groupPlanModalTemplate"
|
||||
}
|
||||
event_tracking.send 'subscription-funnel', 'plans-page', 'group-inquiry-potential'
|
||||
|
||||
eventLabel = (label, location) ->
|
||||
if location && $scope.plansVariant != 'default'
|
||||
label = label + '-' + location
|
||||
if $scope.plansVariant != 'default'
|
||||
label += '-exp-' + $scope.plansVariant
|
||||
label
|
||||
|
||||
switchEvent = (e, label, location) ->
|
||||
e.preventDefault()
|
||||
gaLabel = eventLabel(label, location)
|
||||
event_tracking.send 'subscription-funnel', 'plans-page', gaLabel
|
||||
|
||||
|
|
|
@ -320,6 +320,9 @@ define [
|
|||
name: cloneName
|
||||
id: data.project_id
|
||||
accessLevel: "owner"
|
||||
owner: {
|
||||
_id: user_id
|
||||
}
|
||||
# TODO: Check access level if correct after adding it in
|
||||
# to the rest of the app
|
||||
}
|
||||
|
@ -350,14 +353,15 @@ define [
|
|||
$scope.archiveOrLeaveSelectedProjects()
|
||||
|
||||
$scope.archiveOrLeaveSelectedProjects = () ->
|
||||
selected_projects = $scope.getSelectedProjects()
|
||||
selected_project_ids = $scope.getSelectedProjectIds()
|
||||
$scope.archiveOrLeaveProjects($scope.getSelectedProjects())
|
||||
|
||||
$scope.archiveOrLeaveProjects = (projects) ->
|
||||
projectIds = projects.map (p) -> p.id
|
||||
# Remove project from any tags
|
||||
for tag in $scope.tags
|
||||
$scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
|
||||
$scope._removeProjectIdsFromTagArray(tag, projectIds)
|
||||
|
||||
for project in selected_projects
|
||||
for project in projects
|
||||
project.tags = []
|
||||
if project.accessLevel == "owner"
|
||||
project.archived = true
|
||||
|
@ -414,16 +418,17 @@ define [
|
|||
$scope.updateVisibleProjects()
|
||||
|
||||
$scope.restoreSelectedProjects = () ->
|
||||
selected_projects = $scope.getSelectedProjects()
|
||||
selected_project_ids = $scope.getSelectedProjectIds()
|
||||
$scope.restoreProjects($scope.getSelectedProjects())
|
||||
|
||||
for project in selected_projects
|
||||
$scope.restoreProjects = (projects) ->
|
||||
projectIds = projects.map (p) -> p.id
|
||||
for project in projects
|
||||
project.archived = false
|
||||
|
||||
for project_id in selected_project_ids
|
||||
for projectId in projectIds
|
||||
queuedHttp {
|
||||
method: "POST"
|
||||
url: "/project/#{project_id}/restore"
|
||||
url: "/project/#{projectId}/restore"
|
||||
headers:
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
}
|
||||
|
@ -437,13 +442,14 @@ define [
|
|||
)
|
||||
|
||||
$scope.downloadSelectedProjects = () ->
|
||||
selected_project_ids = $scope.getSelectedProjectIds()
|
||||
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
|
||||
if selected_project_ids.length > 1
|
||||
path = "/project/download/zip?project_ids=#{selected_project_ids.join(',')}"
|
||||
else
|
||||
path = "/project/#{selected_project_ids[0]}/download/zip"
|
||||
$scope.downloadProjectsById($scope.getSelectedProjectIds())
|
||||
|
||||
$scope.downloadProjectsById = (projectIds) ->
|
||||
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
|
||||
if projectIds.length > 1
|
||||
path = "/project/download/zip?project_ids=#{projectIds.join(',')}"
|
||||
else
|
||||
path = "/project/#{projectIds[0]}/download/zip"
|
||||
window.location = path
|
||||
|
||||
$scope.openV1ImportModal = (project) ->
|
||||
|
@ -487,6 +493,25 @@ define [
|
|||
else
|
||||
return "None"
|
||||
|
||||
$scope.isOwner = () ->
|
||||
window.user_id == $scope.project.owner._id
|
||||
|
||||
$scope.$watch "project.selected", (value) ->
|
||||
if value?
|
||||
$scope.updateSelectedProjects()
|
||||
|
||||
$scope.clone = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.cloneProject($scope.project, "#{$scope.project.name} (Copy)")
|
||||
|
||||
$scope.download = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.downloadProjectsById([$scope.project.id])
|
||||
|
||||
$scope.archiveOrLeave = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.archiveOrLeaveProjects([$scope.project])
|
||||
|
||||
$scope.restore = (e) ->
|
||||
e.stopPropagation()
|
||||
$scope.restoreProjects([$scope.project])
|
||||
|
|
|
@ -8,7 +8,7 @@ define [
|
|||
kbIdx = client.initIndex(window.sharelatex.algolia?.indexes?.kb)
|
||||
|
||||
service =
|
||||
searchWiki: wikiIdx.search.bind(wikiIdx)
|
||||
searchKB: kbIdx.search.bind(kbIdx)
|
||||
searchWiki: if wikiIdx then wikiIdx.search.bind(wikiIdx) else null
|
||||
searchKB: if kbIdx then kbIdx.search.bind(kbIdx) else null
|
||||
|
||||
return service
|
20
services/web/public/coffee/services/wait-for.coffee
Normal file
20
services/web/public/coffee/services/wait-for.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory "waitFor", ($q) ->
|
||||
waitFor = (testFunction, timeout, pollInterval=500) ->
|
||||
iterationLimit = Math.floor(timeout / pollInterval)
|
||||
iterations = 0
|
||||
$q(
|
||||
(resolve, reject) ->
|
||||
do tryIteration = () ->
|
||||
if iterations > iterationLimit
|
||||
return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}"))
|
||||
iterations += 1
|
||||
result = testFunction()
|
||||
if result?
|
||||
resolve(result)
|
||||
else
|
||||
setTimeout(tryIteration, pollInterval)
|
||||
)
|
||||
return waitFor
|
BIN
services/web/public/img/advocates/erdogmus.jpg
Normal file
BIN
services/web/public/img/advocates/erdogmus.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
services/web/public/img/advocates/henderson.jpg
Normal file
BIN
services/web/public/img/advocates/henderson.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -80,6 +80,7 @@
|
|||
@import "app/review-features-page.less";
|
||||
@import "app/error-pages.less";
|
||||
@import "app/v1-badge.less";
|
||||
@import "app/editor/history-v2.less";
|
||||
@import "app/metrics.less";
|
||||
|
||||
// Vendor CSS
|
||||
|
|
|
@ -74,9 +74,13 @@
|
|||
|
||||
#ide-body {
|
||||
.full-size;
|
||||
top: 40px;
|
||||
top: @ide-body-top-offset;
|
||||
&.ide-history-open {
|
||||
top: @ide-body-top-offset + @editor-toolbar-height;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#editor, #editor-rich-text {
|
||||
.full-size;
|
||||
}
|
||||
|
@ -88,6 +92,7 @@
|
|||
.toolbar-editor {
|
||||
height: @editor-toolbar-height;
|
||||
background-color: @editor-toolbar-bg;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
|
|
498
services/web/public/stylesheets/app/editor/history-v2.less
Normal file
498
services/web/public/stylesheets/app/editor/history-v2.less
Normal file
|
@ -0,0 +1,498 @@
|
|||
.history-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: @ide-body-top-offset;
|
||||
height: @editor-toolbar-height;
|
||||
line-height: 1;
|
||||
font-size: @font-size-small;
|
||||
background-color: @history-toolbar-bg-color;
|
||||
z-index: 1;
|
||||
color: @history-toolbar-color;
|
||||
padding-left: (@line-height-computed / 2);
|
||||
}
|
||||
.history-toolbar when (@is-overleaf = false) {
|
||||
border-bottom: @toolbar-border-bottom;
|
||||
}
|
||||
.history-toolbar-time {
|
||||
font-weight: bold;
|
||||
}
|
||||
.history-toolbar-btn {
|
||||
.btn;
|
||||
.btn-info;
|
||||
.btn-xs;
|
||||
padding-left: @padding-small-horizontal;
|
||||
padding-right: @padding-small-horizontal;
|
||||
margin-left: (@line-height-computed / 2);
|
||||
}
|
||||
|
||||
.history-entries {
|
||||
font-size: @history-base-font-size;
|
||||
color: @history-base-color;
|
||||
height: 100%;
|
||||
background-color: @history-base-bg;
|
||||
}
|
||||
|
||||
.history-entry-day {
|
||||
display: block;
|
||||
background-color: @history-entry-day-bg;
|
||||
color: #FFF;
|
||||
padding: 5px 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.history-entry-details {
|
||||
background-color: #FFF;
|
||||
margin-bottom: 2px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.history-entry-selected & {
|
||||
background-color: @history-entry-selected-bg;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
.history-entry-changes {
|
||||
.list-unstyled;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.history-entry-change {
|
||||
|
||||
}
|
||||
.history-entry-change-action {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.history-entry-change-doc {
|
||||
color: @history-highlight-color;
|
||||
font-weight: bold;
|
||||
word-break: break-all;
|
||||
.history-entry-selected & {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
.history-entry-metadata {
|
||||
|
||||
}
|
||||
.history-entry-metadata-time {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-entry-metadata-users {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
}
|
||||
.history-entry-metadata-user {
|
||||
display: inline;
|
||||
&::after {
|
||||
content: ', ';
|
||||
}
|
||||
&:last-of-type::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.history-file-tree-inner {
|
||||
.full-size;
|
||||
overflow-y: auto;
|
||||
background-color: @file-tree-bg;
|
||||
|
||||
.loading {
|
||||
color: #FFF;
|
||||
font-size: @history-base-font-size;
|
||||
text-align: center;
|
||||
font-family: @font-family-serif;
|
||||
}
|
||||
}
|
||||
|
||||
.history-file-tree-inner when (@is-overleaf = false) {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.history-file-entity-wrapper {
|
||||
color: #FFF;
|
||||
margin-left: (@line-height-computed / 2);
|
||||
}
|
||||
.history-file-entity-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: @file-tree-item-color;
|
||||
line-height: @file-tree-line-height;
|
||||
&:hover {
|
||||
background-color: @file-tree-item-hover-bg;
|
||||
color: @file-tree-item-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:focus {
|
||||
color: @file-tree-item-color;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
&:hover when (@is-overleaf = true) {
|
||||
.fake-full-width-bg(@file-tree-item-hover-bg);
|
||||
}
|
||||
}
|
||||
.history-file-entity-link-selected {
|
||||
background-color: @file-tree-item-selected-bg;
|
||||
font-weight: bold;
|
||||
padding-right: 32px;
|
||||
color: #FFF;
|
||||
.fake-full-width-bg(@file-tree-item-selected-bg);
|
||||
&:hover {
|
||||
background-color: @file-tree-item-hover-bg;
|
||||
}
|
||||
}
|
||||
.history-file-entity-icon {
|
||||
color: @file-tree-item-icon-color;
|
||||
font-size: 14px;
|
||||
margin-right: .5em;
|
||||
.history-file-entity-link-selected & {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
.history-file-entity-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.history-file-entity-link-selected when (@is-overleaf = false) {
|
||||
color: @brand-primary;
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @brand-primary;
|
||||
}
|
||||
.history-file-entity-icon {
|
||||
color: @brand-primary;
|
||||
}
|
||||
}
|
||||
// @changesListWidth: 250px;
|
||||
// @changesListPadding: @line-height-computed / 2;
|
||||
|
||||
// @selector-padding-vertical: 10px;
|
||||
// @selector-padding-horizontal: @line-height-computed / 2;
|
||||
// @day-header-height: 24px;
|
||||
|
||||
// @range-bar-color: @link-color;
|
||||
// @range-bar-selected-offset: 14px;
|
||||
|
||||
// #history {
|
||||
// .upgrade-prompt {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// z-index: 100;
|
||||
// background-color: rgba(128,128,128,0.4);
|
||||
// .message {
|
||||
// margin: auto;
|
||||
// margin-top: 100px;
|
||||
// padding: (@line-height-computed / 2) @line-height-computed;
|
||||
// width: 400px;
|
||||
// background-color: white;
|
||||
// border-radius: 8px;
|
||||
// }
|
||||
// .message-wider {
|
||||
// width: 650px;
|
||||
// margin-top: 60px;
|
||||
// padding: 0;
|
||||
// }
|
||||
|
||||
// .message-header {
|
||||
// .modal-header;
|
||||
// }
|
||||
|
||||
// .message-body {
|
||||
// .modal-body;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .diff-panel {
|
||||
// .full-size;
|
||||
// margin-right: @changesListWidth;
|
||||
// }
|
||||
|
||||
// .diff {
|
||||
// .full-size;
|
||||
// .toolbar {
|
||||
// padding: 3px;
|
||||
// .name {
|
||||
// float: left;
|
||||
// padding: 3px @line-height-computed / 4;
|
||||
// display: inline-block;
|
||||
// }
|
||||
// }
|
||||
// .diff-editor {
|
||||
// .full-size;
|
||||
// top: 40px;
|
||||
// }
|
||||
// .hide-ace-cursor {
|
||||
// .ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// .diff-deleted {
|
||||
// padding: @line-height-computed;
|
||||
// }
|
||||
// .deleted-warning {
|
||||
// background-color: @brand-danger;
|
||||
// color: white;
|
||||
// padding: @line-height-computed / 2;
|
||||
// margin-right: @line-height-computed / 4;
|
||||
// }
|
||||
// &-binary {
|
||||
// .alert {
|
||||
// margin: @line-height-computed / 2;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// aside.change-list {
|
||||
// border-left: 1px solid @editor-border-color;
|
||||
// height: 100%;
|
||||
// width: @changesListWidth;
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
|
||||
// .loading {
|
||||
// text-align: center;
|
||||
// font-family: @font-family-serif;
|
||||
// }
|
||||
|
||||
// ul {
|
||||
// li.change {
|
||||
// position: relative;
|
||||
// user-select: none;
|
||||
// -ms-user-select: none;
|
||||
// -moz-user-select: none;
|
||||
// -webkit-user-select: none;
|
||||
|
||||
// .day {
|
||||
// background-color: #fafafa;
|
||||
// border-bottom: 1px solid @editor-border-color;
|
||||
// padding: 4px;
|
||||
// font-weight: bold;
|
||||
// text-align: center;
|
||||
// height: @day-header-height;
|
||||
// font-size: 14px;
|
||||
// line-height: 1;
|
||||
// }
|
||||
// .selectors {
|
||||
// input {
|
||||
// margin: 0;
|
||||
// }
|
||||
// position: absolute;
|
||||
// left: @selector-padding-horizontal;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// width: 24px;
|
||||
// .selector-from {
|
||||
// position: absolute;
|
||||
// bottom: @selector-padding-vertical;
|
||||
// left: 0;
|
||||
// opacity: 0.8;
|
||||
// }
|
||||
// .selector-to {
|
||||
// position: absolute;
|
||||
// top: @selector-padding-vertical;
|
||||
// left: 0;
|
||||
// opacity: 0.8;
|
||||
// }
|
||||
// .range {
|
||||
// position: absolute;
|
||||
// left: 5px;
|
||||
// width: 4px;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// }
|
||||
// }
|
||||
// .description {
|
||||
// padding: (@line-height-computed / 4);
|
||||
// padding-left: 38px;
|
||||
// min-height: 38px;
|
||||
// border-bottom: 1px solid @editor-border-color;
|
||||
// cursor: pointer;
|
||||
// &:hover {
|
||||
// background-color: @gray-lightest;
|
||||
// }
|
||||
// }
|
||||
// .users {
|
||||
// .user {
|
||||
// font-size: 0.8rem;
|
||||
// color: @gray;
|
||||
// text-transform: capitalize;
|
||||
// position: relative;
|
||||
// padding-left: 16px;
|
||||
// .color-square {
|
||||
// height: 12px;
|
||||
// width: 12px;
|
||||
// border-radius: 3px;
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// bottom: 3px;
|
||||
// }
|
||||
// .name {
|
||||
// width: 94%;
|
||||
// white-space: nowrap;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .time {
|
||||
// float: right;
|
||||
// color: @gray;
|
||||
// display: inline-block;
|
||||
// padding-right: (@line-height-computed / 2);
|
||||
// font-size: 0.8rem;
|
||||
// line-height: @line-height-computed;
|
||||
// }
|
||||
// .doc {
|
||||
// font-size: 0.9rem;
|
||||
// font-weight: bold;
|
||||
// }
|
||||
// .action {
|
||||
// color: @gray;
|
||||
// text-transform: uppercase;
|
||||
// font-size: 0.7em;
|
||||
// margin-bottom: -2px;
|
||||
// margin-top: 2px;
|
||||
// &-edited {
|
||||
// margin-top: 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.loading-changes, li.empty-message {
|
||||
// padding: 6px;
|
||||
// cursor: default;
|
||||
// &:hover {
|
||||
// background-color: inherit;
|
||||
// }
|
||||
// }
|
||||
// li.selected {
|
||||
// border-left: 4px solid @range-bar-color;
|
||||
// .day {
|
||||
// padding-left: 0;
|
||||
// }
|
||||
// .description {
|
||||
// padding-left: 34px;
|
||||
// }
|
||||
// .selectors {
|
||||
// left: @selector-padding-horizontal - 4px;
|
||||
// .range {
|
||||
// background-color: @range-bar-color;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-to {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.selected-from {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// bottom: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-from {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.first-in-day {
|
||||
// .selectors {
|
||||
// .selector-to {
|
||||
// top: @day-header-height + @selector-padding-vertical;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.first-in-day.selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @day-header-height + @range-bar-selected-offset;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ul.hover-state {
|
||||
// li {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// background-color: transparent;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.hover-selected {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: 0;
|
||||
// background-color: @gray-light;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.hover-selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-to {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.hover-selected-from {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// bottom: @range-bar-selected-offset;
|
||||
// }
|
||||
// .selector-from {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// li.first-in-day.hover-selected-to {
|
||||
// .selectors {
|
||||
// .range {
|
||||
// top: @day-header-height + @range-bar-selected-offset;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// .diff-deleted {
|
||||
// padding-top: 15px;
|
||||
// }
|
||||
|
||||
// .editor-dark {
|
||||
// #history {
|
||||
// aside.change-list {
|
||||
// border-color: @editor-dark-toolbar-border-color;
|
||||
|
||||
// ul li.change {
|
||||
// .day {
|
||||
// background-color: darken(@editor-dark-background-color, 10%);
|
||||
// border-bottom: 1px solid @editor-dark-toolbar-border-color;
|
||||
// }
|
||||
// .description {
|
||||
// border-bottom: 1px solid @editor-dark-toolbar-border-color;
|
||||
// &:hover {
|
||||
// background-color: black;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
|
@ -1,4 +1,4 @@
|
|||
@changesListWidth: 250px;
|
||||
@changesListWidth: 250px;
|
||||
@changesListPadding: @line-height-computed / 2;
|
||||
|
||||
@selector-padding-vertical: 10px;
|
||||
|
@ -40,7 +40,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.diff-panel {
|
||||
.diff-panel,
|
||||
.point-in-time-panel {
|
||||
.full-size;
|
||||
margin-right: @changesListWidth;
|
||||
}
|
||||
|
@ -49,6 +50,7 @@
|
|||
.full-size;
|
||||
.toolbar {
|
||||
padding: 3px;
|
||||
height: 32px;
|
||||
.name {
|
||||
float: left;
|
||||
padding: 3px @line-height-computed / 4;
|
||||
|
@ -57,13 +59,9 @@
|
|||
}
|
||||
.diff-editor {
|
||||
.full-size;
|
||||
top: 40px;
|
||||
}
|
||||
.hide-ace-cursor {
|
||||
.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
|
||||
display: none;
|
||||
}
|
||||
top: 32px;
|
||||
}
|
||||
|
||||
.diff-deleted {
|
||||
padding: @line-height-computed;
|
||||
}
|
||||
|
@ -90,6 +88,7 @@
|
|||
.loading {
|
||||
text-align: center;
|
||||
font-family: @font-family-serif;
|
||||
margin-top: (@line-height-computed / 2);
|
||||
}
|
||||
|
||||
ul {
|
||||
|
@ -305,6 +304,12 @@
|
|||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.hide-ace-cursor {
|
||||
.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-dark {
|
||||
#history {
|
||||
aside.change-list {
|
||||
|
|
|
@ -184,8 +184,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**************************************
|
||||
Toggle Switch
|
||||
***************************************/
|
||||
|
||||
.toggle-wrapper {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
|
@ -241,3 +245,88 @@
|
|||
transform: translate(100%);
|
||||
border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0;
|
||||
}
|
||||
|
||||
/**************************************
|
||||
Formatting buttons
|
||||
***************************************/
|
||||
.formatting-buttons {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.formatting-buttons-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.formatting-btn {
|
||||
color: @formatting-btn-color;
|
||||
background-color: @formatting-btn-bg;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-left: 1px solid @formatting-btn-border;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
color: @formatting-btn-color;
|
||||
}
|
||||
}
|
||||
|
||||
.formatting-btn--icon {
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.formatting-btn--icon:last-of-type {
|
||||
border-right: 1px solid @formatting-btn-border;
|
||||
}
|
||||
|
||||
.formatting-btn--more {
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
|
||||
.caret {
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.formatting-icon {
|
||||
font-style: normal;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.formatting-icon--small {
|
||||
font-size: small;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.formatting-icon--serif {
|
||||
font-family: @font-family-serif;
|
||||
}
|
||||
|
||||
.formatting-more {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.formatting-menu {
|
||||
min-width: auto;
|
||||
max-width: 130px;
|
||||
background-color: @formatting-menu-bg;
|
||||
}
|
||||
|
||||
.formatting-menu-item {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.formatting-menu-item > .formatting-btn {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
// Disable border on left-most icon in menu
|
||||
.formatting-menu-item:nth-of-type(4n + 1) > .formatting-btn {
|
||||
border-left: none;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
padding-bottom: @line-height-computed * 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.circle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
|
@ -63,6 +63,12 @@
|
|||
}
|
||||
|
||||
.card .btn { white-space:normal; }
|
||||
|
||||
.top-switch {
|
||||
.currency-dropdown {
|
||||
margin-right: -15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#changePlanSection {
|
||||
|
@ -127,4 +133,261 @@ input.paymentTypeOption.ng-valid {
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
/**
|
||||
Plans Test
|
||||
*/
|
||||
@best-val-height: 35px;
|
||||
@highlight-border: 3px;
|
||||
@highlight-color: #d3584b;
|
||||
@gray-med: #6d6d6d;
|
||||
@white-med: #fdfdfd;
|
||||
.more-details {
|
||||
.best-value {
|
||||
color: @red;
|
||||
line-height: @line-height-computed;
|
||||
}
|
||||
blockquote {
|
||||
footer{
|
||||
/* accessibility fix */
|
||||
color: @gray-med;
|
||||
}
|
||||
}
|
||||
.btn-header {
|
||||
font-family: @font-family-sans-serif;
|
||||
margin-left: 10px;
|
||||
margin-top: -10px;
|
||||
text-shadow: 0 0 0;
|
||||
}
|
||||
.card-first, .card-last {
|
||||
background: @white-med;
|
||||
}
|
||||
.card-highlighted {
|
||||
border: @highlight-border solid @gray-lighter;
|
||||
padding-top: 10px!important;
|
||||
.best-value {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card-header {
|
||||
padding-bottom: 22px; /* align hr with other plans */
|
||||
}
|
||||
}
|
||||
.card-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.circle {
|
||||
/* accessibility fix */
|
||||
span.small {
|
||||
color: rgba(255, 255, 255, 0.85)
|
||||
}
|
||||
}
|
||||
.circle-img {
|
||||
border-radius: 50%;
|
||||
float: right;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100px;
|
||||
img {
|
||||
display: inline;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.faq:last-child {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.questions-header {
|
||||
color: @red;
|
||||
line-height: 37px;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.tagline {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
/* Media Queries */
|
||||
@media (max-width: @screen-md-min) {
|
||||
.card-highlighted {
|
||||
/*override style in cards.less */
|
||||
margin-top: @line-height-computed!important;
|
||||
}
|
||||
.circle-img {
|
||||
float: left;
|
||||
margin: 0 15px;
|
||||
}
|
||||
}
|
||||
@media (min-width: @screen-md-min) {
|
||||
blockquote {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.faq {
|
||||
.row:nth-child(2) {
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.student-disclaimer {
|
||||
font-size: 14px; /* match .paymentPageFeatures p */
|
||||
color: @gray; /* match .paymentPageFeatures p */
|
||||
margin: 12.5px 0 0 0;
|
||||
}
|
||||
|
||||
/**
|
||||
Plans Table
|
||||
*/
|
||||
.plans-table {
|
||||
border: 1px solid @gray-lighter;
|
||||
background-color: @white-med;
|
||||
margin: @best-val-height 0 15px 0;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
th, td {
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box; /* needed for firefox when there is bg color */
|
||||
border: 1px solid @gray-lighter;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th {
|
||||
border-top: 0;
|
||||
font-family: @headings-font-family;
|
||||
font-size: @font-size-h2;
|
||||
font-weight: @headings-font-weight;
|
||||
line-height: @headings-line-height;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
th:first-child, td:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
th:last-child, td:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
font-weight: bold;
|
||||
padding-left: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr:first-child {
|
||||
th {
|
||||
position: relative;
|
||||
/* keep here position here, otherwise messes up border on safari */
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
td {
|
||||
border-bottom: 0;
|
||||
padding: 18px;
|
||||
}
|
||||
/* highlighted column */
|
||||
td:nth-child(3) {
|
||||
position: relative;
|
||||
/* keep here position here, otherwise messes up border on safari when there is a bg color */
|
||||
&:before {
|
||||
/* needed for safafi */
|
||||
border-top: 1px solid @gray-lighter;
|
||||
content: '';
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
td:first-child {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-check {
|
||||
color: @green;
|
||||
}
|
||||
|
||||
/* accessibility fixes */
|
||||
.small {
|
||||
color: @gray-med;
|
||||
}
|
||||
|
||||
/* highlighted column */
|
||||
td:nth-child(3), th:nth-child(3) {
|
||||
background-color: white;
|
||||
border-left: @highlight-border solid @gray-lighter;
|
||||
border-right: @highlight-border solid @gray-lighter;
|
||||
}
|
||||
.outer {
|
||||
left: -@highlight-border;
|
||||
right: -@highlight-border;
|
||||
position: absolute;
|
||||
|
||||
.outer-content {
|
||||
background: white;
|
||||
border: @highlight-border solid @gray-lighter;
|
||||
border-radius: @border-radius-base;
|
||||
font-size: @font-size-base;
|
||||
font-family: @font-family-sans-serif;
|
||||
font-weight: bold;
|
||||
height: @best-val-height;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
.outer.outer-top {
|
||||
top: -@best-val-height;
|
||||
.outer-content {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
.outer.outer-btm {
|
||||
bottom: -@best-val-height/2;
|
||||
.outer-content {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top: 0;
|
||||
height: @best-val-height/2;
|
||||
}
|
||||
}
|
||||
|
||||
/* highlight rows on hover */
|
||||
tr:hover {
|
||||
td {
|
||||
background-color: @gray-lightest;
|
||||
}
|
||||
}
|
||||
tr:first-child:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
tr:last-child:hover {
|
||||
background-color: transparent;
|
||||
td {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* tooltip */
|
||||
sup {
|
||||
color: @red;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.tooltip.in {
|
||||
min-width: 200px
|
||||
}
|
||||
}
|
||||
|
|
|
@ -369,6 +369,16 @@ ul.project-list {
|
|||
.v1-badge {
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.action-btn-row-header, .action-btn-row {
|
||||
padding-right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0 0.3em;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
i.tablesort {
|
||||
padding-left: 8px;
|
||||
|
|
|
@ -145,10 +145,15 @@ output {
|
|||
opacity: 1; // iOS fix for unreadable disabled content
|
||||
}
|
||||
|
||||
// Reset height for `textarea`s
|
||||
// Reset height for `textarea`s, and smaller border-radius
|
||||
textarea& {
|
||||
height: auto;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
// Smaller border-radius for `select` inputs
|
||||
select& {
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -888,6 +888,7 @@
|
|||
@footer-padding : 2em;
|
||||
|
||||
// Editor header
|
||||
@ide-body-top-offset : 40px;
|
||||
@toolbar-header-bg-color : transparent;
|
||||
@toolbar-header-shadow : 0 0 2px #ccc;
|
||||
@toolbar-btn-color : @link-color;
|
||||
|
@ -940,6 +941,12 @@
|
|||
@toggle-switch-bg : @gray-lightest;
|
||||
@toggle-switch-highlight-color : @brand-primary;
|
||||
|
||||
// Formatting buttons
|
||||
@formatting-btn-color : @btn-default-color;
|
||||
@formatting-btn-bg : @btn-default-bg;
|
||||
@formatting-btn-border : @btn-default-border;
|
||||
@formatting-menu-bg : @btn-default-bg;
|
||||
|
||||
// Chat
|
||||
@chat-bg : transparent;
|
||||
@chat-message-color : @text-color;
|
||||
|
@ -972,4 +979,15 @@
|
|||
// System messages
|
||||
@sys-msg-background : @state-warning-bg;
|
||||
@sys-msg-color : #333;
|
||||
@sys-msg-border : 1px solid @common-border-color;
|
||||
@sys-msg-border : 1px solid @common-border-color;
|
||||
|
||||
// v2 History
|
||||
@history-base-font-size : @font-size-small;
|
||||
@history-base-bg : @gray-lightest;
|
||||
@history-entry-day-bg : @gray;
|
||||
@history-entry-selected-bg : @red;
|
||||
@history-base-color : @gray-light;
|
||||
@history-highlight-color : @gray;
|
||||
@history-toolbar-bg-color : @toolbar-alt-bg-color;
|
||||
@history-toolbar-color : @text-color;
|
||||
|
||||
|
|
|
@ -244,6 +244,12 @@
|
|||
@toggle-switch-radius-left : @btn-border-radius-base 0 0 @btn-border-radius-base;
|
||||
@toggle-switch-radius-right : 0 @btn-border-radius-base @btn-border-radius-base 0;
|
||||
|
||||
// Formatting buttons
|
||||
@formatting-btn-color : #FFF;
|
||||
@formatting-btn-bg : @ol-blue-gray-5;
|
||||
@formatting-btn-border : @ol-blue-gray-4;
|
||||
@formatting-menu-bg : @ol-blue-gray-5;
|
||||
|
||||
// Chat
|
||||
@chat-bg : @ol-blue-gray-5;
|
||||
@chat-message-color : #FFF;
|
||||
|
@ -265,6 +271,17 @@
|
|||
@log-line-no-color : #FFF;
|
||||
@log-hints-color : @ol-blue-gray-4;
|
||||
|
||||
|
||||
// v2 History
|
||||
@history-base-font-size : @font-size-small;
|
||||
@history-base-bg : @ol-blue-gray-1;
|
||||
@history-entry-day-bg : @ol-blue-gray-2;
|
||||
@history-entry-selected-bg : @ol-green;
|
||||
@history-base-color : @ol-blue-gray-2;
|
||||
@history-highlight-color : @ol-type-color;
|
||||
@history-toolbar-bg-color : @editor-toolbar-bg;
|
||||
@history-toolbar-color : #FFF;
|
||||
|
||||
// System messages
|
||||
@sys-msg-background : @ol-blue;
|
||||
@sys-msg-color : #FFF;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue