Просмотр исходного кода

Merge branch 'master' into master-redesign

Conflicts:
	public/stylesheets/less/trackchanges.less
James Allen 12 лет назад
Родитель
Сommit
b6217c401e
30 измененных файлов с 356 добавлено и 56 удалено
  1. 8 3
      services/web/app/coffee/Features/Compile/ClsiManager.coffee
  2. 3 1
      services/web/app/coffee/Features/Compile/CompileController.coffee
  3. 2 2
      services/web/app/coffee/Features/Compile/CompileManager.coffee
  4. 7 2
      services/web/app/coffee/Features/Docstore/DocstoreManager.coffee
  5. 21 0
      services/web/app/coffee/Features/Editor/EditorHttpController.coffee
  6. 1 0
      services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee
  7. 12 2
      services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
  8. 2 1
      services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee
  9. 3 0
      services/web/app/coffee/router.coffee
  10. 14 2
      services/web/app/views/templates.jade
  11. 29 0
      services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee
  12. 1 0
      services/web/public/coffee/file-tree/EntityView.coffee
  13. 29 3
      services/web/public/coffee/file-tree/FileTreeManager.coffee
  14. 1 0
      services/web/public/coffee/models/Doc.coffee
  15. 3 3
      services/web/public/coffee/models/Folder.coffee
  16. 11 1
      services/web/public/coffee/models/Project.coffee
  17. 6 1
      services/web/public/coffee/pdf/CompiledView.coffee
  18. 6 3
      services/web/public/coffee/pdf/PdfManager.coffee
  19. 32 18
      services/web/public/coffee/track-changes/DiffView.coffee
  20. 28 2
      services/web/public/coffee/track-changes/TrackChangesManager.coffee
  21. 1 1
      services/web/public/coffee/track-changes/models/Change.coffee
  22. 1 1
      services/web/public/coffee/track-changes/models/Diff.coffee
  23. 8 0
      services/web/public/stylesheets/app/editor.less
  24. 28 6
      services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee
  25. 2 2
      services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee
  26. 1 1
      services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee
  27. 16 0
      services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee
  28. 46 0
      services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee
  29. 8 0
      services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee
  30. 26 1
      services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee

+ 8 - 3
services/web/app/coffee/Features/Compile/ClsiManager.coffee

@@ -8,8 +8,8 @@ logger = require "logger-sharelatex"
 url = require("url")
 
 module.exports = ClsiManager =
-	sendRequest: (project_id, callback = (error, success) ->) ->
-		ClsiManager._buildRequest project_id, (error, req) ->
+	sendRequest: (project_id, settingsOverride = {}, callback = (error, success) ->) ->
+		ClsiManager._buildRequest project_id, settingsOverride, (error, req) ->
 			return callback(error) if error?
 			logger.log project_id: project_id, "sending compile to CLSI"
 			ClsiManager._postToClsi project_id, req, (error, response) ->
@@ -52,7 +52,7 @@ module.exports = ClsiManager =
 		return outputFiles
 
 	VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
-	_buildRequest: (project_id, callback = (error, request) ->) ->
+	_buildRequest: (project_id, settingsOverride={}, callback = (error, request) ->) ->
 		Project.findById project_id, {compiler: 1, rootDoc_id: 1}, (error, project) ->
 			return callback(error) if error?
 			return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
@@ -67,6 +67,7 @@ module.exports = ClsiManager =
 
 					resources = []
 					rootResourcePath = null
