mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' of github.com:sharelatex/web-sharelatex
This commit is contained in:
commit
3c9f6e0ce0
21 changed files with 259 additions and 156 deletions
|
@ -18,9 +18,11 @@ module.exports = EditorUpdatesController =
|
||||||
if takeSnapshot
|
if takeSnapshot
|
||||||
AutomaticSnapshotManager.markProjectAsUpdated(project_id)
|
AutomaticSnapshotManager.markProjectAsUpdated(project_id)
|
||||||
|
|
||||||
|
logger.log doc_id: doc_id, project_id: project_id, client_id: update.meta?.source, version: update.v, "sending update to doc updater"
|
||||||
|
|
||||||
DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) ->
|
DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) ->
|
||||||
if error?
|
if error?
|
||||||
logger.error err:error, project_id: project_id, "document was not available for update"
|
logger.error err:error, project_id: project_id, doc_id: doc_id, client_id: update.meta?.source, version: update.v, "document was not available for update"
|
||||||
client.disconnect()
|
client.disconnect()
|
||||||
callback(error)
|
callback(error)
|
||||||
|
|
||||||
|
@ -52,8 +54,10 @@ module.exports = EditorUpdatesController =
|
||||||
io = require('../../infrastructure/Server').io
|
io = require('../../infrastructure/Server').io
|
||||||
for client in io.sockets.clients(doc_id)
|
for client in io.sockets.clients(doc_id)
|
||||||
if client.id == update.meta.source
|
if client.id == update.meta.source
|
||||||
|
logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, "distributing update to sender"
|
||||||
client.emit "otUpdateApplied", v: update.v, doc: update.doc
|
client.emit "otUpdateApplied", v: update.v, doc: update.doc
|
||||||
else
|
else
|
||||||
|
logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, client_id: client.id, "distributing update to collaborator"
|
||||||
client.emit "otUpdateApplied", update
|
client.emit "otUpdateApplied", update
|
||||||
|
|
||||||
_processErrorFromDocumentUpdater: (doc_id, error, message) ->
|
_processErrorFromDocumentUpdater: (doc_id, error, message) ->
|
||||||
|
|
|
@ -2,37 +2,43 @@ logger = require('logger-sharelatex')
|
||||||
metrics = require('../../infrastructure/Metrics')
|
metrics = require('../../infrastructure/Metrics')
|
||||||
Settings = require('settings-sharelatex')
|
Settings = require('settings-sharelatex')
|
||||||
metrics = require("../../infrastructure/Metrics")
|
metrics = require("../../infrastructure/Metrics")
|
||||||
ses = require('node-ses')
|
nodemailer = require("nodemailer")
|
||||||
|
|
||||||
if Settings.email? and Settings.email.fromAddress?
|
if Settings.email? and Settings.email.fromAddress?
|
||||||
defaultFromAddress = Settings.email.fromAddress
|
defaultFromAddress = Settings.email.fromAddress
|
||||||
else
|
else
|
||||||
defaultFromAddress = ""
|
defaultFromAddress = ""
|
||||||
|
|
||||||
if Settings.email?.ses? and Settings.email.ses?.key? and Settings.email.ses?.key != "" and Settings.email.ses?.secret? and Settings.email.ses?.secret != ""
|
# provide dummy mailer unless we have a better one configured.
|
||||||
client = ses.createClient({ key: Settings.email.ses.key, secret: Settings.email.ses.secret });
|
client =
|
||||||
else
|
sendMail: (options, callback = (err,status) ->) ->
|
||||||
logger.warn "AWS SES credentials are not configured. No emails will be sent."
|
logger.log options:options, "Would send email if enabled."
|
||||||
client =
|
callback()
|
||||||
sendemail: (options, callback = (err, data, res) ->) ->
|
|
||||||
logger.log options: options, "would send email if SES credentials enabled"
|
if Settings.email?
|
||||||
callback()
|
if Settings.email.transport? and Settings.email.parameters?
|
||||||
|
nm_client = nodemailer.createTransport( Settings.email.transport, Settings.email.parameters )
|
||||||
|
if nm_client
|
||||||
|
client = nm_client
|
||||||
|
else
|
||||||
|
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
|
||||||
|
else
|
||||||
|
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
|
|
||||||
sendEmail : (options, callback = (error) ->)->
|
sendEmail : (options, callback = (error) ->)->
|
||||||
logger.log receiver:options.to, subject:options.subject, "sending email"
|
logger.log receiver:options.to, subject:options.subject, "sending email"
|
||||||
metrics.inc "email"
|
metrics.inc "email"
|
||||||
options =
|
options =
|
||||||
to: options.to
|
to: options.to
|
||||||
from: defaultFromAddress
|
from: defaultFromAddress
|
||||||
subject: options.subject
|
subject: options.subject
|
||||||
message: options.html
|
html: options.html
|
||||||
replyTo: options.replyTo || Settings.email.replyToAddress
|
replyTo: options.replyTo || Settings.email.replyToAddress
|
||||||
client.sendemail options, (err, data, res)->
|
client.sendMail options, (err, res)->
|
||||||
if err?
|
if err?
|
||||||
logger.err err:err, "error sending message"
|
logger.err err:err, "error sending message"
|
||||||
else
|
else
|
||||||
logger.log "Message sent to #{options.to}"
|
logger.log "Message sent to #{options.to}"
|
||||||
callback(err)
|
callback(err)
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,10 @@ module.exports = class Router
|
||||||
require("./models/Project").Project.findOne {}, () ->
|
require("./models/Project").Project.findOne {}, () ->
|
||||||
throw new Error("Test error")
|
throw new Error("Test error")
|
||||||
|
|
||||||
|
app.post '/error/client', (req, res, next) ->
|
||||||
|
logger.error err: req.body.error, meta: req.body.meta, "client side error"
|
||||||
|
res.send(204)
|
||||||
|
|
||||||
app.get '*', HomeController.notFound
|
app.get '*', HomeController.notFound
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,9 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
ga('create', '#{gaToken}', 'sharelatex.com');
|
ga('create', '#{gaToken}', 'sharelatex.com');
|
||||||
ga('send', 'pageview');
|
ga('send', 'pageview');
|
||||||
|
- else
|
||||||
|
script(type='text/javascript')
|
||||||
|
window.ga = function() {};
|
||||||
|
|
||||||
script
|
script
|
||||||
window.csrfToken = "#{csrfToken}";
|
window.csrfToken = "#{csrfToken}";
|
||||||
|
|
|
@ -77,15 +77,6 @@
|
||||||
.modal-body
|
.modal-body
|
||||||
span.message
|
span.message
|
||||||
|
|
||||||
#genericServerErrorModal(style='display: none')
|
|
||||||
.modal
|
|
||||||
.modal-header
|
|
||||||
h3 There was a problem talking to the server
|
|
||||||
.modal-body
|
|
||||||
span.message Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know.
|
|
||||||
.modal-footer
|
|
||||||
button.btn.btn-primary.cancel Ok
|
|
||||||
|
|
||||||
#projectUploadModal(style='display: none')
|
#projectUploadModal(style='display: none')
|
||||||
.modal
|
.modal
|
||||||
.modal-header
|
.modal-header
|
||||||
|
|
|
@ -136,16 +136,6 @@
|
||||||
#subscribeForm
|
#subscribeForm
|
||||||
.modal-footer
|
.modal-footer
|
||||||
|
|
||||||
|
|
||||||
script(type="text/template")#genericModalTemplateWithButton
|
|
||||||
.modal
|
|
||||||
.modal-header
|
|
||||||
h3 {{ title }}
|
|
||||||
.modal-body
|
|
||||||
.message {{ message }}
|
|
||||||
.modal-footer
|
|
||||||
button.btn.btn-primary ok
|
|
||||||
|
|
||||||
script(type="text/template")#genericModalButtonTemplate
|
script(type="text/template")#genericModalButtonTemplate
|
||||||
button(class="btn {{ class }}") {{ text }}
|
button(class="btn {{ class }}") {{ text }}
|
||||||
|
|
||||||
|
@ -300,22 +290,29 @@
|
||||||
-if(session && session.user && session.user.isAdmin)
|
-if(session && session.user && session.user.isAdmin)
|
||||||
.box
|
.box
|
||||||
.page-header
|
.page-header
|
||||||
h2 Publish Project
|
h2 Publish project as template
|
||||||
|
|
||||||
|
#publishedAsTemplateArea.show-when-published.alert.alert-success
|
||||||
|
p
|
||||||
|
.btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish
|
||||||
|
i.icon-ok
|
||||||
|
| Your project is currently published.
|
||||||
|
a#templateLink(href='{{canonicalUrl}}') View in template gallery.
|
||||||
|
p
|
||||||
|
| Lastest version: {{publishedDate}}.
|
||||||
|
|
||||||
#publishedAsTemplateArea(style="display:none;")
|
#problemWithPublishingArea
|
||||||
a#templateLink(href='{{canonicalUrl}}') View Template
|
|
||||||
p published at {{publishedDate}}
|
|
||||||
.btn.btn-warning#unPublishProjectAsTemplate unpublish project as template
|
|
||||||
.btn.btn-success#republishProjectAsTemplate re publish project as template
|
|
||||||
|
|
||||||
#problemWithPublishingArea(style="display:none;")
|
|
||||||
p There is a problem with our publishing service, please try again in a few minutes.
|
p There is a problem with our publishing service, please try again in a few minutes.
|
||||||
#publishWorkingArea(style="display:none;")
|
#publishWorkingArea
|
||||||
p Working.....
|
p Working...
|
||||||
#unpublishedAsTemplateArea(style="display:none;")
|
div.show-when-published.show-when-unpublished.project-description
|
||||||
.btn.btn-success#publishProjectAsTemplate Publish project as template
|
label(for="project-description") Description
|
||||||
div
|
.row-fluid
|
||||||
textarea.span6#projectDescription {{description}}
|
textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}}
|
||||||
|
#unpublishedAsTemplateArea.show-when-unpublished
|
||||||
|
.btn.btn-success#publishProjectAsTemplate Publish
|
||||||
|
p.show-when-published
|
||||||
|
button.btn.btn-success#republishProjectAsTemplate Re-Publish
|
||||||
|
|
||||||
|
|
||||||
script(type="text/template")#settingsPanelTemplate
|
script(type="text/template")#settingsPanelTemplate
|
||||||
|
|
|
@ -134,6 +134,22 @@ module.exports =
|
||||||
{name: "English", code: "en"}
|
{name: "English", code: "en"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Email support
|
||||||
|
# -------------
|
||||||
|
#
|
||||||
|
# ShareLaTeX uses nodemailer (http://www.nodemailer.com/) to send transactional emails.
|
||||||
|
# To see the range of transport and options they support, see http://www.nodemailer.com/docs/transports
|
||||||
|
#email:
|
||||||
|
# fromAddress: ""
|
||||||
|
# replyTo: ""
|
||||||
|
# lifecycle: false
|
||||||
|
## Example transport and parameter settings for Amazon SES
|
||||||
|
# transport: "SES"
|
||||||
|
# parameters:
|
||||||
|
# AWSAccessKeyID: ""
|
||||||
|
# AWSSecretKey: ""
|
||||||
|
|
||||||
|
|
||||||
# Third party services
|
# Third party services
|
||||||
# --------------------
|
# --------------------
|
||||||
#
|
#
|
||||||
|
@ -153,15 +169,6 @@ module.exports =
|
||||||
# tenderUrl: ""
|
# tenderUrl: ""
|
||||||
#
|
#
|
||||||
|
|
||||||
# email:
|
|
||||||
# fromAddress: ""
|
|
||||||
# replyTo: ""
|
|
||||||
# lifecycle: false
|
|
||||||
# ShareLaTeX uses Amazon's SES api to send transactional emails.
|
|
||||||
# Uncomment these lines and provide your credentials to be able to send emails.
|
|
||||||
# ses:
|
|
||||||
# "key":""
|
|
||||||
# "secret":""
|
|
||||||
|
|
||||||
# Production Settings
|
# Production Settings
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
"fairy": "0.0.2",
|
"fairy": "0.0.2",
|
||||||
"node-uuid": "1.4.0",
|
"node-uuid": "1.4.0",
|
||||||
"mongojs": "0.9.8",
|
"mongojs": "0.9.8",
|
||||||
"node-ses": "0.0.3",
|
"nodemailer": "0.6.1",
|
||||||
"bcrypt": "0.7.5",
|
"bcrypt": "0.7.5",
|
||||||
"archiver": "0.5.1",
|
"archiver": "0.5.1",
|
||||||
"nodetime": "0.8.15",
|
"nodetime": "0.8.15",
|
||||||
|
|
|
@ -79,6 +79,14 @@ define () ->
|
||||||
\\end{enumerate}
|
\\end{enumerate}
|
||||||
"""
|
"""
|
||||||
meta: "env"
|
meta: "env"
|
||||||
|
}, {
|
||||||
|
caption: "\\begin{itemize}..."
|
||||||
|
snippet: """
|
||||||
|
\\begin{itemize}
|
||||||
|
\\item $1
|
||||||
|
\\end{itemize}
|
||||||
|
"""
|
||||||
|
meta: "env"
|
||||||
}, {
|
}, {
|
||||||
caption: "\\begin{frame}..."
|
caption: "\\begin{frame}..."
|
||||||
snippet: """
|
snippet: """
|
||||||
|
|
|
@ -136,16 +136,18 @@ define [
|
||||||
@unBindFromSocketEvents()
|
@unBindFromSocketEvents()
|
||||||
|
|
||||||
_bindToShareJsDocEvents: () ->
|
_bindToShareJsDocEvents: () ->
|
||||||
@doc.on "error", (error) => @_onError error
|
@doc.on "error", (error, meta) => @_onError error, meta
|
||||||
@doc.on "externalUpdate", () => @trigger "externalUpdate"
|
@doc.on "externalUpdate", () => @trigger "externalUpdate"
|
||||||
@doc.on "remoteop", () => @trigger "remoteop"
|
@doc.on "remoteop", () => @trigger "remoteop"
|
||||||
@doc.on "op:sent", () => @trigger "op:sent"
|
@doc.on "op:sent", () => @trigger "op:sent"
|
||||||
@doc.on "op:acknowledged", () => @trigger "op:acknowledged"
|
@doc.on "op:acknowledged", () => @trigger "op:acknowledged"
|
||||||
|
|
||||||
_onError: (error) ->
|
_onError: (error, meta = {}) ->
|
||||||
console.error "ShareJS error", error
|
console.error "ShareJS error", error, meta
|
||||||
ga('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
|
ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
|
||||||
@ide.socket.disconnect()
|
@ide.socket.disconnect()
|
||||||
|
meta.doc_id = @doc_id
|
||||||
|
@ide.reportError(error, meta)
|
||||||
@doc?.clearInflightAndPendingOps()
|
@doc?.clearInflightAndPendingOps()
|
||||||
@_cleanUp()
|
@_cleanUp()
|
||||||
@trigger "error", error
|
@trigger "error", error
|
||||||
|
|
|
@ -121,7 +121,7 @@ define [
|
||||||
@aceEditor = aceEditor = AceEditor.edit("editor")
|
@aceEditor = aceEditor = AceEditor.edit("editor")
|
||||||
|
|
||||||
@on "resize", => @aceEditor.resize()
|
@on "resize", => @aceEditor.resize()
|
||||||
@ide.layoutManager.on "resize", => @aceEditor.resize()
|
@ide.layoutManager.on "resize", => @trigger "resize"
|
||||||
|
|
||||||
mode = window.userSettings.mode
|
mode = window.userSettings.mode
|
||||||
theme = window.userSettings.theme
|
theme = window.userSettings.theme
|
||||||
|
|
|
@ -94,14 +94,17 @@ define [
|
||||||
|
|
||||||
INFLIGHT_OP_TIMEOUT: 10000
|
INFLIGHT_OP_TIMEOUT: 10000
|
||||||
_startInflightOpTimeout: (update) ->
|
_startInflightOpTimeout: (update) ->
|
||||||
|
meta =
|
||||||
|
v: update.v
|
||||||
|
op_sent_at: new Date()
|
||||||
timer = setTimeout () =>
|
timer = setTimeout () =>
|
||||||
@_handleError "Doc op was not acknowledged in time"
|
@_handleError new Error("Doc op was not acknowledged in time"), meta
|
||||||
, @INFLIGHT_OP_TIMEOUT
|
, @INFLIGHT_OP_TIMEOUT
|
||||||
@_doc.inflightCallbacks.push () =>
|
@_doc.inflightCallbacks.push () =>
|
||||||
clearTimeout timer
|
clearTimeout timer
|
||||||
|
|
||||||
_handleError: (error) ->
|
_handleError: (error, meta = {}) ->
|
||||||
@trigger "error", error
|
@trigger "error", error, meta
|
||||||
|
|
||||||
_bindToDocChanges: (doc) ->
|
_bindToDocChanges: (doc) ->
|
||||||
submitOp = doc.submitOp
|
submitOp = doc.submitOp
|
||||||
|
|
|
@ -48,7 +48,7 @@ define [
|
||||||
SearchManager,
|
SearchManager,
|
||||||
Project,
|
Project,
|
||||||
User,
|
User,
|
||||||
StandaloneModal,
|
Modal,
|
||||||
FileTreeManager,
|
FileTreeManager,
|
||||||
MessageManager,
|
MessageManager,
|
||||||
HelpManager,
|
HelpManager,
|
||||||
|
@ -144,16 +144,39 @@ define [
|
||||||
setTimeout(joinProject, 100)
|
setTimeout(joinProject, 100)
|
||||||
|
|
||||||
showErrorModal: (title, message)->
|
showErrorModal: (title, message)->
|
||||||
modalOptions =
|
new Modal {
|
||||||
templateId:'genericModalTemplate'
|
|
||||||
isStatic: false
|
|
||||||
title: title
|
title: title
|
||||||
message:message
|
message: message
|
||||||
new Modal modalOptions
|
buttons: [ text: "OK" ]
|
||||||
|
}
|
||||||
|
|
||||||
showGenericServerErrorMessage: (message)->
|
showGenericServerErrorMessage: ()->
|
||||||
new Modal
|
new Modal {
|
||||||
templateId : "genericServerErrorModal"
|
title: "There was a problem talking to the server"
|
||||||
|
message: "Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know."
|
||||||
|
buttons: [ text: "OK" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError: (error, meta = {}) ->
|
||||||
|
meta.client_id = @socket?.socket?.sessionid
|
||||||
|
meta.transport = @socket?.socket?.transport?.name
|
||||||
|
meta.client_now = new Date()
|
||||||
|
meta.last_connected = @connectionManager.lastConnected
|
||||||
|
meta.second_last_connected = @connectionManager.secondLastConnected
|
||||||
|
meta.last_disconnected = @connectionManager.lastDisconnected
|
||||||
|
meta.second_last_disconnected = @connectionManager.secondLastDisconnected
|
||||||
|
errorObj = {}
|
||||||
|
for key in Object.getOwnPropertyNames(error)
|
||||||
|
errorObj[key] = error[key]
|
||||||
|
$.ajax
|
||||||
|
url: "/error/client"
|
||||||
|
type: "POST"
|
||||||
|
data: JSON.stringify
|
||||||
|
error: errorObj
|
||||||
|
meta: meta
|
||||||
|
contentType: "application/json; charset=utf-8"
|
||||||
|
headers:
|
||||||
|
"X-Csrf-Token": window.csrfToken
|
||||||
|
|
||||||
setLoadingMessage: (message) ->
|
setLoadingMessage: (message) ->
|
||||||
$("#loadingMessage").text(message)
|
$("#loadingMessage").text(message)
|
||||||
|
@ -171,45 +194,6 @@ define [
|
||||||
ide.layoutManager.resizeAllSplitters()
|
ide.layoutManager.resizeAllSplitters()
|
||||||
ide.tourManager = new IdeTour ide
|
ide.tourManager = new IdeTour ide
|
||||||
|
|
||||||
class Modal
|
|
||||||
#templateId, title, message, isStatic, cancelCallback
|
|
||||||
constructor: (options, completeCallback = () -> {})->
|
|
||||||
html = $("##{options.templateId}").html()
|
|
||||||
modal = "<div id='modal' style='display:none'>#{html}</div>"
|
|
||||||
$('body').append(modal)
|
|
||||||
$modal = $('#modal')
|
|
||||||
|
|
||||||
if options.title?
|
|
||||||
$modal.find('h3').text(options.title)
|
|
||||||
if options.message?
|
|
||||||
$modal.find('.message').text(options.message)
|
|
||||||
if options.inputValue?
|
|
||||||
$modal.find('input').val(options.inputValue)
|
|
||||||
|
|
||||||
backdrop = true
|
|
||||||
if options.backdrop?
|
|
||||||
backdrop = options.backdrop
|
|
||||||
|
|
||||||
$modal.modal backdrop:backdrop, show:true, keyboard:true, isStatic:options.isStatic
|
|
||||||
$modal.find('input').focus()
|
|
||||||
|
|
||||||
$modal.find('button').click (e)=>
|
|
||||||
e.preventDefault()
|
|
||||||
$modal.modal('hide')
|
|
||||||
if e.target.className.indexOf("cancel") == -1
|
|
||||||
inputval = $modal.find('input').val()
|
|
||||||
completeCallback(inputval)
|
|
||||||
|
|
||||||
$modal.find('input').keydown (event)=>
|
|
||||||
code = event.keyCode || event.which
|
|
||||||
if code == 13
|
|
||||||
$modal.find('button.primary').click()
|
|
||||||
|
|
||||||
$modal.bind 'hide', ()->
|
|
||||||
if options.cancelCallback?
|
|
||||||
options.cancelCallback()
|
|
||||||
$('#modal').remove()
|
|
||||||
|
|
||||||
ide.savingAreaManager =
|
ide.savingAreaManager =
|
||||||
$savingArea : $('#saving-area')
|
$savingArea : $('#saving-area')
|
||||||
timeOut: undefined
|
timeOut: undefined
|
||||||
|
@ -220,7 +204,7 @@ define [
|
||||||
return if @timeOut?
|
return if @timeOut?
|
||||||
@clearTimeout()
|
@clearTimeout()
|
||||||
@timeOut = setTimeout((=>
|
@timeOut = setTimeout((=>
|
||||||
ga('send', 'event', 'editor-interaction', 'notification-shown', "saving")
|
ga?('send', 'event', 'editor-interaction', 'notification-shown', "saving")
|
||||||
$("#savingProblems").show()
|
$("#savingProblems").show()
|
||||||
), 1000)
|
), 1000)
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,15 @@ define [
|
||||||
@socket = @ide.socket
|
@socket = @ide.socket
|
||||||
@socket.on "connect", () =>
|
@socket.on "connect", () =>
|
||||||
@connected = true
|
@connected = true
|
||||||
|
@secondLastConnected = @lastConnected
|
||||||
|
@lastConnected = new Date()
|
||||||
@hideModal()
|
@hideModal()
|
||||||
@cancelReconnect()
|
@cancelReconnect()
|
||||||
|
|
||||||
@socket.on 'disconnect', () =>
|
@socket.on 'disconnect', () =>
|
||||||
@connected = false
|
@connected = false
|
||||||
|
@secondLastDisconnected = @lastDisconnected
|
||||||
|
@lastDisconnected = new Date()
|
||||||
@ide.trigger "disconnect"
|
@ide.trigger "disconnect"
|
||||||
setTimeout(=>
|
setTimeout(=>
|
||||||
ga('send', 'event', 'editor-interaction', 'disconnect')
|
ga('send', 'event', 'editor-interaction', 'disconnect')
|
||||||
|
|
|
@ -3,9 +3,10 @@ define [
|
||||||
"models/ProjectMemberList"
|
"models/ProjectMemberList"
|
||||||
"account/AccountManager"
|
"account/AccountManager"
|
||||||
"utils/Modal"
|
"utils/Modal"
|
||||||
|
"moment"
|
||||||
"libs/backbone"
|
"libs/backbone"
|
||||||
"libs/mustache"
|
"libs/mustache"
|
||||||
], (User, ProjectMemberList, AccountManager, Modal) ->
|
], (User, ProjectMemberList, AccountManager, Modal, moment) ->
|
||||||
INFINITE_COLLABORATORS = -1
|
INFINITE_COLLABORATORS = -1
|
||||||
|
|
||||||
class ProjectMembersManager
|
class ProjectMembersManager
|
||||||
|
@ -20,6 +21,8 @@ define [
|
||||||
name: "Share"
|
name: "Share"
|
||||||
content : $(@templates.userPanel)
|
content : $(@templates.userPanel)
|
||||||
lock: true
|
lock: true
|
||||||
|
onShown: () =>
|
||||||
|
@publishProjectView?.refreshPublishStatus()
|
||||||
|
|
||||||
setupPublish = _.once =>
|
setupPublish = _.once =>
|
||||||
@publishProjectView = new PublishProjectView
|
@publishProjectView = new PublishProjectView
|
||||||
|
@ -177,25 +180,32 @@ define [
|
||||||
initialize: () ->
|
initialize: () ->
|
||||||
@ide = @options.ide
|
@ide = @options.ide
|
||||||
@model = @ide.project
|
@model = @ide.project
|
||||||
_.bindAll(this, "render");
|
@render()
|
||||||
this.model.bind('change', this.render)
|
|
||||||
@refreshPublishStatus()
|
|
||||||
|
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
viewModel =
|
viewModel =
|
||||||
description: @model.get("description")
|
description: @model.get("description")
|
||||||
canonicalUrl: @model.get("template.canonicalUrl")
|
canonicalUrl: @model.get("template.canonicalUrl")
|
||||||
isPublished: @model.get("template.isPublished")
|
isPublished: @model.get("template.isPublished")
|
||||||
publishedDate: @model.get("template.publishedDate")
|
publishedDate: moment(@model.get("template.publishedDate")).format("Do MMM YYYY, h:mm a")
|
||||||
|
|
||||||
$(@el).html $(Mustache.to_html(@template, viewModel))
|
@$el.html $(Mustache.to_html(@template, viewModel))
|
||||||
@publishedArea = $('#publishedAsTemplateArea')
|
@publishedArea = $('.show-when-published')
|
||||||
@unpublishedArea = $('#unpublishedAsTemplateArea')
|
@unpublishedArea = $('.show-when-unpublished')
|
||||||
|
$('#problemWithPublishingArea').hide()
|
||||||
|
$('#publishWorkingArea').hide()
|
||||||
|
|
||||||
|
refreshView: () ->
|
||||||
|
if @model.get("template.isPublished")
|
||||||
|
@$("a#templateLink").attr("href", @model.get("template.canonicalUrl"))
|
||||||
|
@publishedArea.show()
|
||||||
|
else
|
||||||
|
@unpublishedArea.show()
|
||||||
|
|
||||||
refreshPublishStatus: ->
|
refreshPublishStatus: ->
|
||||||
|
@showWorking()
|
||||||
@ide.socket.emit "getPublishedDetails", @ide.user.get("id"), (err, details)=>
|
@ide.socket.emit "getPublishedDetails", @ide.user.get("id"), (err, details)=>
|
||||||
|
@hideWorking()
|
||||||
if err?
|
if err?
|
||||||
return @showError()
|
return @showError()
|
||||||
|
|
||||||
|
@ -203,16 +213,17 @@ define [
|
||||||
if details.exists
|
if details.exists
|
||||||
@model.set("template.canonicalUrl", details.canonicalUrl)
|
@model.set("template.canonicalUrl", details.canonicalUrl)
|
||||||
@model.set("template.publishedDate", details.publishedDate)
|
@model.set("template.publishedDate", details.publishedDate)
|
||||||
@publishedArea.show()
|
|
||||||
@unpublishedArea.hide()
|
@refreshView()
|
||||||
else
|
|
||||||
@publishedArea.hide()
|
|
||||||
@unpublishedArea.show()
|
|
||||||
|
|
||||||
showError: ->
|
showError: ->
|
||||||
|
@publishedArea.hide()
|
||||||
|
@unpublishedArea.hide()
|
||||||
$('#problemWithPublishingArea').show()
|
$('#problemWithPublishingArea').show()
|
||||||
|
|
||||||
showWorking: ->
|
showWorking: ->
|
||||||
|
@publishedArea.hide()
|
||||||
|
@unpublishedArea.hide()
|
||||||
$('#publishWorkingArea').show()
|
$('#publishWorkingArea').show()
|
||||||
|
|
||||||
hideWorking: ->
|
hideWorking: ->
|
||||||
|
|
|
@ -16,11 +16,11 @@ define [
|
||||||
@createAceEditor()
|
@createAceEditor()
|
||||||
@aceEditor.setValue(@getPlainDiffContent())
|
@aceEditor.setValue(@getPlainDiffContent())
|
||||||
@aceEditor.clearSelection()
|
@aceEditor.clearSelection()
|
||||||
session = @aceEditor.getSession()
|
@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
|
||||||
session.setMode(new LatexMode.Mode())
|
|
||||||
session.setUseWrapMode(true)
|
|
||||||
@insertMarkers()
|
@insertMarkers()
|
||||||
@insertNameTag()
|
@insertNameTag()
|
||||||
|
@insertMoreChangeLabels()
|
||||||
|
@scrollToFirstChange()
|
||||||
return @
|
return @
|
||||||
|
|
||||||
destroy: () ->
|
destroy: () ->
|
||||||
|
@ -34,12 +34,18 @@ define [
|
||||||
@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
|
@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
|
||||||
@aceEditor.setReadOnly true
|
@aceEditor.setReadOnly true
|
||||||
@aceEditor.setShowPrintMargin(false)
|
@aceEditor.setShowPrintMargin(false)
|
||||||
|
session = @aceEditor.getSession()
|
||||||
|
session.setMode(new LatexMode.Mode())
|
||||||
|
session.setUseWrapMode(true)
|
||||||
|
|
||||||
@aceEditor.on "mousemove", (e) =>
|
@aceEditor.on "mousemove", (e) =>
|
||||||
position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
|
position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
|
||||||
e.position = position
|
e.position = position
|
||||||
@updateVisibleNames(e)
|
@updateVisibleNames(e)
|
||||||
|
|
||||||
|
session.on "changeScrollTop", (e) =>
|
||||||
|
@updateMoreChangeLabels()
|
||||||
|
|
||||||
getPlainDiffContent: () ->
|
getPlainDiffContent: () ->
|
||||||
content = ""
|
content = ""
|
||||||
for entry in @model.get("diff") or []
|
for entry in @model.get("diff") or []
|
||||||
|
@ -102,7 +108,6 @@ define [
|
||||||
, foreground
|
, foreground
|
||||||
|
|
||||||
insertNameTag: () ->
|
insertNameTag: () ->
|
||||||
@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
|
|
||||||
@$nameTagEl = $("<div class='change-name-marker'></div>")
|
@$nameTagEl = $("<div class='change-name-marker'></div>")
|
||||||
@$nameTagEl.css({
|
@$nameTagEl.css({
|
||||||
position: "absolute"
|
position: "absolute"
|
||||||
|
@ -110,6 +115,19 @@ define [
|
||||||
@$nameTagEl.hide()
|
@$nameTagEl.hide()
|
||||||
@$ace.append(@$nameTagEl)
|
@$ace.append(@$nameTagEl)
|
||||||
|
|
||||||
|
insertMoreChangeLabels: () ->
|
||||||
|
@$changesBefore = $("<div class='changes-before'><span></span> <i class='icon-arrow-up'></div>")
|
||||||
|
@$changesAfter = $("<div class='changes-after'><span></span> <i class='icon-arrow-down'></div>")
|
||||||
|
@$ace.append(@$changesBefore)
|
||||||
|
@$ace.append(@$changesAfter)
|
||||||
|
@updateMoreChangeLabels()
|
||||||
|
|
||||||
|
scrollToFirstChange: () ->
|
||||||
|
if @entries? and @entries[0]?
|
||||||
|
row = @entries[0].range.start.row
|
||||||
|
@aceEditor.gotoLine(0)
|
||||||
|
@aceEditor.gotoLine(row, 0, true)
|
||||||
|
|
||||||
_drawNameTag: (entry, position) ->
|
_drawNameTag: (entry, position) ->
|
||||||
@$nameTagEl.show()
|
@$nameTagEl.show()
|
||||||
|
|
||||||
|
@ -161,5 +179,30 @@ define [
|
||||||
if !visibleName
|
if !visibleName
|
||||||
@_hideNameTag()
|
@_hideNameTag()
|
||||||
|
|
||||||
|
updateMoreChangeLabels: () ->
|
||||||
|
return if !@$changesBefore or !@$changesAfter
|
||||||
|
firstRow = @aceEditor.renderer.getFirstFullyVisibleRow()
|
||||||
|
lastRow = @aceEditor.renderer.getLastFullyVisibleRow()
|
||||||
|
changesBefore = 0
|
||||||
|
changesAfter = 0
|
||||||
|
for entry in @entries or []
|
||||||
|
if entry.range.start.row < firstRow
|
||||||
|
changesBefore += 1
|
||||||
|
if entry.range.end.row > lastRow
|
||||||
|
changesAfter += 1
|
||||||
|
if changesBefore > 0
|
||||||
|
@$changesBefore.find("span").text("#{changesBefore} more change#{if changesBefore > 1 then "s" else ""} above")
|
||||||
|
@$changesBefore.show()
|
||||||
|
else
|
||||||
|
@$changesBefore.hide()
|
||||||
|
if changesAfter > 0
|
||||||
|
@$changesAfter.find("span").text("#{changesAfter} more change#{if changesAfter > 1 then "s" else ""} below")
|
||||||
|
@$changesAfter.show()
|
||||||
|
else
|
||||||
|
@$changesAfter.hide()
|
||||||
|
|
||||||
|
resize: () ->
|
||||||
|
@aceEditor.resize()
|
||||||
|
|
||||||
return DiffView
|
return DiffView
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,9 @@ define [
|
||||||
@ide.editor.on "change:doc", () =>
|
@ide.editor.on "change:doc", () =>
|
||||||
@hideEl()
|
@hideEl()
|
||||||
|
|
||||||
|
@ide.editor.on "resize", () =>
|
||||||
|
@diffView?.resize()
|
||||||
|
|
||||||
@$el.find(".track-changes-close").on "click", (e) =>
|
@$el.find(".track-changes-close").on "click", (e) =>
|
||||||
e.preventDefault
|
e.preventDefault
|
||||||
@hide()
|
@hide()
|
||||||
|
|
|
@ -891,6 +891,18 @@ i[class*="sprite-"] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-description {
|
||||||
|
textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
border-bottom: 1px solid #eeeeee;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1.8em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-facebook {
|
.btn-facebook {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
|
|
@ -56,7 +56,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleted-change-background, .deleted-change-foreground, .inserted-change-background, .change-name-marker {
|
.deleted-change-background,
|
||||||
|
.deleted-change-foreground,
|
||||||
|
.inserted-change-background,
|
||||||
|
.change-name-marker,
|
||||||
|
.changes-before,
|
||||||
|
.changes-after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
@ -71,6 +76,21 @@
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.changes-before {
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
.changes-after {
|
||||||
|
bottom: 6px;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
.changes-before, .changes-after {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #999;
|
||||||
|
.border-radius(3px);
|
||||||
|
}
|
||||||
|
|
||||||
ul.change-list {
|
ul.change-list {
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe "EditorUpdatesController", ->
|
||||||
@client = new MockClient()
|
@client = new MockClient()
|
||||||
@callback = sinon.stub()
|
@callback = sinon.stub()
|
||||||
@EditorUpdatesController = SandboxedModule.require modulePath, requires:
|
@EditorUpdatesController = SandboxedModule.require modulePath, requires:
|
||||||
"logger-sharelatex": @logger = { error: sinon.stub() }
|
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub() }
|
||||||
"./EditorRealTimeController" : @EditorRealTimeController = {}
|
"./EditorRealTimeController" : @EditorRealTimeController = {}
|
||||||
"../DocumentUpdater/DocumentUpdaterHandler" : @DocumentUpdaterHandler = {}
|
"../DocumentUpdater/DocumentUpdaterHandler" : @DocumentUpdaterHandler = {}
|
||||||
"../Versioning/AutomaticSnapshotManager" : @AutomaticSnapshotManager = {}
|
"../Versioning/AutomaticSnapshotManager" : @AutomaticSnapshotManager = {}
|
||||||
|
|
|
@ -12,20 +12,21 @@ describe "Email", ->
|
||||||
|
|
||||||
@settings =
|
@settings =
|
||||||
email:
|
email:
|
||||||
ses:
|
transport: "ses"
|
||||||
key: "key"
|
parameters:
|
||||||
secret: "secret"
|
AWSAccessKeyID: "key"
|
||||||
|
AWSSecretKey: "secret"
|
||||||
fromAddress: "bob@bob.com"
|
fromAddress: "bob@bob.com"
|
||||||
replyToAddress: "sally@gmail.com"
|
replyToAddress: "sally@gmail.com"
|
||||||
|
|
||||||
@sesClient =
|
@sesClient =
|
||||||
sendemail: sinon.stub()
|
sendMail: sinon.stub()
|
||||||
@ses =
|
@ses =
|
||||||
createClient: => @sesClient
|
createTransport: => @sesClient
|
||||||
@sender = SandboxedModule.require modulePath, requires:
|
@sender = SandboxedModule.require modulePath, requires:
|
||||||
'node-ses': @ses
|
'nodemailer': @ses
|
||||||
"settings-sharelatex":@settings
|
"settings-sharelatex":@settings
|
||||||
"logger-sharelatex":
|
"logger-sharelatex":
|
||||||
log:->
|
log:->
|
||||||
warn:->
|
warn:->
|
||||||
err:->
|
err:->
|
||||||
|
@ -38,44 +39,44 @@ describe "Email", ->
|
||||||
describe "sendEmail", ->
|
describe "sendEmail", ->
|
||||||
|
|
||||||
it "should set the properties on the email to send", (done)->
|
it "should set the properties on the email to send", (done)->
|
||||||
@sesClient.sendemail.callsArgWith(1)
|
@sesClient.sendMail.callsArgWith(1)
|
||||||
|
|
||||||
@sender.sendEmail @opts, =>
|
@sender.sendEmail @opts, =>
|
||||||
args = @sesClient.sendemail.args[0][0]
|
args = @sesClient.sendMail.args[0][0]
|
||||||
args.message.should.equal @opts.html
|
args.html.should.equal @opts.html
|
||||||
args.to.should.equal @opts.to
|
args.to.should.equal @opts.to
|
||||||
args.subject.should.equal @opts.subject
|
args.subject.should.equal @opts.subject
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should return the error", (done)->
|
it "should return the error", (done)->
|
||||||
@sesClient.sendemail.callsArgWith(1, "error")
|
@sesClient.sendMail.callsArgWith(1, "error")
|
||||||
@sender.sendEmail {}, (err)=>
|
@sender.sendEmail {}, (err)=>
|
||||||
err.should.equal "error"
|
err.should.equal "error"
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
it "should use the from address from settings", (done)->
|
it "should use the from address from settings", (done)->
|
||||||
@sesClient.sendemail.callsArgWith(1)
|
@sesClient.sendMail.callsArgWith(1)
|
||||||
|
|
||||||
@sender.sendEmail @opts, =>
|
@sender.sendEmail @opts, =>
|
||||||
args = @sesClient.sendemail.args[0][0]
|
args = @sesClient.sendMail.args[0][0]
|
||||||
args.from.should.equal @settings.email.fromAddress
|
args.from.should.equal @settings.email.fromAddress
|
||||||
done()
|
done()
|
||||||
|
|
||||||
it "should use the reply to address from settings", (done)->
|
it "should use the reply to address from settings", (done)->
|
||||||
@sesClient.sendemail.callsArgWith(1)
|
@sesClient.sendMail.callsArgWith(1)
|
||||||
|
|
||||||
@sender.sendEmail @opts, =>
|
@sender.sendEmail @opts, =>
|
||||||
args = @sesClient.sendemail.args[0][0]
|
args = @sesClient.sendMail.args[0][0]
|
||||||
args.replyTo.should.equal @settings.email.replyToAddress
|
args.replyTo.should.equal @settings.email.replyToAddress
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
|
||||||
it "should use the reply to address in options as an override", (done)->
|
it "should use the reply to address in options as an override", (done)->
|
||||||
@sesClient.sendemail.callsArgWith(1)
|
@sesClient.sendMail.callsArgWith(1)
|
||||||
|
|
||||||
@opts.replyTo = "someone@else.com"
|
@opts.replyTo = "someone@else.com"
|
||||||
@sender.sendEmail @opts, =>
|
@sender.sendEmail @opts, =>
|
||||||
args = @sesClient.sendemail.args[0][0]
|
args = @sesClient.sendMail.args[0][0]
|
||||||
args.replyTo.should.equal @opts.replyTo
|
args.replyTo.should.equal @opts.replyTo
|
||||||
done()
|
done()
|
||||||
|
|
Loading…
Reference in a new issue