فهرست منبع

Merge branch 'master-redesign' into master-redesign-templates-and-blog

Henry Oswald 12 سال پیش
والد
کامیت
86a9d08e5c
32فایلهای تغییر یافته به همراه446 افزوده شده و 201 حذف شده
  1. 20 0
      services/web/app/views/project/editor.jade
  2. 19 0
      services/web/app/views/project/editor/binary-file.jade
  3. 2 1
      services/web/app/views/project/editor/editor.jade
  4. 39 13
      services/web/app/views/project/editor/file-tree.jade
  5. 24 3
      services/web/app/views/project/editor/header.jade
  6. 7 7
      services/web/app/views/project/editor/share.jade
  7. 17 1
      services/web/app/views/project/editor/track-changes.jade
  8. 6 0
      services/web/public/coffee/app/ide.coffee
  9. 12 0
      services/web/public/coffee/app/ide/binary-files/BinaryFilesManager.coffee
  10. 7 0
      services/web/public/coffee/app/ide/binary-files/controllers/BinaryFileController.coffee
  11. 2 0
      services/web/public/coffee/app/ide/connection/ConnectionManager.coffee
  12. 4 0
      services/web/public/coffee/app/ide/editor/Document.coffee
  13. 10 17
      services/web/public/coffee/app/ide/editor/EditorManager.coffee
  14. 33 0
      services/web/public/coffee/app/ide/editor/controllers/SavingNotificationController.coffee
  15. 4 4
      services/web/public/coffee/app/ide/editor/highlights/HighlightsManager.coffee
  16. 13 3
      services/web/public/coffee/app/ide/file-tree/FileTreeManager.coffee
  17. 13 0
      services/web/public/coffee/app/ide/file-tree/controllers/FileTreeRootFolderController.coffee
  18. 7 5
      services/web/public/coffee/app/ide/file-tree/directives/draggable.coffee
  19. 7 5
      services/web/public/coffee/app/ide/file-tree/directives/droppable.coffee
  20. 9 1
      services/web/public/coffee/app/ide/file-tree/directives/fileEntity.coffee
  21. 1 1
      services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee
  22. 19 0
      services/web/public/coffee/app/ide/permissions/PermissionsManager.coffee
  23. 18 1
      services/web/public/coffee/app/ide/services/ide.coffee
  24. 26 0
      services/web/public/coffee/app/ide/settings/controllers/ProjectNameController.coffee
  25. 2 0
      services/web/public/coffee/app/ide/settings/index.coffee
  26. 16 11
      services/web/public/coffee/app/ide/track-changes/TrackChangesManager.coffee
  27. 36 1
      services/web/public/stylesheets/app/editor.less
  28. 20 0
      services/web/public/stylesheets/app/editor/binary-file.less
  29. 4 0
      services/web/public/stylesheets/app/editor/file-tree.less
  30. 19 11
      services/web/public/stylesheets/app/editor/pdf.less
  31. 11 5
      services/web/public/stylesheets/app/editor/toolbar.less
  32. 19 111
      services/web/public/stylesheets/app/editor/track-changes.less

+ 20 - 0
services/web/app/views/project/editor.jade

@@ -33,6 +33,14 @@ block content
 			.alert.alert-warning.small(ng-if="connection.reconnecting")
 				strong Reconnecting...
 
+			.div(ng-controller="SavingNotificationController")
+				.alert.alert-warning.small(
+					ng-repeat="(doc_id, state) in docSavingStatus"
+					ng-if="state.unsavedSeconds > 3"
+				)
+					| Saving {{ state.doc.name }}... ({{ state.unsavedSeconds }} seconds of unsaved changes)
+
+
 		include ./editor/left-menu
 
 		#chat-wrapper(
@@ -56,11 +64,23 @@ block content
 
 					.ui-layout-center
 						include ./editor/editor
+						include ./editor/binary-file
 						include ./editor/track-changes
 
 			.ui-layout-east
 				include ./editor/chat
 
+	script(type="text/ng-template", id="genericMessageModalTemplate")
+		.modal-header
+			button.close(
+				type="button"
+				data-dismiss="modal"
+				ng-click="done()"
+			) ×
+			h3 {{ title }}
+		.modal-body {{ message }}
+		.modal-footer
+			button.btn.btn-info(ng-click="done()") OK
 
 	script(src='/socket.io/socket.io.js')
 

+ 19 - 0
services/web/app/views/project/editor/binary-file.jade

@@ -0,0 +1,19 @@
+div.binary-file.full-size(
+	ng-controller="BinaryFileController"
+	ng-show="ui.view == 'file'"
+	ng-if="openFile"
+)
+	img(
+		ng-src="/project/{{ project_id }}/file/{{ openFile.id }}"
+		ng-if="['png', 'jpg', 'jpeg', 'gif'].indexOf(extension(openFile)) > -1"
+	)
+	img(
+		ng-src="/project/{{ project_id }}/file/{{ openFile.id }}?format=png"
+		ng-if="['pdf', 'eps'].indexOf(extension(openFile)) > -1"
+	)
+	p.no-preview(
+		ng-if="['png', 'jpg', 'jpeg', 'gif', 'pdf', 'eps'].indexOf(extension(openFile)) == -1"
+	) Sorry, no preview is available.
+	a.btn.btn-info(
+		ng-href="/project/{{ project_id }}/file/{{ openFile.id }}"
+	) Download {{ openFile.name }}

