Przeglądaj źródła

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

Henry Oswald 12 lat temu
rodzic
commit
3c9f6e0ce0

+ 5 - 1
services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee

@@ -18,9 +18,11 @@ module.exports = EditorUpdatesController =
 			if takeSnapshot
 				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) ->
 			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()
 			callback(error)
 
@@ -52,8 +54,10 @@ module.exports = EditorUpdatesController =
 		io = require('../../infrastructure/Server').io
 		for client in io.sockets.clients(doc_id)
 			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
 			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
 
 	_processErrorFromDocumentUpdater: (doc_id, error, message) ->

+ 21 - 15
services/web/app/coffee/Features/Email/EmailSender.coffee

@@ -2,37 +2,43 @@ logger = require('logger-sharelatex')
 metrics = require('../../infrastructure/Metrics')
 Settings = require('settings-sharelatex')
 metrics = require("../../infrastructure/Metrics")
-ses = require('node-ses')
+nodemailer = require("nodemailer")
 
 if Settings.email? and Settings.email.fromAddress?
 	defaultFromAddress = Settings.email.fromAddress
-else 
+else
 	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 != ""
-	client = ses.createClient({ key: Settings.email.ses.key, secret: Settings.email.ses.secret });
-else
-	logger.warn "AWS SES credentials are not configured. No emails will be sent."
-	client =
-		sendemail: (options, callback = (err, data, res) ->) ->
-			logger.log options: options, "would send email if SES credentials enabled"
-			callback()
+# provide dummy mailer unless we have a better one configured.
+client =
+	sendMail: (options, callback = (err,status) ->) ->
+		logger.log options:options, "Would send email if enabled."
+		callback()
 
-module.exports =
+if Settings.email?
+	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 =
 	sendEmail : (options, callback = (error) ->)->
 		logger.log receiver:options.to, subject:options.subject, "sending email"
 		metrics.inc "email"
-		options = 
+		options =
 			to: options.to
 			from: defaultFromAddress
 			subject: options.subject
-			message: options.html
+			html: options.html
 			replyTo: options.replyTo || Settings.email.replyToAddress
-		client.sendemail options, (err, data, res)->
+		client.sendMail options, (err, res)->
 			if err?
 				logger.err err:err, "error sending message"
 			else
 				logger.log "Message sent to #{options.to}"
 			callback(err)
-	
+

+ 4 - 0
services/web/app/coffee/router.coffee

@@ -217,6 +217,10 @@ module.exports = class Router
 			require("./models/Project").Project.findOne {}, () ->
 				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
 
 

+ 3 - 0
services/web/app/views/layout.jade

@@ -22,6 +22,9 @@ html(itemscope, itemtype='http://schema.org/Product')
 				})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
 				ga('create', '#{gaToken}', 'sharelatex.com');
 				ga('send', 'pageview');
+		- else
+			script(type='text/javascript')
+				window.ga = function() {};
 
 		script
 			window.csrfToken = "#{csrfToken}";

+ 0 - 9
services/web/app/views/modals.jade

@@ -77,15 +77,6 @@
 		.modal-body
 			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')
 	.modal
 		.modal-header

+ 22 - 25
services/web/app/views/templates.jade

@@ -136,16 +136,6 @@
 				#subscribeForm
 			.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
 		button(class="btn {{ class }}") {{ text }}
 
@@ -300,22 +290,29 @@
 		-if(session && session.user && session.user.isAdmin)
 			.box
 				.page-header
-					h2 Publish Project
-
-				#publishedAsTemplateArea(style="display:none;")
-					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;")
+					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}}. 
+
+				#problemWithPublishingArea
 					p There is a problem with our publishing service, please try again in a few minutes.
-				#publishWorkingArea(style="display:none;")
-					p Working.....
-				#unpublishedAsTemplateArea(style="display:none;")
-					.btn.btn-success#publishProjectAsTemplate Publish project as template
-				div
-					textarea.span6#projectDescription {{description}}
+				#publishWorkingArea
+					p Working...
+				div.show-when-published.show-when-unpublished.project-description
+					label(for="project-description") Description
+					.row-fluid
+						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

+ 16 - 9
services/web/config/settings.development.coffee