+					rootResourcePathOverride = null
 
 					for path, doc of docs
 						path = path.replace(/^\//, "") # Remove leading /
@@ -75,6 +76,10 @@ module.exports = ClsiManager =
 							content: doc.lines.join("\n")
 						if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString()
 							rootResourcePath = path
+						if settingsOverride.rootDoc_id? and doc._id.toString() == settingsOverride.rootDoc_id.toString()
+							rootResourcePathOverride = path
+
+					rootResourcePath = rootResourcePathOverride if rootResourcePathOverride?
 
 					for path, file of files
 						path = path.replace(/^\//, "") # Remove leading /

+ 3 - 1
services/web/app/coffee/Features/Compile/CompileController.coffee

@@ -11,9 +11,11 @@ module.exports = CompileController =
 	compile: (req, res, next = (error) ->) ->
 		project_id = req.params.Project_id
 		isAutoCompile = !!req.query?.auto_compile
+		settingsOverride = req.body?.settingsOverride ? {};
+		logger.log "root doc overriden" if settingsOverride.rootDoc_id?
 		AuthenticationController.getLoggedInUserId req, (error, user_id) ->
 			return next(error) if error?
-			CompileManager.compile project_id, user_id, { isAutoCompile }, (error, status, outputFiles) ->
+			CompileManager.compile project_id, user_id, { isAutoCompile, settingsOverride }, (error, status, outputFiles) ->
 				return next(error) if error?
 				res.contentType("application/json")
 				res.send 200, JSON.stringify {

+ 2 - 2
services/web/app/coffee/Features/Compile/CompileManager.coffee

@@ -25,12 +25,12 @@ module.exports = CompileManager =
 				return callback(error) if error?
 				if recentlyCompiled
 					return callback new Error("project was recently compiled so not continuing")
-
+				
 				CompileManager._ensureRootDocumentIsSet project_id, (error) ->
 					return callback(error) if error?
 					DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
 						return callback(error) if error?
-						ClsiManager.sendRequest project_id, (error, status, outputFiles) ->
+						ClsiManager.sendRequest project_id, opt.settingsOverride, (error, status, outputFiles) ->
 							return callback(error) if error?
 							logger.log files: outputFiles, "output files"
 							callback(null, status, outputFiles)

+ 7 - 2
services/web/app/coffee/Features/Docstore/DocstoreManager.coffee

@@ -30,9 +30,14 @@ module.exports = DocstoreManager =
 				logger.error err: error, project_id: project_id, "error getting all docs from docstore"
 				callback(error)
 
-	getDoc: (project_id, doc_id, callback = (error, lines, rev) ->) ->
-		logger.log project_id: project_id, doc_id: doc_id, "getting doc in docstore api"
+	getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) ->
+		if typeof(options) == "function"
+			callback = options
+			options = {}
+		logger.log project_id: project_id, doc_id: doc_id, options: options, "getting doc in docstore api"
 		url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
+		if options.include_deleted
+			url += "?include_deleted=true"
 		request.get {
 			url: url
 			json: true

+ 21 - 0
services/web/app/coffee/Features/Editor/EditorHttpController.coffee

@@ -0,0 +1,21 @@
+ProjectEntityHandler = require "../Project/ProjectEntityHandler"
+logger = require "logger-sharelatex"
+EditorRealTimeController = require "./EditorRealTimeController"
+
+module.exports = EditorHttpController =
+	restoreDoc: (req, res, next) ->
+		project_id = req.params.Project_id
+		doc_id = req.params.doc_id
+		name = req.body.name
+
+		if !name?
+			return res.send 400 # Malformed request
+
+		logger.log project_id: project_id, doc_id: doc_id, "restoring doc"
+		ProjectEntityHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) =>
+			return next(error) if error?
+			EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc)
+			res.json {
+				doc_id: doc._id
+			}
+

+ 1 - 0
services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee

@@ -15,6 +15,7 @@ module.exports = ProjectEditorHandler =
 			description: project.description
 			spellCheckLanguage: project.spellCheckLanguage
 			deletedByExternalDataSource : project.deletedByExternalDataSource || false
+			deletedDocs: project.deletedDocs
 
 		if options.includeUsers
 			result.features =

+ 12 - 2
services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee

@@ -111,8 +111,11 @@ module.exports = ProjectEntityHandler =
 		logger.log sl_req_id: sl_req_id, project_id: project_id, "removing root doc"
 		Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
 
-	getDoc: (project_id, doc_id, callback = (error, lines, rev) ->) ->
-		DocstoreManager.getDoc project_id, doc_id, callback
+	getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev) ->) ->
+		if typeof(options) == "function"
+			callback = options
+			options = {}
+		DocstoreManager.getDoc project_id, doc_id, options, callback
 
 	addDoc: (project_or_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc, folder_id) ->)=>
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
@@ -135,6 +138,13 @@ module.exports = ProjectEntityHandler =
 							return callback(err) if err?
 							callback(null, doc, folder_id)
 