+ 2 - 1
services/web/app/views/project/editor/editor.jade

@@ -25,7 +25,8 @@ div.full-size(
 			cursor-position="editor.cursorPosition",
 			goto-line="editor.gotoLine",
 			resize-on="layout:main:resize,layout:pdf:resize",
-			annotations="pdf.logEntryAnnotations[editor.open_doc_id]"
+			annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
+			read-only="!permissions.write"
 		)
 
 	.ui-layout-east

+ 39 - 13
services/web/app/views/project/editor/file-tree.jade

@@ -1,5 +1,5 @@
 aside#file-tree(ng-controller="FileTreeController")
-	.toolbar.toolbar-small
+	.toolbar.toolbar-small(ng-if="permissions.write")
 		a(
 			href,
 			ng-click="openNewDocModal()",
@@ -39,10 +39,19 @@ aside#file-tree(ng-controller="FileTreeController")
 			)
 				i.fa.fa-trash-o
 
-	.file-tree-inner
-		ul.list-unstyled.file-tree-list
+	.file-tree-inner(
+		ng-if="rootFolder", 
+		ng-controller="FileTreeRootFolderController",
+		ng-class="{ 'no-toolbar': !permissions.write }"
+	)
+		ul.list-unstyled.file-tree-list(
+			droppable="permissions.write"
+			accept=".entity-name"
+			on-drop-callback="onDrop"
+		)
 			file-entity(
 				entity="entity",
+				permissions="permissions",
 				ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
 			)
 
@@ -72,11 +81,12 @@ script(type='text/ng-template', id='entityListItemTemplate')
 		.entity(ng-if="entity.type != 'folder'")
 			.entity-name(
 				ng-click="select()"
-				ng-dblclick="startRenaming()"
-				draggable
+				ng-dblclick="permissions.write && startRenaming()"
+				draggable="permissions.write"
 				context-menu
 				data-target="context-menu-{{ entity.id }}"
 				context-menu-container="body"
+				context-menu-disabled="!permissions.write"
 			)
 				//- Just a spacer to align with folders
 				i.fa.fa-fw.toggle(ng-if="entity.type != 'folder'")
@@ -88,6 +98,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
 					ng-hide="entity.renaming"
 				)  {{ entity.name }}
 				input(
+					ng-if="permissions.write",
 					ng-show="entity.renaming",
 					ng-model="inputs.name",
 					ng-blur="finishRenaming()",
@@ -95,7 +106,10 @@ script(type='text/ng-template', id='entityListItemTemplate')
 					on-enter="finishRenaming()"
 				)
 
-				span.dropdown(ng-show="entity.selected")
+				span.dropdown(
+					ng-show="entity.selected",
+					ng-if="permissions.write"
+				)
 					a.dropdown-toggle(href)
 						i.fa.fa-chevron-down
 
@@ -113,7 +127,10 @@ script(type='text/ng-template', id='entityListItemTemplate')
 								ng-click="openDeleteModal()"
 							) Delete
 
-				div.dropdown.context-menu(id="context-menu-{{ entity.id }}")
+				div.dropdown.context-menu(
+					id="context-menu-{{ entity.id }}",
+					ng-if="permissions.write"
+				)
 					ul.dropdown-menu
 						li
 							a(
@@ -134,9 +151,9 @@ script(type='text/ng-template', id='entityListItemTemplate')
 		.entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController")
 			.entity-name(
 				ng-click="select()"
-				ng-dblclick="startRenaming()"
-				draggable
-				droppable
+				ng-dblclick="permissions.write && startRenaming()"
+				draggable="permissions.write"
+				droppable="permissions.write"
 				accept=".entity-name"
 				on-drop-callback="onDrop"
 			)
@@ -144,6 +161,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
 					context-menu
 					data-target="context-menu-{{ entity.id }}"
 					context-menu-container="body"
+					context-menu-disabled="!permissions.write"
 				)
 					i.fa.fa-fw.toggle(
 						ng-if="entity.type == 'folder'"
@@ -164,6 +182,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
 						ng-hide="entity.renaming"
 					)  {{ entity.name }}
 					input(
+						ng-if="permissions.write",
 						ng-show="entity.renaming",
 						ng-model="inputs.name",
 						ng-blur="finishRenaming()",
@@ -171,7 +190,10 @@ script(type='text/ng-template', id='entityListItemTemplate')
 						on-enter="finishRenaming()"
 					)
 
-					span.dropdown(ng-show="entity.selected")
+					span.dropdown(
+						ng-if="permissions.write"
+						ng-show="entity.selected"
+					)
 						a.dropdown-toggle(href)
 							i.fa.fa-chevron-down
 
@@ -208,7 +230,10 @@ script(type='text/ng-template', id='entityListItemTemplate')
 									ng-click="openUploadFileModal()"
 								) Upload File
 