@@ -134,6 +134,22 @@ module.exports =
 		{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
 	# --------------------
 	#
@@ -153,15 +169,6 @@ module.exports =
 	# 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
 	# -------------------

+ 1 - 1
services/web/package.json

@@ -32,7 +32,7 @@
     "fairy": "0.0.2",
     "node-uuid": "1.4.0",
     "mongojs": "0.9.8",
-    "node-ses": "0.0.3",
+		"nodemailer": "0.6.1",
     "bcrypt": "0.7.5",
     "archiver": "0.5.1",
     "nodetime": "0.8.15",

+ 8 - 0
services/web/public/coffee/auto-complete/Snippets.coffee

@@ -79,6 +79,14 @@ define () ->
 			\\end{enumerate}
 		"""
 		meta: "env"
+	}, {
+		caption: "\\begin{itemize}..."
+		snippet: """
+			\\begin{itemize}
+			\\item $1
+			\\end{itemize}
+		"""
+		meta: "env"
 	}, {
 		caption: "\\begin{frame}..."
 		snippet: """

+ 6 - 4
services/web/public/coffee/editor/Document.coffee

@@ -136,16 +136,18 @@ define [
 			@unBindFromSocketEvents()
 
 		_bindToShareJsDocEvents: () ->
-			@doc.on "error", (error) => @_onError error
+			@doc.on "error", (error, meta) => @_onError error, meta
 			@doc.on "externalUpdate", () => @trigger "externalUpdate"
 			@doc.on "remoteop", () => @trigger "remoteop"
 			@doc.on "op:sent", () => @trigger "op:sent"
 			@doc.on "op:acknowledged", () => @trigger "op:acknowledged"
 
-		_onError: (error) ->
-			console.error "ShareJS error", error
-			ga('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
+		_onError: (error, meta = {}) ->
+			console.error "ShareJS error", error, meta
+			ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
 			@ide.socket.disconnect()
+			meta.doc_id = @doc_id
+			@ide.reportError(error, meta)
 			@doc?.clearInflightAndPendingOps()
 			@_cleanUp()
 			@trigger "error", error

+ 1 - 1
services/web/public/coffee/editor/Editor.coffee

@@ -121,7 +121,7 @@ define [
 			@aceEditor = aceEditor = AceEditor.edit("editor")
 
 			@on "resize", => @aceEditor.resize()
-			@ide.layoutManager.on "resize", => @aceEditor.resize()
+			@ide.layoutManager.on "resize", => @trigger "resize"
 
 			mode = window.userSettings.mode
 			theme = window.userSettings.theme

+ 6 - 3
services/web/public/coffee/editor/ShareJsDoc.coffee

@@ -94,14 +94,17 @@ define [
 	
 		INFLIGHT_OP_TIMEOUT: 10000
 		_startInflightOpTimeout: (update) ->
+			meta =
+				v: update.v
+				op_sent_at: new Date()
 			timer = setTimeout () =>
-				@_handleError "Doc op was not acknowledged in time"
+				@_handleError new Error("Doc op was not acknowledged in time"), meta
 			, @INFLIGHT_OP_TIMEOUT
 			@_doc.inflightCallbacks.push () =>
 				clearTimeout timer
 
-		_handleError: (error) ->
-			@trigger "error", error
+		_handleError: (error, meta = {}) ->
+			@trigger "error", error, meta
 
 		_bindToDocChanges: (doc) ->
 			submitOp = doc.submitOp

+ 34 - 50
services/web/public/coffee/ide.coffee

@@ -48,7 +48,7 @@ define [
 	SearchManager,
 	Project,
 	User,
-	StandaloneModal,
+	Modal,
 	FileTreeManager,
 	MessageManager,
 	HelpManager,
@@ -144,16 +144,39 @@ define [
 				setTimeout(joinProject, 100)
 	
 		showErrorModal: (title, message)->
-			modalOptions =
-				templateId:'genericModalTemplate'
-				isStatic: false
+			new Modal {
 				title: title
-				message:message
-			new Modal modalOptions
-
-		showGenericServerErrorMessage: (message)->
-			new Modal
-				templateId : "genericServerErrorModal"
+				message: message
+				buttons: [ text: "OK" ]
+			}
+
+		showGenericServerErrorMessage: ()->
+			new Modal {
+				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) ->
 			$("#loadingMessage").text(message)
@@ -171,45 +194,6 @@ define [
 	ide.layoutManager.resizeAllSplitters()
 	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 =
 		$savingArea : $('#saving-area')
 		timeOut: undefined
@@ -220,7 +204,7 @@ define [
 			return if @timeOut?
 			@clearTimeout()
 			@timeOut = setTimeout((=>
-				ga('send', 'event', 'editor-interaction', 'notification-shown', "saving")
+				ga?('send', 'event', 'editor-interaction', 'notification-shown', "saving")
 				$("#savingProblems").show()
 			), 1000)
 

+ 4 - 0
services/web/public/coffee/ide/ConnectionManager.coffee

@@ -8,11 +8,15 @@ define [
 			@socket = @ide.socket
 			@socket.on "connect", () =>
 				@connected = true
+				@secondLastConnected = @lastConnected
+				@lastConnected = new Date()
 				@hideModal()
 				@cancelReconnect()
 
 			@socket.on 'disconnect', () =>
 				@connected = false
+				@secondLastDisconnected = @lastDisconnected
+				@lastDisconnected = new Date()
 				@ide.trigger "disconnect"
 				setTimeout(=>
 					ga('send', 'event', 'editor-interaction', 'disconnect')

+ 25 - 14
services/web/public/coffee/project-members/ProjectMembersManager.coffee

@@ -3,9 +3,10 @@ define [
 	"models/ProjectMemberList"
 	"account/AccountManager"
 	"utils/Modal"
+	"moment"
 	"libs/backbone"
 	"libs/mustache"
-], (User, ProjectMemberList, AccountManager, Modal) ->
+], (User, ProjectMemberList, AccountManager, Modal, moment) ->
 	INFINITE_COLLABORATORS = -1
 
 	class ProjectMembersManager
@@ -20,6 +21,8 @@ define [
 					name: "Share"
 					content : $(@templates.userPanel)
 					lock: true
+					onShown: () =>
+						@publishProjectView?.refreshPublishStatus()
 
 			setupPublish = _.once =>
 				@publishProjectView = new PublishProjectView
@@ -177,25 +180,32 @@ define [
 		initialize: () ->
 			@ide = @options.ide
 			@model = @ide.project
-			_.bindAll(this, "render");
-			this.model.bind('change', this.render)
-			@refreshPublishStatus()
-			
+			@render()
 
 		render: ->
 			viewModel = 
 				description: @model.get("description")
 				canonicalUrl: @model.get("template.canonicalUrl")
 				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))
-			@publishedArea = $('#publishedAsTemplateArea')
-			@unpublishedArea = $('#unpublishedAsTemplateArea')
+			@$el.html $(Mustache.to_html(@template, viewModel))
+			@publishedArea = $('.show-when-published')
+			@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: ->
+			@showWorking()
 			@ide.socket.emit "getPublishedDetails", @ide.user.get("id"), (err, details)=>
+				@hideWorking()
 				if err?
 					return @showError()
 
@@ -203,16 +213,17 @@ define [
 				if details.exists
 					@model.set("template.canonicalUrl", details.canonicalUrl)
 					@model.set("template.publishedDate", details.publishedDate)
-					@publishedArea.show()
-					@unpublishedArea.hide()
-				else
-					@publishedArea.hide()
-					@unpublishedArea.show()
+
+				@refreshView()
 
 		showError: ->
+			@publishedArea.hide()
+			@unpublishedArea.hide()
 			$('#problemWithPublishingArea').show()
 
 		showWorking: ->
+			@publishedArea.hide()
+			@unpublishedArea.hide()
 			$('#publishWorkingArea').show()
 
 		hideWorking: ->

+ 47 - 4
services/web/public/coffee/track-changes/DiffView.coffee

@@ -16,11 +16,11 @@ define [
 			@createAceEditor()
 			@aceEditor.setValue(@getPlainDiffContent())
 			@aceEditor.clearSelection()
-			session = @aceEditor.getSession()
-			session.setMode(new LatexMode.Mode())
-			session.setUseWrapMode(true)
+			@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
 			@insertMarkers()
 			@insertNameTag()
+			@insertMoreChangeLabels()
+			@scrollToFirstChange()
 			return @
 
 		destroy: () ->
@@ -34,12 +34,18 @@ define [
 			@aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
 			@aceEditor.setReadOnly true
 			@aceEditor.setShowPrintMargin(false)
+			session = @aceEditor.getSession()
+			session.setMode(new LatexMode.Mode())
+			session.setUseWrapMode(true)
 
 			@aceEditor.on "mousemove", (e) =>
 				position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
 				e.position = position
 				@updateVisibleNames(e)
 
+			session.on "changeScrollTop", (e) =>
+				@updateMoreChangeLabels()
+
 		getPlainDiffContent: () ->
 			content = ""
 			for entry in @model.get("diff") or []
@@ -102,7 +108,6 @@ define [
 			, foreground
 
 		insertNameTag: () ->
-			@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
 			@$nameTagEl = $("<div class='change-name-marker'></div>")
 			@$nameTagEl.css({
 				position: "absolute"
@@ -110,6 +115,19 @@ define [
 			@$nameTagEl.hide()
 			@$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) ->
 			@$nameTagEl.show()
 			
@@ -161,5 +179,30 @@ define [
 			if !visibleName
 				@_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
 

+ 3 - 0
services/web/public/coffee/track-changes/TrackChangesManager.coffee

@@ -19,6 +19,9 @@ define [
 			@ide.editor.on "change:doc", () =>
 				@hideEl()
 
+			@ide.editor.on "resize", () =>
+				@diffView?.resize()
+
 			@$el.find(".track-changes-close").on "click", (e) =>
 				e.preventDefault
 				@hide()

+ 12 - 0
services/web/public/stylesheets/less/editor.less

@@ -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 {
   color: #ffffff;
   text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);

+ 21 - 1
services/web/public/stylesheets/less/trackchanges.less

@@ -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;
 		z-index: 2;
 	}
@@ -71,6 +76,21 @@
 		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 {
 		li {
 			position: relative;

+ 1 - 1
services/web/test/UnitTests/coffee/Editor/EditorUpdatesControllerTests.coffee

@@ -12,7 +12,7 @@ describe "EditorUpdatesController", ->
 		@client = new MockClient()
 		@callback = sinon.stub()
 		@EditorUpdatesController = SandboxedModule.require modulePath, requires:
-			"logger-sharelatex": @logger = { error: sinon.stub() }
+			"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub() }
 			"./EditorRealTimeController" : @EditorRealTimeController = {}
 			"../DocumentUpdater/DocumentUpdaterHandler" : @DocumentUpdaterHandler = {}
 			"../Versioning/AutomaticSnapshotManager" : @AutomaticSnapshotManager = {}

+ 21 - 20
services/web/test/UnitTests/coffee/Email/EmailSenderTests.coffee

@@ -12,20 +12,21 @@ describe "Email", ->
 
 		@settings =
 			email:
-				ses:
-					key: "key"
-					secret: "secret"
+				transport: "ses"
+				parameters:
+					AWSAccessKeyID: "key"
+					AWSSecretKey: "secret"
 				fromAddress: "bob@bob.com"
 				replyToAddress: "sally@gmail.com"
 
-		@sesClient = 
-			sendemail: sinon.stub()
-		@ses = 
-			createClient: => @sesClient
+		@sesClient =
+			sendMail: sinon.stub()
+		@ses =
+			createTransport: => @sesClient
 		@sender = SandboxedModule.require modulePath, requires:
-			'node-ses': @ses
+			'nodemailer': @ses
 			"settings-sharelatex":@settings
-			"logger-sharelatex": 
+			"logger-sharelatex":
 				log:->
 				warn:->
 				err:->
@@ -38,44 +39,44 @@ describe "Email", ->
 	describe "sendEmail", ->
 
 		it "should set the properties on the email to send", (done)->
-			@sesClient.sendemail.callsArgWith(1)
+			@sesClient.sendMail.callsArgWith(1)
 
 			@sender.sendEmail @opts, =>
-				args = @sesClient.sendemail.args[0][0]
-				args.message.should.equal @opts.html
+				args = @sesClient.sendMail.args[0][0]
+				args.html.should.equal @opts.html
 				args.to.should.equal @opts.to
 				args.subject.should.equal @opts.subject
 				done()
 
-		it "should return the error", (done)->	
-			@sesClient.sendemail.callsArgWith(1, "error")
+		it "should return the error", (done)->
+			@sesClient.sendMail.callsArgWith(1, "error")
 			@sender.sendEmail {}, (err)=>
 				err.should.equal "error"
 				done()
 
 
 		it "should use the from address from settings", (done)->
-			@sesClient.sendemail.callsArgWith(1)
+			@sesClient.sendMail.callsArgWith(1)
 
 			@sender.sendEmail @opts, =>
-				args = @sesClient.sendemail.args[0][0]
+				args = @sesClient.sendMail.args[0][0]
 				args.from.should.equal @settings.email.fromAddress
 				done()
 
 		it "should use the reply to address from settings", (done)->
-			@sesClient.sendemail.callsArgWith(1)
+			@sesClient.sendMail.callsArgWith(1)
 
 			@sender.sendEmail @opts, =>
-				args = @sesClient.sendemail.args[0][0]
+				args = @sesClient.sendMail.args[0][0]
 				args.replyTo.should.equal @settings.email.replyToAddress
 				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"
 			@sender.sendEmail @opts, =>
-				args = @sesClient.sendemail.args[0][0]
+				args = @sesClient.sendMail.args[0][0]
 				args.replyTo.should.equal @opts.replyTo
 				done()