+	restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
+		# getDoc will return the deleted doc's lines, but we don't actually remove
+		# the deleted doc, just create a new one from its lines.
+		ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
+			return callback(error) if error?
+			ProjectEntityHandler.addDoc project_id, null, name, lines, callback
+
 	addFile: (project_or_id, folder_id, fileName, path, sl_req_id, callback = (error, fileRef, folder_id) ->)->
 		{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
 		Project.getProject project_or_id, "", (err, project) ->

+ 2 - 1
services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee

@@ -10,7 +10,8 @@ module.exports = ProjectRootDocManager =
 			root_doc_id = null
 			for path, doc of docs
 				for line in doc.lines || []
-					if Path.extname(path).match(/\.R?tex$/) and line.match(/\\documentclass/)
+					match = line.match /(.*)\\documentclass/ # no lookbehind in js regexp :(
+					if Path.extname(path).match(/\.R?tex$/) and match and !match[1].match /%/
 						root_doc_id = doc._id
 			if root_doc_id?
 				ProjectEntityHandler.setRootDoc project_id, root_doc_id, sl_req_id, callback

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

@@ -7,6 +7,7 @@ SpellingController = require('./Features/Spelling/SpellingController')
 SecurityManager = require('./managers/SecurityManager')
 AuthorizationManager = require('./Features/Security/AuthorizationManager')
 EditorController = require("./Features/Editor/EditorController")
+EditorHttpController = require("./Features/Editor/EditorHttpController")
 EditorUpdatesController = require("./Features/Editor/EditorUpdatesController")
 Settings = require('settings-sharelatex')
 TpdsController = require('./Features/ThirdPartyDataStore/TpdsController')
@@ -125,6 +126,8 @@ module.exports = class Router
 		app.get  "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
 		app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
 
+		app.post "/project/:Project_id/doc/:doc_id/restore", SecurityManager.requestCanAccessProject, EditorHttpController.restoreDoc
+
 		app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
 		app.get  '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
 

+ 14 - 2
services/web/app/views/templates.jade

@@ -82,6 +82,12 @@
 			.dropdown-caret
 				i.icon-chevron-down
 			.entity-label.label.label-success
+
+	script(type="text/template")#deletedDocsFolderTemplate
+		.entity-list-item.entity-deleted-docs-folder(class="entity-{{ type }}", entity-type="{{ type }}", id="{{ id }}")
+			.clickable.js-clickable
+				span.name {{ name }}
+			.entity-label.label.label-success
 	
 	script(type="text/template")#entityListTemplate
 		.contents
@@ -386,8 +392,14 @@
 
 	script(type='text/template')#trackChangesDiffTemplate
 		.track-changes-diff-toolbar.btn-toolbar
-			.number-of-changes {{ changes }} in <strong>{{ name }}</strong>
-			a(href="#").restore.btn.btn-small.btn-danger Restore to before these changes
+			.change-info
+				.number-of-changes {{ changes }} in <strong>{{ name }}</strong>
+				a(href="#").restore.btn.btn-small.btn-danger Restore to before these changes
+			.deleted-info(style="display:none;")
+				strong {{ name }}
+				.controls
+					span This file has been deleted 
+					a(href="#").restore-deleted.btn.btn-small.btn-success Restore
 		.track-changes-diff-editor
 
 	script(type='text/template')#changeListItemTemplate

+ 29 - 0
services/web/public/coffee/file-tree/DeletedDocsFolderView.coffee

@@ -0,0 +1,29 @@
+define [
+	"file-tree/FolderView"
+], (FolderView) ->
+	DeletedDocsFolderView = FolderView.extend
+		template: $("#deletedDocsFolderTemplate").html()
+
+		render: () ->
+			@$el.append(Mustache.to_html @template, @model.attributes)
+			@_bindToDomElements()
+			@hideRenameBox()
+			@hideToggle()
+			@renderEntries()
+			@showEntries()
+			return @
+
+		onClick: () ->
+			e.preventDefault()
+
+		onToggle: () ->
+			e.preventDefault()
+
+		getContextMenuEntries: () -> null
+
+		hideToggle: () ->
+			@$(".js-toggle").hide()
+
+		
+
+

+ 1 - 0
services/web/public/coffee/file-tree/EntityView.coffee

@@ -112,6 +112,7 @@ define [
 
 		showContextMenu: (position) ->
 			entries = @getContextMenuEntries()
+			return if !entries?
 
 			@manager.trigger "contextmenu:beforeshow", @model, entries
 

+ 29 - 3
services/web/public/coffee/file-tree/FileTreeManager.coffee

@@ -3,11 +3,13 @@ define [
 	"models/File"
 	"models/Folder"
 	"file-tree/FileTreeView"
+	"file-tree/FolderView"
+	"file-tree/DeletedDocsFolderView"
 	"utils/Effects"
 	"utils/Modal"
 	"libs/backbone"
 	"libs/jquery.storage"
-], (Doc, File, Folder, FileTreeView, Effects, Modal) ->
+], (Doc, File, Folder, FileTreeView, FolderView, DeletedDocsFolderView, Effects, Modal) ->
 	class FileTreeManager
 		constructor: (@ide) ->
 			_.extend(@, Backbone.Events)
@@ -37,6 +39,13 @@ define [
 
 		populateFileTree: () ->
 			@view.bindToRootFolder(@project.get("rootFolder"))
+			
+			if @deletedDocsView?
+				@deletedDocsView.$el.remove()
+			@deletedDocsView = new DeletedDocsFolderView(model: @project.get("deletedDocs"), manager: @)
+			@deletedDocsView.render()
+			$("#sections").append(@deletedDocsView.$el)
+			@hideDeletedDocs()
 
 		listenForUpdates: () ->
 			@ide.socket.on 'reciveNewDoc', (folder_id, doc) =>
@@ -100,8 +109,12 @@ define [
 			@ide.sideBarView.deselectAll()
 			@views[entity_id]?.select()
 
-		getEntity: (entity_id) ->
-			@views[entity_id]?.model
+		getEntity: (entity_id, options = {include_deleted: false}) ->
+			model = @views[entity_id]?.model
+			if !model? or (model.get("deleted") and !options.include_deleted)
+				return
+			else
+				return model
 
 		getSelectedEntity: () -> @getEntity(@selected_entity_id)
 		getSelectedEntityId: () -> @getSelectedEntity()?.id
@@ -283,6 +296,19 @@ define [
 			entity.set("deleted", true)
 			entity.collection?.remove(entity)
 			delete @views[entity_id]
+
+			# Do this after the remove so that it's never in two places at once 
+			# and so that it doesn't get reset by deleting from @views
+			if entity.get("type") == "doc"
+				@project.get("deletedDocs").get("children").add entity
 			
 		setLabels: (labels) ->
 			@view.setLabels(labels)
+			@deletedDocsView.setLabels(labels)
+
+		showDeletedDocs: () ->
+			if @project.get("deletedDocs").get("children").length > 0
+				@deletedDocsView.$el.show()
+
+		hideDeletedDocs: () ->
+			@deletedDocsView.$el.hide()

+ 1 - 0
services/web/public/coffee/models/Doc.coffee

@@ -9,3 +9,4 @@ define [
 			attributes =
 				id: rawAttributes._id
 				name: rawAttributes.name
+				deleted: !!rawAttributes.deleted

+ 3 - 3
services/web/public/coffee/models/Folder.coffee

@@ -20,11 +20,11 @@ define [
 				id: rawAttributes._id
 				name: rawAttributes.name
 			children = []
-			for childFolder in rawAttributes.folders
+			for childFolder in rawAttributes.folders or []
 				children.push new Folder(childFolder, parse: true)
-			for file in rawAttributes.fileRefs
+			for file in rawAttributes.fileRefs or []
 				children.push new File(file, parse: true)
-			for doc in rawAttributes.docs
+			for doc in rawAttributes.docs or []
 				children.push new Doc(doc, parse: true)
 			attributes.children = new FolderChildren(children)
 			return attributes

+ 11 - 1
services/web/public/coffee/models/Project.coffee

@@ -2,8 +2,9 @@ define [
 	"models/User"
 	"models/ProjectMemberList"
 	"models/Folder"
+	"models/Doc"
 	"libs/backbone"
-], (User, ProjectMemberList, Folder) ->
+], (User, ProjectMemberList, Folder, Doc) ->
 	Project = Backbone.Model.extend
 		initialize: ->
 			@on "change:ide", (project, ide) =>
@@ -39,6 +40,15 @@ define [
 				member = User.findOrBuild rawMember._id, rawMember
 				members.add member
 
+			for doc in rawAttributes.deletedDocs
+				doc.deleted = true
+
+			attributes.deletedDocs = new Folder({
+				_id: "deleted-docs-folder"
+				name: "Deleted documents"
+				docs: rawAttributes.deletedDocs
+			}, parse: true)
+
 			return attributes
 	
 		bindToRootDocId: ->

+ 6 - 1
services/web/public/coffee/pdf/CompiledView.coffee

@@ -204,7 +204,12 @@ define [
 
 		recompilePdf: () ->
 			@options.manager.trigger "compile:pdf"
-			@options.manager.refreshPdf()
+			rootDocOverride_id = null
+			for line in @ide.editor.getLines()
+				match = line.match /(.*)\\documentclass/
+				if match and !match[1].match /%/
+					rootDocOverride_id = @ide.editor.getCurrentDocId()
+			@options.manager.refreshPdf {rootDocOverride_id}
 
 		toggleFlatViewButton: () -> @$("#flatViewButton").button("toggle")
 		toggleSplitViewButton: () -> @$("#splitViewButton").button("toggle")

+ 6 - 3
services/web/public/coffee/pdf/PdfManager.coffee

@@ -143,7 +143,7 @@ define [
 				@view.onCompiling()
 				@syncButtonsView?.hide()
 				@compiling = true
-				@_doCompile opts.isAutoCompile, (error, status, outputFiles) =>
+				@_doCompile opts, (error, status, outputFiles) =>
 					@compiling = false
 					doneCompiling()
 
@@ -172,16 +172,19 @@ define [
 					if outputFiles?
 						@view.showOutputFileDownloadLinks(outputFiles)
 
-		_doCompile: (isAutoCompile, callback = (error, status, outputFiles) ->) ->
+		_doCompile: (opts, callback = (error, status, outputFiles) ->) ->
 			url = "/project/#{@ide.project_id}/compile"
-			if isAutoCompile
+			if opts.isAutoCompile
 				url += "?auto_compile=true"
 			$.ajax(
 				url: url
 				type: "POST"
 				headers:
 					"X-CSRF-Token": window.csrfToken
+				contentType: "application/json; charset=utf-8"
 				dataType: 'json'
+				data: JSON.stringify settingsOverride: 
+					rootDoc_id: opts.rootDocOverride_id ? null
 				success: (body, status, response) ->
 					callback null, body.status, body.outputFiles
 				error: (error) ->

+ 32 - 18
services/web/public/coffee/track-changes/DiffView.coffee

@@ -10,17 +10,23 @@ define [
 		template: $("#trackChangesDiffTemplate").html()
 
 		events:
-			"click .restore": () ->
-				console.log "click"
+			"click .restore": (e) ->
+				e.preventDefault()
 				@trigger "restore"
+			"click .restore-deleted": (e) ->
+				e.preventDefault()
+				@$("a.restore-deleted").attr("disabled", true)
+				@$("a.restore-deleted").text("Restoring...")
+				@trigger "restore-deleted"
 
 		initialize: () ->
-			@model.on "change:diff", () => @render()
+			if !@model.get("doc").get("deleted")
+				@model.on "change:diff", () => @render()
+				@model.fetch()
+			else
+				@render()
 
 		render: ->
-			diff = @model.get("diff")
-			return unless diff?
-
 			changes = @getNumberOfChanges()
 			html = Mustache.to_html @template, {
 				changes: "#{changes} change#{if changes == 1 then "" else "s"}"
@@ -28,18 +34,26 @@ define [
 			}
 			@$el.html(html)
 
-			if !@model.get("from")? or !@model.get("to")? or changes == 0
-				@$(".restore").hide()
-
-			@createAceEditor()
-			@aceEditor.setValue(@getPlainDiffContent())
-			@aceEditor.clearSelection()
-			@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
-			@insertMarkers()
-			@insertNameTag()
-			@insertMoreChangeLabels()
-			@bindToScrollEvents()
-			@scrollToFirstChange()
+			if @model.get("doc").get("deleted")
+				@$(".change-info").hide()
+				@$(".deleted-info").show()
+			else
+				diff = @model.get("diff")
+				return unless diff?
+
+				if !@model.get("from")? or !@model.get("to")? or changes == 0
+					@$(".restore").hide()
+
+				@createAceEditor()
+				@aceEditor.setValue(@getPlainDiffContent())
+				@aceEditor.clearSelection()
+				@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
+				@insertMarkers()
+				@insertNameTag()
+				@insertMoreChangeLabels()
+				@bindToScrollEvents()
+				@scrollToFirstChange()
+
 			return @
 
 		remove: () ->

+ 28 - 2
services/web/public/coffee/track-changes/TrackChangesManager.coffee

@@ -71,6 +71,9 @@ define [
 			@ide.mainAreaManager.change "trackChanges"
 			@ide.editor.disable()
 			@ide.fileViewManager.disable()
+
+			@ide.fileTreeManager.showDeletedDocs()
+
 			@enable()
 
 		showUpgradeView: () ->
@@ -93,6 +96,7 @@ define [
 			@ide.fileTreeManager.openDoc(@doc_id)
 			@ide.tabManager.show "code"
 			@resetLabels()
+			@ide.fileTreeManager.hideDeletedDocs()
 
 		autoSelectDiff: () ->
 			if @changes.models.length == 0
@@ -160,7 +164,7 @@ define [
 				@diffView.remove()
 
 			if !@diff.get("doc")?
-				console.log "This document has been deleted. What should we do?"
+				console.log "This document does not exist. What should we do?"
 				return
 
 			@diffView = new DiffView(
@@ -171,7 +175,14 @@ define [
 			@diffView.on "restore", () =>
 				@restoreDiff(@diff)
 
-			@diff.fetch()
+			@diffView.on "restore-deleted", () =>
+				@restoreDeletedDoc @diff.get("doc"), (error, doc_id) =>
+					return if error? or !doc_id?
+					setTimeout () =>
+						# Give doc a chance to appear in file tree via socket.io
+						@hide()
+						@ide.fileTreeManager.openDoc(doc_id)
+					, 1000
 
 			@ide.fileTreeManager.selectEntity(@doc_id)
 
@@ -217,6 +228,21 @@ define [
 				}]
 			})
 
+		restoreDeletedDoc: (doc, callback) ->
+			$.ajax {
+				url: "/project/#{@project_id}/doc/#{doc.get("id")}/restore"
+				type: "POST"
+				dataType: "json"
+				data:
+					name: doc.get("name")
+				headers:
+					"X-CSRF-Token": window.csrfToken
+				success: (body, status, response) ->
+					callback(null, body?.doc_id)
+				error: (error) ->
+					callback(error)
+			}
+
 		enable: () ->
 			@enabled = true
 

+ 1 - 1
services/web/public/coffee/track-changes/models/Change.coffee

@@ -21,6 +21,6 @@ define [
 					toV: data.toV
 					# TODO: We should not use a global reference here, but 
 					# it's hard to get @ide into Backbone at this point.
-					entity: ide.fileTreeManager.getEntity(doc_id)
+					entity: ide.fileTreeManager.getEntity(doc_id, include_deleted: true)
 
 			return model

+ 1 - 1
services/web/public/coffee/track-changes/models/Diff.coffee

@@ -5,7 +5,7 @@ define [
 	Diff = Backbone.Model.extend
 		initialize: (attributes, options) ->
 			@ide = options.ide
-			@set "doc", @ide.fileTreeManager.getEntity(@get("doc_id"))
+			@set "doc", @ide.fileTreeManager.getEntity(@get("doc_id"), include_deleted: true)
 
 		url: () ->
 			url = "/project/#{@get("project_id")}/doc/#{@get("doc_id")}/diff"

+ 8 - 0
services/web/public/stylesheets/app/editor.less

@@ -414,6 +414,14 @@ body.editor {
 		}
 	}
 
+	.entity-deleted-docs-folder {
+		margin-top: 16px;
+		span.name {
+			padding: 6px;
+			border-bottom: 1px solid #ccc;
+		}
+	}
+
 	.entity-list {
 		padding-left: 0px;
 	}

+ 28 - 6
services/web/test/UnitTests/coffee/Compile/ClsiManagerTests.coffee

@@ -24,7 +24,7 @@ describe "ClsiManager", ->
 
 	describe "sendRequest", ->
 		beforeEach ->
-			@ClsiManager._buildRequest = sinon.stub().callsArgWith(1, null, @request = "mock-request")
+			@ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @request = "mock-request")
 
 		describe "with a successful compile", ->
 			beforeEach ->
@@ -39,7 +39,7 @@ describe "ClsiManager", ->
 							type: "log"
 						}]
 				})
-				@ClsiManager.sendRequest @project_id, @callback
+				@ClsiManager.sendRequest @project_id, {}, @callback
 
 			it "should build the request", ->
 				@ClsiManager._buildRequest
@@ -67,7 +67,7 @@ describe "ClsiManager", ->
 					compile:
 						status: @status = "failure"
 				})
-				@ClsiManager.sendRequest @project_id, @callback
+				@ClsiManager.sendRequest @project_id, {}, @callback
 			
 			it "should call the callback with a failure statue", ->
 				@callback.calledWith(null, @status).should.equal true
@@ -121,7 +121,7 @@ describe "ClsiManager", ->
 
 		describe "with a valid project", ->
 			beforeEach (done) ->
-				@ClsiManager._buildRequest @project_id, (error, request) =>
+				@ClsiManager._buildRequest @project_id, null, (error, request) =>
 					@request = request
 					done()
 
@@ -159,10 +159,32 @@ describe "ClsiManager", ->
 						}]
 				)
 
+
+		describe "when root doc override is valid", ->
+			beforeEach (done) ->
+				@ClsiManager._buildRequest @project_id, {rootDoc_id:"mock-doc-id-2"}, (error, request) =>
+					@request = request
+					done()
+
+			it "should change root path", ->
+				@request.compile.rootResourcePath.should.equal "chapters/chapter1.tex"
+
+
+		describe "when root doc override is invalid", ->
+			beforeEach (done) ->
+				@ClsiManager._buildRequest @project_id, {rootDoc_id:"invalid-id"}, (error, request) =>
+					@request = request
+					done()
+
+			it "should fallback to default root doc", ->
+				@request.compile.rootResourcePath.should.equal "main.tex"
+
+
+
 		describe "when the project has an invalid compiler", ->
 			beforeEach (done) ->
 				@project.compiler = "context"
-				@ClsiManager._buildRequest @project, (error, request) =>
+				@ClsiManager._buildRequest @project, null, (error, request) =>
 					@request = request
 					done()
 
@@ -172,7 +194,7 @@ describe "ClsiManager", ->
 		describe "when there is no valid root document", ->
 			beforeEach (done) ->
 				@project.rootDoc_id = "not-valid"
-				@ClsiManager._buildRequest @project, (@error, @request) =>
+				@ClsiManager._buildRequest @project, null, (@error, @request) =>
 					done()
 			
 			it "should return an error", ->

+ 2 - 2
services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee

@@ -44,7 +44,7 @@ describe "CompileController", ->
 
 			it "should do the compile without the auto compile flag", ->
 				@CompileManager.compile
-					.calledWith(@project_id, @user_id, { isAutoCompile: false })
+					.calledWith(@project_id, @user_id, { isAutoCompile: false, settingsOverride:{} })
 					.should.equal true
 
 			it "should set the content-type of the response to application/json", ->
@@ -71,7 +71,7 @@ describe "CompileController", ->
 
 			it "should do the compile with the auto compile flag", ->
 				@CompileManager.compile
-					.calledWith(@project_id, @user_id, { isAutoCompile: true })
+					.calledWith(@project_id, @user_id, { isAutoCompile: true, settingsOverride:{} })
 					.should.equal true
 
 	describe "downloadPdf", ->

+ 1 - 1
services/web/test/UnitTests/coffee/Compile/CompileManagerTests.coffee

@@ -36,7 +36,7 @@ describe "CompileManager", ->
 			@CompileManager._checkIfRecentlyCompiled = sinon.stub().callsArgWith(2, null, false)
 			@CompileManager._ensureRootDocumentIsSet = sinon.stub().callsArgWith(1, null)
 			@DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
-			@ClsiManager.sendRequest = sinon.stub().callsArgWith(1, null, @status = "mock-status")
+			@ClsiManager.sendRequest = sinon.stub().callsArgWith(2, null, @status = "mock-status")
 
 		describe "succesfully", ->
 			beforeEach ->

+ 16 - 0
services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee

@@ -131,6 +131,22 @@ describe "DocstoreManager", ->
 					}, "error getting doc from docstore")
 					.should.equal true
 
+		describe "with include_deleted=true", ->
+			beforeEach ->
+				@request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @doc)
+				@DocstoreManager.getDoc @project_id, @doc_id, include_deleted: true, @callback
+
+			it "should get the doc from the docstore api (including deleted)", ->
+				@request.get
+					.calledWith({
+						url: "#{@settings.apis.docstore.url}/project/#{@project_id}/doc/#{@doc_id}?include_deleted=true"
+						json: true
+					})
+					.should.equal true
+
+			it "should call the callback with the lines, version and rev", ->
+				@callback.calledWith(null, @lines, @rev).should.equal true
+
 	describe "getAllDocs", ->
 		describe "with a successful response code", ->
 			beforeEach ->

+ 46 - 0
services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee

@@ -0,0 +1,46 @@
+SandboxedModule = require('sandboxed-module')
+sinon = require('sinon')
+require('chai').should()
+modulePath = require('path').join __dirname, '../../../../app/js/Features/Editor/EditorHttpController'
+
+describe "EditorHttpController", ->
+	beforeEach ->
+		@EditorHttpController = SandboxedModule.require modulePath, requires:
+			'../Project/ProjectEntityHandler' : @ProjectEntityHandler = {}
+			"./EditorRealTimeController": @EditorRealTimeController = {}
+			"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
+		@project_id = "mock-project-id"
+		@doc_id = "mock-doc-id"
+		@req = {}
+		@res =
+			send: sinon.stub()
+			json: sinon.stub()
+
+	describe "restoreDoc", ->
+		beforeEach ->
+			@req.params =
+				Project_id: @project_id
+				doc_id: @doc_id
+			@req.body =
+				name: @name = "doc-name"
+			@ProjectEntityHandler.restoreDoc = sinon.stub().callsArgWith(3, null,
+				@doc = { "mock": "doc", _id: @new_doc_id = "new-doc-id" }
+				@folder_id = "mock-folder-id"
+			)
+			@EditorRealTimeController.emitToRoom = sinon.stub()
+			@EditorHttpController.restoreDoc @req, @res
+
+		it "should restore the doc", ->
+			@ProjectEntityHandler.restoreDoc
+				.calledWith(@project_id, @doc_id, @name)
+				.should.equal true
+
+		it "should the real-time clients about the new doc", ->
+			@EditorRealTimeController.emitToRoom
+				.calledWith(@project_id, 'reciveNewDoc', @folder_id, @doc)
+				.should.equal true
+
+		it "should return the new doc id", ->
+			@res.json
+				.calledWith(doc_id: @new_doc_id)
+				.should.equal true

+ 8 - 0
services/web/test/UnitTests/coffee/Project/ProjectEditorHandlerTests.coffee

@@ -55,6 +55,10 @@ describe "ProjectEditorHandler", ->
 				last_name  : "Write"
 				email      : "read-write@sharelatex.com"
 			}]