-					.dropdown.context-menu(id="context-menu-{{ entity.id }}")
+					.dropdown.context-menu(
+						ng-if="permissions.write"
+						id="context-menu-{{ entity.id }}"
+					)
 						ul.dropdown-menu
 							li
 								a(
@@ -250,12 +275,13 @@ script(type='text/ng-template', id='entityListItemTemplate')
 			ul.list-unstyled(
 				ng-if="entity.type == 'folder'"
 				ng-show="expanded"
-				droppable
+				droppable="permissions.write"
 				accept=".entity-name"
 				on-drop-callback="onDrop"
 			)
 				file-entity(
 					entity="child",
+					permissions="permissions",
 					ng-repeat="child in entity.children | orderBy:[orderByFoldersFirst, 'name']"
 				)
 			

+ 24 - 3
services/web/app/views/project/editor/header.jade

@@ -7,14 +7,35 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
 	)
 		i.fa.fa-fw.fa-bars
 
-	span.name {{ project.name }}
+	.toolbar-center.project-name(ng-controller="ProjectNameController")
+		span.name(
+			ng-dblclick="startRenaming()",
+			ng-show="!state.renaming"
+		) {{ project.name }}
 
-	a(href='#', data-toggle="tooltip", title="Rename")
-		i.fa.fa-pencil
+		input.form-control(
+			type="text"
+			ng-model="inputs.name",
+			ng-show="state.renaming",
+			on-enter="finishRenaming()",
+			ng-blur="finishRenaming()",
+			select-name-when="state.renaming"
+		)
+
+		a.rename(
+			href='#',
+			tooltip-placement="bottom",
+			tooltip="Rename",
+			tooltip-append-to-body="true",
+			ng-click="startRenaming()",
+			ng-show="!state.renaming"
+		)
+			i.fa.fa-pencil
 
 	.toolbar-right
 		a.btn.btn-full-height(
 			href,
+			ng-if="permissions.admin",
 			tooltip="Share",
 			tooltip-placement="bottom",
 			ng-click="openShareProjectModal()",

+ 7 - 7
services/web/app/views/project/editor/share.jade

@@ -9,7 +9,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 	.modal-body.modal-body-share
 		.container-fluid
 			.row.public-access-level(ng-show="project.publicAccesLevel == 'private'")
-				.col-md-12.text-center
+				.col-xs-12.text-center
 					| This project is private and can only be accessed by the people below.
 					|   
 					a(
@@ -17,7 +17,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						ng-click="openMakePublicModal()"
 					) Make Public
 			.row.public-access-level(ng-show="project.publicAccesLevel != 'private'")
-				.col-md-12.text-center
+				.col-xs-12.text-center
 					strong(ng-if="project.publicAccesLevel == 'readAndWrite'") This project is public and can be edited by anyone with the URL.
 					strong(ng-if="project.publicAccesLevel == 'readOnly'") This project is public and can be viewed by anyone with the URL.
 					|   
@@ -26,16 +26,16 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						ng-click="openMakePrivateModal()"
 					) Make Private
 			.row.project-member
-				.col-md-8 {{ project.owner.email }}
+				.col-xs-8 {{ project.owner.email }}
 				.text-right(
-					ng-class="{'col-md-3': project.members.length > 0, 'col-md-4': project.members.length == 0}"
+					ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
 				) Owner
 			.row.project-member(ng-repeat="member in project.members")
-				.col-md-8 {{ member.email }}
-				.col-md-3.text-right
+				.col-xs-8 {{ member.email }}
+				.col-xs-3.text-right
 					span(ng-show="member.privileges == 'readAndWrite'") Can Edit
 					span(ng-show="member.privileges == 'readOnly'") Read Only
