Merge branch 'master' of github.com:sharelatex/web-sharelatex

This commit is contained in:
Henry Oswald 2014-03-18 15:09:50 +00:00
commit 3c9f6e0ce0
21 changed files with 259 additions and 156 deletions

View file

@ -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) ->

View file

@ -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)

View file

@ -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

View file

@ -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}";

View file

@ -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

View file

@ -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

View file

@ -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
# ------------------- # -------------------

View file

@ -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",

View file

@ -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: """

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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')

View file

@ -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: ->

View file

@ -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

View file

@ -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()

View file

@ -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);

View file

@ -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;

View file

@ -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 = {}

View file

@ -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()