+			deletedDocs: [{
+				_id: "deleted-doc-id"
+				name: "main.tex"
+			}]
 		@handler = SandboxedModule.require modulePath
 
 	describe "buildProjectModelView", ->
@@ -85,6 +89,10 @@ describe "ProjectEditorHandler", ->
 				@result.owner.first_name.should.equal "Owner"
 				@result.owner.last_name.should.equal "ShareLaTeX"
 				@result.owner.privileges.should.equal "owner"
+
+			it "should include the deletedDocs", ->
+				should.exist @result.deletedDocs
+				@result.deletedDocs.should.equal @project.deletedDocs
 				
 			it "should gather readOnly_refs and collaberators_refs into a list of members", ->
 				findMember = (id) =>

+ 26 - 1
services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee

@@ -286,7 +286,7 @@ describe 'ProjectEntityHandler', ->
 		beforeEach ->
 			@lines = ["mock", "doc", "lines"]
 			@rev = 5
-			@DocstoreManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @rev)
+			@DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev)
 			@ProjectEntityHandler.getDoc project_id, doc_id, @callback
 
 		it "should call the docstore", ->
@@ -339,6 +339,31 @@ describe 'ProjectEntityHandler', ->
 				.calledWith(project_id, @doc._id.toString(), @lines)
 				.should.equal true
 
+	describe "restoreDoc", ->
+		beforeEach ->
+			@name = "doc-name"
+			@lines = ['1234','abc']
+			@doc = { "mock": "doc" }
+			@folder_id = "mock-folder-id"
+			@callback = sinon.stub()
+			@ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(3, null, @lines)
+			@ProjectEntityHandler.addDoc = sinon.stub().callsArgWith(4, null, @doc, @folder_id)
+
+			@ProjectEntityHandler.restoreDoc project_id, doc_id, @name, @callback
+
+		it 'should get the doc lines', ->
+			@ProjectEntityHandler.getDoc
+				.calledWith(project_id, doc_id, include_deleted: true)
+				.should.equal true
+
+		it "should add a new doc with these doc lines", ->
+			@ProjectEntityHandler.addDoc
+				.calledWith(project_id, null, @name, @lines)
+				.should.equal true
+
+		it "should call the callback with the new folder and doc", ->
+			@callback.calledWith(null, @doc, @folder_id).should.equal true
+
 	describe 'adding file', ->
 		fileName = "something.jpg"
 		beforeEach ->