-				.col-md-1
+				.col-xs-1
 					a(
 						href
 						tooltip="Remove collaborator"

+ 17 - 1
services/web/app/views/project/editor/track-changes.jade

@@ -1,4 +1,17 @@
 div#trackChanges(ng-show="ui.view == 'track-changes'")
+	.upgrade-prompt(ng-show="!project.features.versioning")
+		.message(ng-show="project.owner._id == user.id")
+			p You need to upgrade your account to use the History feature.
+			p
+				a.btn.btn-info(
+					href="/user/subscription/plans"
+					target="_blank"
+					ng-click="startedFreeTrial = true"
+				) Start Free Trial
+			p.small(ng-show="startedFreeTrial") Please refresh the page after starting your free trial.
+		.message(ng-show="project.owner._id != user.id")
+			p Please ask the project owner to upgrade to use the History feature.
+
 	aside.change-list(
 		ng-controller="TrackChangesListController"
 		infinite-scroll="loadMore()"
@@ -57,8 +70,11 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
 						div.users
 							div.user(ng-repeat="update_user in update.meta.users")
 								.color-square(ng-style="{'background-color': 'hsl({{ update_user.hue }}, 100%, 50%)'}")
-								span(ng-if="update_user.id != user.id") {{user.first_name}} {{user.last_name}}
+								span(ng-if="update_user.id != user.id") {{update_user.first_name}} {{update_user.last_name}}
 								span(ng-if="update_user.id == user.id") You
+							div.user(ng-if="update.meta.users.length == 0")
+								.color-square(style="background-color: hsl(100, 100%, 50%)")
+								span Anonymous
 
 			.loading(ng-show="trackChanges.loading")
 				i.fa.fa-spin.fa-refresh

+ 6 - 0
services/web/public/coffee/app/ide.coffee

@@ -5,7 +5,9 @@ define [
 	"ide/editor/EditorManager"
 	"ide/online-users/OnlineUsersManager"
 	"ide/track-changes/TrackChangesManager"
+	"ide/permissions/PermissionsManager"
 	"ide/pdf/PdfManager"
+	"ide/binary-files/BinaryFilesManager"
 	"ide/settings/index"
 	"ide/share/index"
 	"ide/chat/index"
@@ -22,7 +24,9 @@ define [
 	EditorManager
 	OnlineUsersManager
 	TrackChangesManager
+	PermissionsManager
 	PdfManager
+	BinaryFilesManager
 ) ->
 	App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) ->
 		# Don't freak out if we're already in an apply callback
@@ -59,6 +63,8 @@ define [
 		ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
 		ide.trackChangesManager = new TrackChangesManager(ide, $scope)
 		ide.pdfManager = new PdfManager(ide, $scope)
+		ide.permissionsManager = new PermissionsManager(ide, $scope)
+		ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
 	]
 
 	angular.bootstrap(document.body, ["SharelatexApp"])

+ 12 - 0
services/web/public/coffee/app/ide/binary-files/BinaryFilesManager.coffee

@@ -0,0 +1,12 @@
+define [
+	"ide/binary-files/controllers/BinaryFileController"
+], () ->
+	class BinaryFilesManager
+		constructor: (@ide, @$scope) ->
+			@$scope.$on "entity:selected", (event, entity) =>
+				if (@$scope.ui.view != "track-changes" and entity.type == "file")
+					@openFile(entity)
+
+		openFile: (file) ->
+			@$scope.ui.view = "file"
+			@$scope.openFile = file

+ 7 - 0
services/web/public/coffee/app/ide/binary-files/controllers/BinaryFileController.coffee

@@ -0,0 +1,7 @@
+define [
+	"base"
+], (App) ->
+	App.controller "BinaryFileController", ["$scope", ($scope) ->
+		$scope.extension = (file) ->
+			return file.name.split(".").pop()?.toLowerCase()
+	]

+ 2 - 0
services/web/public/coffee/app/ide/connection/ConnectionManager.coffee

@@ -57,6 +57,7 @@ define [], () ->
 				@$scope.$apply () =>
 					@$scope.protocolVersion = protocolVersion
 					@$scope.project = project
+					@$scope.permissionsLevel = permissionsLevel
 					@$scope.state.load_progress = 100
 					@$scope.state.loading = false
 					@$scope.$emit "project:joined"
@@ -79,6 +80,7 @@ define [], () ->
 				countdown = 3 + Math.floor(Math.random() * 7)
 
 			@$scope.$apply () =>
+				@$scope.connection.reconnecting = false
 				@$scope.connection.reconnection_countdown = countdown
 
 			setTimeout(=>

+ 4 - 0
services/web/public/coffee/app/ide/editor/Document.coffee

@@ -149,6 +149,10 @@ define [
 				@ide.connectionManager.disconnect()
 				return
 
+			if Math.random() < (@ide.ignoreRate or 0)
+				console.log "Simulating lost update"
+				return
+
 			if update?.doc == @doc_id and @doc?
 				@doc.processUpdateFromServer update
 

+ 10 - 17
services/web/public/coffee/app/ide/editor/EditorManager.coffee

@@ -1,6 +1,7 @@
 define [
 	"ide/editor/Document"
 	"ide/editor/directives/aceEditor"
+	"ide/editor/controllers/SavingNotificationController"
 ], (Document) ->
 	class EditorManager
 		constructor: (@ide, @$scope) ->
@@ -14,7 +15,7 @@ define [
 			}
 
 			@$scope.$on "entity:selected", (event, entity) =>
-				if (@$scope.ui.view == "editor" and entity.type == "doc")
+				if (@$scope.ui.view != "track-changes" and entity.type == "doc")
 					@openDoc(entity)
 
 			initialized = false
@@ -77,25 +78,17 @@ define [
 
 		_bindToDocumentEvents: (doc, sharejs_doc) ->
 			sharejs_doc.on "error", (error) =>
-				console.error "DOC ERROR", error
 				@openDoc(doc, forceReopen: true)
-
-				#TODO!!!
-				# Modal.createModal
-				# 	title: "Out of sync"
-				# 	message: "Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
-				# 	buttons:[
-				# 		text: "Ok"
-				# 	]
+				@ide.showGenericMessageModal(
+					"Out of sync"
+					"Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
+				)
 
 			sharejs_doc.on "externalUpdate", () =>
-				#TODO!!!
-				# Modal.createModal
-				# 	title: "Document Updated Externally"
-				# 	message: "This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
-				# 	buttons:[
-				# 		text: "Ok"
-				# 	]
+				@ide.showGenericMessageModal(
+					"Document Updated Externally"
+					"This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
+				)
 
 		_unbindFromDocumentEvents: (document) ->
 			document.off()

+ 33 - 0
services/web/public/coffee/app/ide/editor/controllers/SavingNotificationController.coffee

@@ -0,0 +1,33 @@
+define [
+	"base"
+	"ide/editor/Document"
+], (App, Document) ->
+	App.controller "SavingNotificationController", ["$scope", "$interval", "ide", ($scope, $interval, ide) ->
+		$interval () ->
+			pollSavedStatus()
+		, 1000
+
+		$(window).bind 'beforeunload', () =>
+			warnAboutUnsavedChanges()
+
+		$scope.docSavingStatus = {}
+		pollSavedStatus = () ->
+			oldStatus = $scope.docSavingStatus
+			$scope.docSavingStatus = {}
+
+			for doc_id, doc of Document.openDocs
+				saving = doc.pollSavedStatus()
+				if !saving
+					if oldStatus[doc_id]?
+						$scope.docSavingStatus[doc_id] = oldStatus[doc_id]
+						$scope.docSavingStatus[doc_id].unsavedSeconds += 1
+					else
+						$scope.docSavingStatus[doc_id] = {
+							unsavedSeconds: 0
+							doc: ide.fileTreeManager.findEntityById(doc_id)
+						}
+
+		warnAboutUnsavedChanges = () ->
+			if Document.hasUnsavedChanges()
+				return "You have unsaved changes. If you leave now they will not be saved."
+	]

+ 4 - 4
services/web/public/coffee/app/ide/editor/highlights/HighlightsManager.coffee

@@ -204,16 +204,16 @@ define [
 		_getColorScheme: (hue) ->
 			if @_isDarkTheme()
 				return {
-					cursor: "hsl(#{hue}, 100%, 50%)"
-					labelBackgroundColor: "hsl(#{hue}, 100%, 50%)"
+					cursor: "hsl(#{hue}, 70%, 50%)"
+					labelBackgroundColor: "hsl(#{hue}, 70%, 50%)"
 					highlightBackgroundColor: "hsl(#{hue}, 100%, 28%);"
 					strikeThroughBackgroundColor: "hsl(#{hue}, 100%, 20%);"
 					strikeThroughForegroundColor: "hsl(#{hue}, 100%, 60%);"
 				}
 			else
 				return {
-					cursor: "hsl(#{hue}, 100%, 50%)"
-					labelBackgroundColor: "hsl(#{hue}, 100%, 50%)"
+					cursor: "hsl(#{hue}, 70%, 50%)"
+					labelBackgroundColor: "hsl(#{hue}, 70%, 50%)"
 					highlightBackgroundColor: "hsl(#{hue}, 70%, 85%);"
 					strikeThroughBackgroundColor: "hsl(#{hue}, 70%, 95%);"
 					strikeThroughForegroundColor: "hsl(#{hue}, 70%, 40%);"

+ 13 - 3
services/web/public/coffee/app/ide/file-tree/FileTreeManager.coffee

@@ -5,6 +5,7 @@ define [
 	"ide/file-tree/controllers/FileTreeController"
 	"ide/file-tree/controllers/FileTreeEntityController"
 	"ide/file-tree/controllers/FileTreeFolderController"
+	"ide/file-tree/controllers/FileTreeRootFolderController"
 ], () ->
 	class FileTreeManager
 		constructor: (@ide, @$scope) ->
@@ -59,6 +60,7 @@ define [
 			@ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
 				entity = @findEntityById(entity_id)
 				folder = @findEntityById(folder_id)
+				console.log "Got recive ENTITY", entity_id, folder_id, entity, folder
 				@$scope.$apply () =>
 					@_moveEntityInScope(entity, folder)
 
@@ -68,7 +70,15 @@ define [
 				entity.selected = false
 			entity.selected = true
 
+		findSelectedEntity: () ->
+			selected = null
+			@forEachEntity (entity) ->
+				selected = entity if entity.selected
+			return selected
+
 		findEntityById: (id, options = {}) ->
+			return @$scope.rootFolder if @$scope.rootFolder.id == id
+
 			entity = @_findEntityByIdInFolder @$scope.rootFolder, id
 			return entity if entity?
 
@@ -250,7 +260,7 @@ define [
 				_csrf: window.csrfToken
 			}
 
-		_deleteEntityFromScope: (entity) ->
+		_deleteEntityFromScope: (entity, options = { moveToDeleted: true }) ->
 			parent_folder = null
 			@forEachEntity (possible_entity, folder) ->
 				if possible_entity == entity
@@ -261,11 +271,11 @@ define [
 				if index > -1
 					parent_folder.children.splice(index, 1)
 
-			if entity.type == "doc"
+			if entity.type == "doc" and options.moveToDeleted
 				entity.deleted = true
 				@$scope.deletedDocs.push entity
 
 		_moveEntityInScope: (entity, parent_folder) ->
 			return if entity in parent_folder.children
-			@_deleteEntityFromScope(entity)
+			@_deleteEntityFromScope(entity, moveToDeleted: false)
 			parent_folder.children.push(entity)

+ 13 - 0
services/web/public/coffee/app/ide/file-tree/controllers/FileTreeRootFolderController.coffee

@@ -0,0 +1,13 @@
+define [
+	"base"
+], (App) ->
+	App.controller "FileTreeRootFolderController", ["$scope", "ide", ($scope, ide) ->
+		console.log "CREATING FileTreeRootFolderController"
+		rootFolder = $scope.rootFolder
+		console.log "ROOT FOLDER", rootFolder
+		$scope.onDrop = (events, ui) ->
+			source = $(ui.draggable).scope().entity
+			console.log "DROPPED INTO ROOT", source, rootFolder
+			return if !source?
+			ide.fileTreeManager.moveEntity(source, rootFolder)
+	]

+ 7 - 5
services/web/public/coffee/app/ide/file-tree/directives/draggable.coffee

@@ -4,9 +4,11 @@ define [
 	App.directive "draggable", () ->
 		return {
 			link: (scope, element, attrs) ->
-				element.draggable
-					delay: 250
-					opacity: 0.7
-					helper: "clone"
-					scroll: true
+				scope.$watch attrs.draggable, (draggable) ->
+					if draggable
+						element.draggable
+							delay: 250
+							opacity: 0.7
+							helper: "clone"
+							scroll: true
 		}

+ 7 - 5
services/web/public/coffee/app/ide/file-tree/directives/droppable.coffee

@@ -7,9 +7,11 @@ define [
 				onDropCallback: "="
 			}
 			link: (scope, element, attrs) ->
-				element.droppable
-					greedy: true
-					hoverClass: "droppable-hover"
-					accept: attrs.accept
-					drop: scope.onDropCallback
+				scope.$watch attrs.droppable, (droppable) ->
+					if droppable
+						element.droppable
+							greedy: true
+							hoverClass: "droppable-hover"
+							accept: attrs.accept
+							drop: scope.onDropCallback
 		}

+ 9 - 1
services/web/public/coffee/app/ide/file-tree/directives/fileEntity.coffee

@@ -6,10 +6,18 @@ define [
 			restrict: "E"
 			scope: {
 				entity: "="
+				permissions: "="
 			}
 			templateUrl: "entityListItemTemplate"
 			compile: (element) ->
 				RecursionHelper.compile element, (scope, element, attrs, ctrl) ->
-					# Link function here if needed
+					# Don't freak out if we're already in an apply callback
+					scope.$originalApply = scope.$apply
+					scope.$apply = (fn = () ->) ->
+						phase = @$root.$$phase
+						if (phase == '$apply' || phase == '$digest')
+							fn()
+						else
+							@$originalApply(fn);
 		}
 	]

+ 1 - 1
services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee

@@ -56,7 +56,7 @@ define [
 		OWN_HUE: 200 # We will always appear as this color to ourselves
 		ANONYMOUS_HUE: 100
 		getHueForUserId: (user_id) ->
-			if !user_id?
+			if !user_id? or user_id == "anonymous-user"
 				return @ANONYMOUS_HUE
 
 			if window.user.id == user_id

+ 19 - 0
services/web/public/coffee/app/ide/permissions/PermissionsManager.coffee

@@ -0,0 +1,19 @@
+define [], () ->
+	class PermissionsManager
+		constructor: (@ide, @$scope) ->
+			@$scope.$watch "permissionsLevel", (permissionsLevel) =>
+				@$scope.permissions =
+					read:  false
+					write: false
+					admin: false
+				if permissionsLevel?
+					if permissionsLevel == "readOnly"
+						@$scope.permissions.read = true
+					else if permissionsLevel == "readAndWrite"
+						@$scope.permissions.read = true
+						@$scope.permissions.write = true
+					else if permissionsLevel == "owner"
+						@$scope.permissions.read = true
+						@$scope.permissions.write = true
+						@$scope.permissions.admin = true
+

+ 18 - 1
services/web/public/coffee/app/ide/services/ide.coffee

@@ -3,7 +3,7 @@ define [
 ], (App) ->
 	# We create and provide this as service so that we can access the global ide
 	# from within other parts of the angular app.
-	App.factory "ide", ["$http", ($http) ->
+	App.factory "ide", ["$http", "$modal", ($http, $modal) ->
 		ide = {}
 		ide.$http = $http
 
@@ -16,5 +16,22 @@ define [
 		ide.showGenericServerErrorMessage = () ->
 			console.error "GENERIC SERVER ERROR MESSAGE STUB"
 
+		ide.showGenericMessageModal = (title, message) ->
+			$modal.open {
+				templateUrl: "genericMessageModalTemplate"
+				controller:  "GenericMessageModalController"
+				resolve:
+					title:   -> title
+					message: -> message
+			}
+
 		return ide
 	]
+
+	App.controller "GenericMessageModalController", ["$scope", "$modalInstance", "title", "message", ($scope, $modalInstance, title, message) ->
+		$scope.title = title
+		$scope.message = message
+
+		$scope.done = () ->
+			$modalInstance.close()
+	]

+ 26 - 0
services/web/public/coffee/app/ide/settings/controllers/ProjectNameController.coffee

@@ -0,0 +1,26 @@
+define [
+	"base"
+], (App) ->
+	App.controller "ProjectNameController", ["$scope", "settings", "ide", ($scope, settings, ide) ->
+		$scope.state =
+			renaming: false
+		$scope.inputs = {}
+
+		$scope.startRenaming = () ->
+			$scope.inputs.name = $scope.project.name
+			$scope.state.renaming = true
+			$scope.$emit "project:rename:start"
+
+		$scope.finishRenaming = () ->
+			$scope.project.name = $scope.inputs.name
+			settings.saveProjectSettings({name: $scope.inputs.name})
+			$scope.state.renaming = false
+
+		ide.socket.on "projectNameUpdated", (name) ->
+			$scope.$apply () ->
+				$scope.project.name = name
+
+		$scope.$watch "project.name", (name) ->
+			if name?
+				window.document.title = name + " - Online LaTeX Editor ShareLaTeX"
+	]

+ 2 - 0
services/web/public/coffee/app/ide/settings/index.coffee

@@ -1,5 +1,7 @@
 define [
 	"ide/settings/services/settings"
 	"ide/settings/controllers/SettingsController"
+	"ide/settings/controllers/ProjectNameController"
+
 ], () ->
 			

+ 16 - 11
services/web/public/coffee/app/ide/track-changes/TrackChangesManager.coffee

@@ -9,10 +9,9 @@ define [
 
 			@$scope.toggleTrackChanges = () =>
 				if @$scope.ui.view == "track-changes"
-					@$scope.ui.view = "editor"
+					@hide()
 				else
-					@$scope.ui.view = "track-changes"
-					@onShow()
+					@show()
 
 			@$scope.$watch "trackChanges.selection.updates", (updates) =>
 				if updates? and updates.length > 0
@@ -24,11 +23,14 @@ define [
 					@$scope.trackChanges.selection.doc = entity
 					@reloadDiff()
 
-		onShow: () ->
+		show: () ->
+			@$scope.ui.view = "track-changes"
 			@reset()
-			# @fetchNextBatchOfChanges()
-			# 	.success () =>
-			# 		@autoSelectRecentUpdates()
+
+		hide: () ->
+			@$scope.ui.view = "editor"
+			# Make sure we run the 'open' logic for whatever is currently selected
+			@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
 
 		reset: () ->
 			@$scope.trackChanges = {
@@ -157,21 +159,24 @@ define [
 				}
 
 				if entry.i? or entry.d?
-					name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
-					if entry.meta.user.id == @$scope.user.id
+					if entry.meta.user?
+						name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
+					else
+						name = "Anonymous"
+					if entry.meta.user?.id == @$scope.user.id
 						name = "you"
 					date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
 					if entry.i?
 						highlights.push {
 							label: "Added by #{name} on #{date}"
 							highlight: range
-							hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user.id)
+							hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user?.id)
 						}
 					else if entry.d?
 						highlights.push {
 							label: "Deleted by #{name} on #{date}"
 							strikeThrough: range
-							hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user.id)
+							hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user?.id)
 						}
 
 			return {text, highlights}

+ 36 - 1
services/web/public/stylesheets/app/editor.less

@@ -5,6 +5,7 @@
 @import "./editor/pdf.less";
 @import "./editor/share.less";
 @import "./editor/chat.less";
+@import "./editor/binary-file.less";
 
 .full-size {
 	position: absolute;
@@ -17,10 +18,15 @@
 .global-alerts {
 	position: absolute;
 	z-index: 20;
-	top: (@line-height-computed / 4);
+	top: 2px;
 	width: 400px;
 	left: 50%;
 	margin-left: -200px;
+	.alert {
+		padding: (@line-height-computed / 4);
+		font-size: 14px;
+		margin-bottom: (@line-height-computed / 4);
+	}
 }
 
 #chat-wrapper {
@@ -57,6 +63,35 @@
 	}
 }
 
+.project-name {
+	.name {
+		display: inline-block;
+		max-width: 250px;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		vertical-align: top;
+		padding: 6px;
+		color: @gray;
+		font-weight: 700;
+		white-space: nowrap;
+	}
+	input {
+		height: 30px;
+		margin-top: 4px;
+		text-align: center;
+		padding: 6px;
+		font-weight: 700;
+	}
+	a.rename {
+		visibility: hidden;
+	}
+	&:hover {
+		a.rename {
+			visibility: visible;
+		}
+	}
+}
+
 // The internal components of the aceEditor directive
 .ace-editor-wrapper {
 	.full-size;

+ 20 - 0
services/web/public/stylesheets/app/editor/binary-file.less

@@ -0,0 +1,20 @@
+.binary-file {
+	padding: @line-height-computed / 2;
+	background-color: @gray-lightest;
+	text-align: center;
+	img {
+		max-width: 100%;
+		max-height: 90%;
+		display: block;
+		margin: auto;
+		margin-bottom: @line-height-computed / 2;
+		border: 1px solid @gray;
+		.box-shadow(0 2px 3px @gray;);
+		background-color: white;
+	}
+	p.no-preview {
+		font-size: 24px;
+		color: @gray;
+	}
+
+}

+ 4 - 0
services/web/public/stylesheets/app/editor/file-tree.less

@@ -9,6 +9,10 @@ aside#file-tree {
 		left: 0;
 		right: 0;
 		overflow-y: auto;
+
+		&.no-toolbar {
+			top: 0;
+		}
 	}
 
 	h3 {

+ 19 - 11
services/web/public/stylesheets/app/editor/pdf.less

@@ -73,18 +73,26 @@
 	}
 }
 
-.log-btn {
-	position: relative;
-	.label {
-		position: absolute;
-		top: 0;
-		right: 0;
-		padding: .15em .6em .2em;
-		font-size: 60%;
-	}
-	&.active, &:active {
+.toolbar {
+	.log-btn {
+		position: relative;
 		.label {
-			display: none;
+			position: absolute;
+			top: 0;
+			right: 0;
+			padding: .15em .6em .2em;
+			font-size: 60%;
+		}
+		&.active, &:active {
+			.label {
+				display: none;
+			}
+			color: white;
+			background-color: @link-color;
+			.box-shadow(inset 0 3px 5px rgba(0, 0, 0, 0.225));
+			&:hover {
+				color: white;
+			}
 		}
 	}
 }

+ 11 - 5
services/web/public/stylesheets/app/editor/toolbar.less

@@ -13,11 +13,6 @@
 			color: @gray-dark;
 			text-decoration: none;
 		}
-		&.active, &:active {
-			color: white;
-			background-color: @link-color;
-			.box-shadow(inset 0 3px 5px rgba(0, 0, 0, 0.225));
-		}
 	}
 
 	.btn-full-height {
@@ -47,6 +42,17 @@
 		}
 	}
 
+	.toolbar-center {
+		width: 300px;
+		position: absolute;
+		top: 0;
+		left: 50%;
+		margin-left: -150px;
+		text-align: center;
+		text-overflow: ellipsis;
+		overflow: hidden;
+	}
+
 	&.toolbar-header {
 		box-shadow: 0 0 2px #ccc;
 		position: absolute;

+ 19 - 111
services/web/public/stylesheets/app/editor/track-changes.less

@@ -9,117 +9,25 @@
 @range-bar-selected-offset: 14px;
 
 #trackChanges {
-	// .track-changes-diff {
-	// 	position: absolute;
-	// 	right: @changesListWidth + 1px;
-	// 	left: 0;
-	// 	top: 0;
-	// 	bottom: 0;
-	// 	height: 100%;
-	// 	.ace_editor {
-	// 		position: absolute;
-	// 		top: 42px;
-	// 		left: 0;
-	// 		right: 0;
-	// 		bottom: 0;
-	// 		.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
-	// 			display: none;
-	// 		}
-	// 	}
-	// 	.track-changes-diff-toolbar {
-	// 		position: absolute;
-	// 		top: 0;
-	// 		left: 0;
-	// 		right: -1px;
-	// 		height: 32px;
-	// 		padding: 5px 5px 5px 5px;
-	// 		margin: 0;
-	// 		background-color: #282828;
-	// 		color: white;
-	// 		border-right: 1px solid white;
-	// 		.number-of-changes, .restore {
-	// 			position: absolute;
-	// 		}
-	// 		.number-of-changes {
-	// 			left: 10px;
-	// 			bottom: 7px;
-	// 		}
-	// 		.restore {
-	// 			right: 10px;
-	// 			bottom: 5px;
-	// 			padding: 3px 9px;
-	// 		}
-	// 	}
-	// }
-
-
-	// .track-changes-upgrade-control, .track-changes-upgrade-popup {
-	// 	position: absolute;
-	// 	top: 0;
-	// 	bottom: 0;
-	// 	left: 0;
-	// 	right: 0;
-	// 	z-index: 100;
-	// }
-
-	// .track-changes-upgrade-popup {
-	// 	background-color: rgba(128,128,128,0.4);
-	// 	.message {
-	// 		margin: auto;
-	// 		margin-top: 200px;
-	// 		padding: 10px 10px 14px 10px;
-	// 		width: 400px;
-	// 		font-weight: bold;
-	// 		text-align: center;
-	// 		background-color: white;
-	// 		.border-radius(8px);
-	// 	}
-	// }
-
-	// .track-changes-upgrade-control {
-	// 	background-color: #eeeeee;
-	// 	text-align: center;
-	// 	.message {
-	// 		font-size: 18px;
-	// 		margin: 12px;
-	// 		margin-top: 36px;
-	// 	}
-	// }
-
-	// .deleted-background,
-	// .deleted-foreground,
-	// .inserted-background,
-	// .name-marker,
-	// .changes-before,
-	// .changes-after {
-	// 	position: absolute;
-	// 	z-index: 2;
-	// }
-
-	// .name-marker {
-	// 	font-size: 0.8em;
-	// 	padding: 2px 6px;
-	// 	.border-radius(3px 3px 3px 3px);
-	// 	position: absolute;
-	// 	border: 1px solid #999;
-	// 	left: 0;
-	// 	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);
-	// }
+	.upgrade-prompt {
+		position: absolute;
+		top: 0;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		z-index: 100;
+		background-color: rgba(128,128,128,0.4);
+		.message {
+			margin: auto;
+			margin-top: 100px;
+			padding: 10px 10px 14px 10px;
+			width: 500px;
+			font-weight: bold;
+			text-align: center;
+			background-color: white;
+			border-radius: 8px;
+		}
+	}
 
 	.diff {
 		margin-right: @changesListWidth;