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

Conflicts:
	app/views/project/editor.jade
This commit is contained in:
Henry Oswald 2014-07-01 15:23:05 +01:00
commit 000be22f16
34 changed files with 1000 additions and 173 deletions

View file

@ -26,7 +26,8 @@ module.exports =
addUserToProject: (project_id, email, privilegeLevel, callback)->
emails = mimelib.parseAddresses(email)
email = emails[0].address?.toLowerCase()
email = emails[0]?.address?.toLowerCase()
return callback(new Error("no valid email provided")) if !email?
self = @
User.findOne {'email':email}, (err, user)->
async.waterfall [

View file

@ -138,7 +138,7 @@ module.exports = EditorController =
CollaboratorsHandler.addUserToProject project_id, email, privileges, (err, user)=>
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, "", ->
EditorRealTimeController.emitToRoom(project_id, 'userAddedToProject', user, privileges)
callback null, true
callback null, ProjectEditorHandler.buildUserModelView(user, privileges)
removeUserFromProject: (project_id, user_id, callback)->
CollaboratorsHandler.removeUserFromProject project_id, user_id, =>

View file

@ -37,6 +37,8 @@ block content
include ./editor/header
include ./editor/share
#ide-body(ng-cloak, layout="main", ng-hide="state.loading")
.ui-layout-west
include ./editor/file-tree
@ -45,34 +47,6 @@ block content
include ./editor/editor
include ./editor/track-changes
//- #loadingScreen
//- h3 Loading...
//- p#loadingMessage Loading editor
//- #errorMessages
//- #connectionLostMessage(style="display: none;")
//- | Lost connection.
//- span#trying-reconnect
//- | Reconnecting in
//- span#reconnection-countdown ?
//- | seconds.
//- a(href='#')#try-reconnect-now Try now.
//- span#reconnecting
//- | Reconnecting...
//- #savingProblems(style="display: none")
//- | Saving...
//- div#toolbar.sidebar-navigation
//- ul#tabs
//- #toolbar-footer
//- #tab-content.tab-content
//- include ../templates
//- include ../templates/dropbox
script(src='/socket.io/socket.io.js')
script(type='text/javascript').

View file

@ -16,13 +16,35 @@ div.full-size(
font-size="settings.fontSize",
auto-complete="settings.autoComplete",
spell-check-language="project.spellCheckLanguage",
annotations="onlineUserCursorAnnotations[editor.open_doc_id]"
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
show-print-margin="false",
sharejs-doc="editor.sharejs_doc",
last-updated="editor.last_updated",
cursor-position="editor.cursorPosition",
resize-on="layout:main:resize,layout:pdf:resize"
goto-line="editor.gotoLine",
resize-on="layout:main:resize,layout:pdf:resize",
annotations="pdf.logEntryAnnotations[editor.open_doc_id]"
)
.ui-layout-east
include ./pdf
include ./pdf
.ui-layout-resizer-controls.synctex-controls(
ng-show="!!pdf.url && settings.pdfViewer == 'pdfjs' && showControls"
ng-controller="PdfSynctexController"
)
a.btn.btn-default.btn-xs(
tooltip="Go to code location in PDF"
tooltip-placement="right"
tooltip-append-to-body="true"
ng-click="syncToPdf()"
)
i.fa.fa-long-arrow-right
br
a.btn.btn-default.btn-xs(
tooltip-html-unsafe="Go to PDF location in code<br/>(or double click PDF)"
tooltip-placement="right"
tooltip-append-to-body="true"
ng-click="syncToCode()"
)
i.fa.fa-long-arrow-left

View file

@ -13,7 +13,13 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
i.fa.fa-pencil
.toolbar-right
a.btn.btn-full-height(href='#', tooltip="Share", tooltip-placement="bottom")
a.btn.btn-full-height(
href,
tooltip="Share",
tooltip-placement="bottom",
ng-click="openShareProjectModal()",
ng-controller="ShareController"
)
i.fa.fa-fw.fa-group
a.btn.btn-full-height(
href,

View file

@ -3,7 +3,7 @@ aside#left-menu.full-size(
ng-cloak
)
h4 Settings
form
form(ng-controller="SettingsController")
.form-controls
label(for="compiler") Compiler
select.form-control(
@ -64,6 +64,15 @@ aside#left-menu.full-size(
each size in ['10','11','12','13','14','16','20','24']
option(value=size) #{size}px
.form-controls
label(for="pdfViewer") PDF Viewer
select.form-control(
name="pdfViewer"
ng-model="settings.pdfViewer"
)
option(value="pdfjs") Built-In
option(value="native") Native
#left-menu-mask(
ng-show="ui.leftMenuShown",
ng-click="ui.leftMenuShown = false"

View file

@ -14,32 +14,67 @@ div.full-size(ng-controller="PdfController")
a.log-btn(
href
ng-click="toggleLogs()"
ng-class="{ 'active': pdf.view == 'logs' }"
ng-class="{ 'active': (pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled }"
tooltip="Logs"
tooltip-placement="bottom"
)
i.fa.fa-file-text-o
span.label.label-danger(ng-show="pdf.logEntries.errors.length > 0")
| {{ pdf.logEntries.errors.length }}
span.label.label-warning(ng-show="pdf.logEntries.warnings.length > 0")
| {{ pdf.logEntries.warnings.length }}
span.label(
ng-show="pdf.logEntries.warnings.length + pdf.logEntries.errors.length > 0"
ng-class="{\
'label-warning': pdf.logEntries.errors.length == 0,\
'label-danger': pdf.logEntries.errors.length > 0\
}"
) {{ pdf.logEntries.errors.length + pdf.logEntries.warnings.length }}
.pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf'")
.pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error")
div(
pdfjs
ng-if="settings.pdfViewer == 'pdfjs'"
pdf-src="pdf.url"
key="project_id"
resize-on="layout:main:resize,layout:pdf:resize"
highlights="pdf.highlights"
position="pdf.position"
dbl-click-callback="syncToCode"
)
iframe(
ng-src="{{ pdf.url }}"
ng-if="settings.pdfViewer == 'native'"
)
.logs(ng-show="pdf.view == 'logs'")
div(ng-repeat="entry in pdf.logEntries.all")
.pdf-uncompiled(ng-show="pdf.uncompiled && !pdf.compiling")
| &nbsp;
i.fa.fa-level-up.fa-flip-horizontal.fa-2x
| &nbsp;&nbsp;Click here to preview your work as a PDF.
.pdf-errors(ng-show="pdf.timeout || pdf.error")
.alert.alert-danger(ng-show="pdf.error")
strong Server Error.
span Sorry, something went wrong and your project could not be compiled. Please try again in a few moments.
.alert.alert-danger(ng-show="pdf.timeout")
strong Timed out.
span Sorry, your compile was taking too long and timed out.
| This may be due to a large number of high-res images, or lots of complicated diagrams.
| Please try to make your document simpler, or contact support for help.
.pdf-logs(ng-show="(pdf.view == 'logs' || pdf.failure) && !pdf.error && !pdf.timeout && !pdf.uncompiled")
.alert.alert-success(ng-show="pdf.logEntries.all.length == 0")
| No errors, good job!
.alert.alert-danger(ng-show="pdf.failure")
strong Compile Error.
span Sorry, your LaTeX code couldn't compile for some reason. Please check the errors below for details, or view the raw log.
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
.alert(
ng-class="{\
'alert-danger': entry.level == 'error',\
'alert-warning': entry.level == 'warning',\
'alert-info': entry.level == 'typesetting'\
}"
ng-click="openInEditor(entry)"
)
span.line-no
span(ng-show="entry.file") {{ entry.file }}
@ -49,9 +84,50 @@ div.full-size(ng-controller="PdfController")
p
.pull-right
a.btn.btn-default.btn-sm(href) Other logs &amp; files
| &nbsp;
a.btn.btn-default.btn-sm(href, tooltip="Clear cached files", tooltip-placement="top")
a.btn.btn-default.btn-sm(
href,
tooltip="Clear cached files",
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="openClearCacheModal()"
)
i.fa.fa-trash-o
a.btn.btn-info.btn-sm(href) View Raw Logs
| &nbsp;
div.dropdown(style="display: inline-block;")
a.btn.btn-default.btn-sm(
href
data-toggle="dropdown"
)
| Other logs &amp; files
span.caret
ul.dropdown-menu.dropdown-menu-right
li(ng-repeat="file in pdf.outputFiles")
a(
href="/project/{{project_id}}/output/{{file.path}}"
target="_blank"
ng-click="openOutputFile(file)"
) {{ file.name }}
a.btn.btn-info.btn-sm(href, ng-click="toggleRawLog()")
span(ng-show="!pdf.showRawLog") View Raw Logs
span(ng-show="pdf.showRawLog") Hide Raw Logs
pre(ng-bind="pdf.rawLog", ng-show="pdf.showRawLog")
script(type='text/ng-template', id='clearCacheModalTemplate')
.modal-header
h3 Clear cache?
.modal-body
p This will clear all hidden LaTeX files (.aux, .bbl, etc) from our compile server.
| You generally don't need to do this unless you're having trouble with references.
p Your project files will not be deleted or changed.
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
ng-disabled="state.inflight"
) Cancel
button.btn.btn-info(
ng-click="clear()"
ng-disabled="state.inflight"
)
span(ng-show="!state.inflight") Clear cache
span(ng-show="state.inflight") Clearing...

View file

@ -0,0 +1,125 @@
script(type='text/ng-template', id='shareProjectModalTemplate')
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 Share Project
.modal-body.modal-body-share
.container-fluid
.row.public-access-level(ng-show="project.publicAccesLevel == 'private'")
.col-md-12.text-center
| This project is private and can only be accessed by the people below.
| &nbsp;&nbsp;
a(
href
ng-click="openMakePublicModal()"
) Make Public
.row.public-access-level(ng-show="project.publicAccesLevel != 'private'")
.col-md-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.
| &nbsp;&nbsp;
a(
href
ng-click="openMakePrivateModal()"
) Make Private
.row.project-member
.col-md-8 {{ project.owner.email }}
.text-right(
ng-class="{'col-md-3': project.members.length > 0, 'col-md-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
span(ng-show="member.privileges == 'readAndWrite'") Can Edit
span(ng-show="member.privileges == 'readOnly'") Read Only
.col-md-1
a(
href
tooltip="Remove collaborator"
tooltip-placement="bottom"
ng-click="removeMember(member)"
)
i.fa.fa-times
.row.invite-controls
form(ng-show="canAddCollaborators")
.small Share with your collaborators
.form-group
input.form-control(
type="email"
placeholder="Enter email address..."
ng-model="inputs.email"
focus-on="open"
)
.form-group
.pull-right
select.privileges.form-control(
ng-model="inputs.privileges"
name="privileges"
)
option(value="readAndWrite") Can Edit
option(value="readOnly") Read Only
| &nbsp;&nbsp;
button.btn.btn-info(
type="submit"
ng-click="addMember()"
) Share
div.text-center(ng-hide="canAddCollaborators")
p You need to upgrade your account to add more collaborators.
p
a.btn.btn-info(href, ng-click="startFreeTrial()") Start Free Trial
p.small(ng-show="state.startedFreeTrial")
| Please refresh this page after starting your free trial.
.modal-footer
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") {{ state.error }}
button.btn.btn-primary(
ng-click="done()"
) Done
script(type="text/ng-template", id="makePublicModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 Make project public?
.modal-body.modal-body-share
p If you make your project public then anyone with the URL will be able to access it.
p
select.form-control(
ng-model="inputs.privileges"
name="privileges"
)
option(value="readAndWrite") Allow public editing
option(value="readOnly") Allow public read only access
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
) Cancel
button.btn.btn-info(
ng-click="makePublic()"
) Make public
script(type="text/ng-template", id="makePrivateModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 Make project private?
.modal-body.modal-body-share
p If you make your project public then only the people you choose to share it with will have access.
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
) Cancel
button.btn.btn-info(
ng-click="makePrivate()"
) Make private

View file

@ -71,7 +71,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
theme="settings.theme",
font-size="settings.fontSize",
text="trackChanges.diff.text",
annotations="trackChanges.diff.annotations",
highlights="trackChanges.diff.highlights",
read-only="true",
resize-on="layout:main:resize"
)

View file

@ -3,10 +3,11 @@ define [
"ide/file-tree/FileTreeManager"
"ide/connection/ConnectionManager"
"ide/editor/EditorManager"
"ide/settings/SettingsManager"
"ide/online-users/OnlineUsersManager"
"ide/track-changes/TrackChangesManager"
"ide/pdf/PdfManager"
"ide/settings/index"
"ide/share/index"
"ide/directives/layout"
"ide/services/ide"
"directives/focus"
@ -18,7 +19,6 @@ define [
FileTreeManager
ConnectionManager
EditorManager
SettingsManager
OnlineUsersManager
TrackChangesManager
PdfManager
@ -42,6 +42,7 @@ define [
view: "editor"
}
$scope.user = window.user
$scope.settings = window.userSettings
window._ide = ide
@ -51,7 +52,6 @@ define [
ide.connectionManager = new ConnectionManager(ide, $scope)
ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(ide, $scope)
ide.settingsManager = new SettingsManager(ide, $scope)
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
ide.trackChangesManager = new TrackChangesManager(ide, $scope)
ide.pdfManager = new PdfManager(ide, $scope)

View file

@ -10,9 +10,15 @@ define [
spacing_open: 24
spacing_closed: 24
onresize: () =>
console.log "Triggering", "layout:#{name}:resize", name
scope.$broadcast "layout:#{name}:resize"
#maskIframesOnResize: true
onResize()
maskIframesOnResize: scope.$eval(
attrs.maskIframesOnResize or "false"
)
onResize = () ->
state = element.layout().readState()
scope.$broadcast "layout:#{name}:resize", state
repositionControls()
# Restore previously recorded state
if (state = $.localStorage("layout.#{name}"))?
@ -28,4 +34,13 @@ define [
# Save state when exiting
$(window).unload () ->
$.localStorage("layout.#{name}", element.layout().readState())
repositionControls = () ->
state = element.layout().readState()
if state.east?
element.find(".ui-layout-resizer-controls").css({
position: "absolute"
right: state.east.size
"z-index": 10
})
}

View file

@ -10,6 +10,7 @@ define [
open_doc_id: null
opening: true
cursorPosition: null
gotoLine: null
}
@$scope.$on "entity:selected", (event, entity) =>
@ -33,25 +34,33 @@ define [
openDoc: (doc, options = {}) ->
@$scope.ui.view = "editor"
done = () =>
if options.gotoLine?
@$scope.editor.gotoLine = options.gotoLine
console.log "Trying to open doc", doc.id
return if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
@$scope.$apply () =>
done()
return
@$scope.editor.open_doc_id = doc.id
console.log "Actually opening doc", doc.id
$.localStorage "doc.open_id.#{@$scope.project_id}", doc.id
@ide.fileTreeManager.selectEntity(doc)
@$scope.editor.opening = true
@_openNewDocument doc, (error, sharejs_doc) =>
console.log "OPENED DOC", error, sharejs_doc
if error?
@ide.showGenericServerErrorMessage()
return
@$scope.$broadcast "doc:opened"
@$scope.$apply () =>
@$scope.editor.opening = false
@$scope.editor.sharejs_doc = sharejs_doc
done()
_openNewDocument: (doc, callback = (error, sharejs_doc) ->) ->
current_sharejs_doc = @$scope.editor.sharejs_doc
@ -93,3 +102,9 @@ define [
lastUpdated: () ->
@$scope.editor.last_updated
getCurrentDocValue: () ->
@$scope.editor.sharejs_doc?.getSnapshot()
getCurrentDocId: () ->
@$scope.editor.open_doc_id

View file

@ -11,6 +11,21 @@ define [], () ->
@gotoStoredPosition()
@editor.on "changeSelection", () =>
cursor = @editor.getCursorPosition()
console.log "Updating cursor position", cursor
@$scope.$apply () =>
@$scope.cursorPosition = cursor
@$scope.$watch "gotoLine", (value) =>
console.log "Going to line", value
if value?
setTimeout () =>
@gotoLine(value)
@$scope.$apply () =>
@$scope.gotoLine = null
, 0
onScrollTopChange: (event) ->
if !@ignoreCursorPositionChanges and doc_id = @$scope.sharejsDoc?.doc_id
docPosition = $.localStorage("doc.position.#{doc_id}") || {}
@ -30,4 +45,8 @@ define [], () ->
@ignoreCursorPositionChanges = true
@editor.moveCursorToPosition(pos.cursorPosition or {row: 0, column: 0})
@editor.getSession().setScrollTop(pos.scrollTop or 0)
delete @ignoreCursorPositionChanges
delete @ignoreCursorPositionChanges
gotoLine: (line) ->
@editor.gotoLine(line)
@editor.focus()

View file

@ -4,7 +4,7 @@ define [
"ide/editor/undo/UndoManager"
"ide/editor/auto-complete/AutoCompleteManager"
"ide/editor/spell-check/SpellCheckManager"
"ide/editor/annotations/AnnotationsManager"
"ide/editor/highlights/HighlightsManager"
"ide/editor/cursor-position/CursorPositionManager"
"ace/keyboard/vim"
"ace/keyboard/emacs"
@ -26,9 +26,11 @@ define [
lastUpdated: "="
spellCheckLanguage: "="
cursorPosition: "="
annotations: "="
highlights: "="
text: "="
readOnly: "="
gotoLine: "="
annotations: "="
}
link: (scope, element, attrs) ->
# Don't freak out if we're already in an apply callback
@ -63,12 +65,6 @@ define [
scope.$on event, () ->
editor.resize()
editor.on "changeSelection", () ->
cursor = editor.getCursorPosition()
scope.$apply () ->
if scope.cursorPosition?
scope.cursorPosition = cursor
scope.$watch "theme", (value) ->
editor.setTheme("ace/theme/#{value}")
@ -100,15 +96,26 @@ define [
session.setUseWrapMode(true)
session.setMode(new LatexMode())
scope.$watch "annotations", (annotations) ->
console.log "SETTING ANNOTATIONS", annotations
if annotations?
session = editor.getSession()
session.setAnnotations annotations
scope.$watch "readOnly", (value) ->
editor.setReadOnly !!value
resetSession = () ->
session = editor.getSession()
session.setUseWrapMode(true)
session.setMode(new LatexMode())
session.setAnnotations scope.annotations
attachToAce = (sharejs_doc) ->
lines = sharejs_doc.getSnapshot().split("\n")
editor.setSession(new EditSession(lines))
resetSession()
session = editor.getSession()
session.setUseWrapMode(true)
session.setMode(new LatexMode())
autoCompleteManager.bindToSession(session)
annotationsManager.redrawAnnotations()

View file

@ -3,7 +3,7 @@ define [
], () ->
Range = require("ace/range").Range
class AnnotationsManager
class HighlightsManager
constructor: (@$scope, @editor, @element) ->
@markerIds = []
@labels = []
@ -18,7 +18,7 @@ define [
text: ""
}
@$scope.$watch "annotations", (value) =>
@$scope.$watch "highlights", (value) =>
@redrawAnnotations()
@$scope.$watch "theme", (value) =>
@ -33,12 +33,13 @@ define [
@_clearMarkers()
@_clearLabels()
for annotation in @$scope.annotations or []
for annotation in @$scope.highlights or []
do (annotation) =>
colorScheme = @_getColorScheme(annotation.hue)
if annotation.cursor?
console.log "DRAWING CURSOR", annotation
@labels.push {
text: annotation.text
text: annotation.label
range: new Range(
annotation.cursor.row, annotation.cursor.column,
annotation.cursor.row, annotation.cursor.column + 1

View file

@ -88,6 +88,25 @@ define [
return null
findEntityByPath: (path) ->
@_findEntityByPathInFolder @$scope.rootFolder, path
_findEntityByPathInFolder: (folder, path) ->
parts = path.split("/")
name = parts.shift()
rest = parts.join("/")
if name == "."
return @_findEntityByPathInFolder(folder, rest)
for entity in folder.children
if entity.name == name
if rest == ""
return entity
else if entity.type == "folder"
return @_findEntityByPathInFolder(entity, rest)
return null
forEachEntity: (callback = (entity, parent_folder) ->) ->
@_forEachEntityInFolder(@$scope.rootFolder, callback)
@ -100,6 +119,26 @@ define [
if entity.children?
@_forEachEntityInFolder(entity, callback)
getEntityPath: (entity) ->
@_getEntityPathInFolder @$scope.rootFolder, entity
_getEntityPathInFolder: (folder, entity) ->
for child in folder.children or []
if child == entity
return entity.name
else if child.type == "folder"
path = @_getEntityPathInFolder(child, entity)
if path?
return child.name + "/" + path
return null
getRootDocDirname: () ->
rootDoc = @findEntityById @$scope.project.rootDoc_id
return if !rootDoc?
path = @getEntityPath(rootDoc)
return if !path?
return path.split("/").slice(0, -1).join("/")
# forEachFolder: (callback) ->
# @forEachEntity (entity) ->
# if entity.type == "folder"

View file

@ -4,7 +4,7 @@ define [
class OnlineUsersManager
constructor: (@ide, @$scope) ->
@$scope.onlineUsers = {}
@$scope.onlineUserCursorAnnotations = {}
@$scope.onlineUserCursorHighlights = {}
@$scope.$watch "editor.cursorPosition", (position) =>
if position?
@ -23,12 +23,12 @@ define [
updateCursorHighlights: () ->
console.log "UPDATING CURSOR HIGHLIGHTS"
@$scope.onlineUserCursorAnnotations = {}
@$scope.onlineUserCursorHighlights = {}
for client_id, client of @$scope.onlineUsers
doc_id = client.doc_id
continue if !doc_id?
@$scope.onlineUserCursorAnnotations[doc_id] ||= []
@$scope.onlineUserCursorAnnotations[doc_id].push {
@$scope.onlineUserCursorHighlights[doc_id] ||= []
@$scope.onlineUserCursorHighlights[doc_id].push {
label: client.name
cursor:
row: client.row
@ -38,6 +38,7 @@ define [
UPDATE_INTERVAL: 500
sendCursorPositionUpdate: () ->
console.log "SENDING CURSOR POSITION UPDATE", @$scope.editor.cursorPosition
if !@cursorUpdateTimeout?
@cursorUpdateTimeout = setTimeout ()=>
position = @$scope.editor.cursorPosition

View file

@ -4,5 +4,17 @@ define [
], () ->
class PdfManager
constructor: (@ide, @$scope) ->
# All the logic actually happens in the controller
@$scope.pdf =
url: null # Pdf Url
error: false # Server error
timeout: false # Server timed out
failure: false # PDF failed to compile
compiling: false
uncompiled: true
logEntries: []
logEntryAnnotations: {}
rawLog: ""
view: null # 'pdf' 'logs'
showRawLog: false
highlights: []
position: null

View file

@ -2,36 +2,35 @@ define [
"base"
"libs/latex-log-parser"
], (App, LogParser) ->
App.controller "PdfController", ["$scope", "$http", ($scope, $http) ->
$scope.pdf =
url: null # Pdf Url
view: null # 'pdf' 'logs'
error: false # Server error
timeout: false # Server timed out
failure: false # PDF failed to compile
compiling: false
logEntries: []
App.controller "PdfController", ["$scope", "$http", "ide", "$modal", "synctex", ($scope, $http, ide, $modal, synctex) ->
autoCompile = true
$scope.$on "doc:opened", () ->
return if !autoCompile
autoCompile = false
$scope.recompile(isAutoCompile: true)
sendCompileRequest = (options = {}) ->
url = "/project/#{$scope.project_id}/compile"
if options.isAutoCompile
url += "?auto_compile=true"
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
settingsOverride:
rootDoc_id: options.rootDocOverride_id or null
_csrf: window.csrfToken
}
parseCompileResponse = (response) ->
# Reset everything
$scope.pdf.error = false
$scope.pdf.timedout = false
$scope.pdf.failure = false
$scope.pdf.url = null
$scope.pdf.error = false
$scope.pdf.timedout = false
$scope.pdf.failure = false
$scope.pdf.uncompiled = false
$scope.pdf.url = null
if response.status == "timedout"
$scope.pdf.timedout = true
else if response.status == "autocompile-backoff"
# Nothing to do
$scope.pdf.uncompiled = true
else if response.status == "failure"
$scope.pdf.failure = true
fetchLogs()
@ -39,35 +38,241 @@ define [
$scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}"
fetchLogs()
IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
$scope.pdf.outputFiles = []
for file in response.outputFiles
if IGNORE_FILES.indexOf(file.path) == -1
# Turn 'output.blg' into 'blg file'.
if file.path.match(/^output\./)
file.name = "#{file.path.replace(/^output\./, "")} file"
else
file.name = file.path
$scope.pdf.outputFiles.push file
fetchLogs = () ->
$http.get "/project/#{$scope.project_id}/output/output.log"
.success (log) ->
$scope.pdf.rawLog = log
logEntries = LogParser.parse(log, ignoreDuplicates: true)
$scope.pdf.logEntries = logEntries
$scope.pdf.logEntries.all = logEntries.errors.concat(logEntries.warnings).concat(logEntries.typesetting)
for entry in logEntries.all
entry.file = entry.file.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
entry.file = entry.file.replace(/^\/compile\//, "")
console.log "LOG", logEntries
$scope.recompile = () ->
console.log "Recompiling"
$scope.pdf.logEntryAnnotations = {}
for entry in logEntries.all
entry.file = normalizeFilePath(entry.file)
entity = ide.fileTreeManager.findEntityByPath(entry.file)
if entity?
$scope.pdf.logEntryAnnotations[entity.id] ||= []
$scope.pdf.logEntryAnnotations[entity.id].push {
row: entry.line - 1
type: if entry.level == "error" then "error" else "warning"
text: entry.message
}
.error () ->
$scope.pdf.logEntries = []
$scope.pdf.rawLog = ""
getRootDocOverride_id = () ->
doc = ide.editorManager.getCurrentDocValue()
return null if !doc?
for line in doc.split("\n")
match = line.match /(.*)\\documentclass/
if match and !match[1].match /%/
return ide.editorManager.getCurrentDocId()
return null
normalizeFilePath = (path) ->
path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
path = path.replace(/^\/compile\//, "")
rootDocDirname = ide.fileTreeManager.getRootDocDirname()
if rootDocDirname?
path = path.replace(/^\.\//, rootDocDirname + "/")
return path
$scope.recompile = (options = {}) ->
console.log "Recompiling", options
return if $scope.pdf.compiling
$scope.pdf.compiling = true
sendCompileRequest()
options.rootDocOverride_id = getRootDocOverride_id()
sendCompileRequest(options)
.success (data) ->
$scope.pdf.view = "pdf"
$scope.pdf.compiling = false
parseCompileResponse(data)
.error () ->
$scope.pdf.compiling = false
$scope.pdf.error = true
$scope.clearCache = () ->
$http {
url: "/project/#{$scope.project_id}/output"
method: "DELETE"
headers:
"X-Csrf-Token": window.csrfToken
}
$scope.toggleLogs = () ->
if $scope.pdf.view == "pdf"
if !$scope.pdf.view? or $scope.pdf.view == "pdf"
$scope.pdf.view = "logs"
else
$scope.pdf.view = "pdf"
$scope.showPdf = () ->
$scope.pdf.view = "pdf"
$scope.toggleRawLog = () ->
$scope.pdf.showRawLog = !$scope.pdf.showRawLog
$scope.openOutputFile = (file) ->
window.open("/project/#{$scope.project_id}/output/#{file.path}")
$scope.openClearCacheModal = () ->
modalInstance = $modal.open(
templateUrl: "clearCacheModalTemplate"
controller: "ClearCacheModalController"
scope: $scope
)
$scope.syncToCode = (position) ->
console.log "SYNCING VIA DBL CLICK", position
synctex
.syncToCode(position)
.then (data) ->
{doc, line} = data
ide.editorManager.openDoc(doc, gotoLine: line)
]
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
synctex =
syncToPdf: (cursorPosition) ->
deferred = $q.defer()
doc_id = ide.editorManager.getCurrentDocId()
if !doc_id?
deferred.reject()
return deferred.promise
doc = ide.fileTreeManager.findEntityById(doc_id)
if !doc?
deferred.reject()
return deferred.promise
path = ide.fileTreeManager.getEntityPath(doc)
if !path?
deferred.reject()
return deferred.promise
# If the root file is folder/main.tex, then synctex sees the
# path as folder/./main.tex
rootDocDirname = ide.fileTreeManager.getRootDocDirname()
if rootDocDirname? and rootDocDirname != ""
path = path.replace(RegExp("^#{rootDocDirname}"), "#{rootDocDirname}/.")
{row, column} = cursorPosition
$http({
url: "/project/#{ide.project_id}/sync/code",
method: "GET",
params: {
file: path
line: row + 1
column: column
}
})
.success (data) ->
deferred.resolve(data.pdf or [])
.error (error) ->
deferred.reject(error)
return deferred.promise
syncToCode: (position, options = {}) ->
deferred = $q.defer()
if !position?
deferred.reject()
return deferred.promise
# It's not clear exactly where we should sync to if it wasn't directly
# clicked on, but a little bit down from the very top seems best.
if options.includeVisualOffset
position.offset.top = position.offset.top + 80
$http({
url: "/project/#{ide.project_id}/sync/pdf",
method: "GET",
params: {
page: position.page + 1
h: position.offset.left.toFixed(2)
v: position.offset.top.toFixed(2)
}
})
.success (data) ->
if data.code? and data.code.length > 0
doc = ide.fileTreeManager.findEntityByPath(data.code[0].file)
return if !doc?
deferred.resolve({doc: doc, line: data.code[0].line})
.error (error) ->
deferred.reject(error)
return deferred.promise
return synctex
]
App.controller "PdfSynctexController", ["$scope", "synctex", "ide", ($scope, synctex, ide) ->
$scope.showControls = true
$scope.$on "layout:pdf:resize", (event, data) ->
console.log "RESIZE DATA", data.east
if data.east.initClosed
$scope.showControls = false
else
$scope.showControls = true
setTimeout () ->
$scope.$digest()
, 0
$scope.syncToPdf = () ->
synctex
.syncToPdf($scope.editor.cursorPosition)
.then (highlights) ->
$scope.pdf.highlights = highlights
$scope.syncToCode = () ->
synctex
.syncToCode($scope.pdf.position, includeVisualOffset: true)
.then (data) ->
{doc, line} = data
console.log "OPENING DOC", doc, line
ide.editorManager.openDoc(doc, gotoLine: line)
]
App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
$scope.openInEditor = (entry) ->
console.log "OPENING", entry.file, entry.line
entity = ide.fileTreeManager.findEntityByPath(entry.file)
return if !entity? or entity.type != "doc"
if entry.line?
line = entry.line
ide.editorManager.openDoc(entity, gotoLine: line)
]
App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) ->
$scope.state =
inflight: false
$scope.clear = () ->
$scope.state.inflight = true
$scope
.clearCache()
.then () ->
$scope.state.inflight = false
$modalInstance.close()
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
]

View file

@ -28,12 +28,16 @@ define [
return {
scope: {
"pdfSrc": "="
"highlights": "="
"position": "="
"dblClickCallback": "="
}
link: (scope, element, attrs) ->
pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
textLayerBuilder: TextLayerBuilder
annotationsLayerBuilder: AnnotationsLayerBuilder
highlightsLayerBuilder: HighlightsLayerBuilder
ondblclick: (e) -> onDoubleClick(e)
logLevel: PDFListView.Logger.DEBUG
pdfListView.listView.pageWidthOffset = 20
pdfListView.listView.pageHeightOffset = 20
@ -58,6 +62,8 @@ define [
if (position = $.localStorage("pdf.position.#{attrs.key}"))
pdfListView.setPdfPosition(position)
scope.position = pdfListView.getPdfPosition(true)
$(window).unload () =>
$.localStorage "pdf.scale", {
scaleMode: pdfListView.getScaleMode()
@ -69,7 +75,14 @@ define [
scope.flashControls = true
$timeout () ->
scope.flashControls = false
, 1000
, 1000
element.find(".pdfjs-viewer").scroll () ->
console.log "UPDATING POSITION", pdfListView.getPdfPosition(true)
scope.position = pdfListView.getPdfPosition(true)
onDoubleClick = (e) ->
scope.dblClickCallback?(page: e.page, offset: { top: e.y, left: e.x })
scope.$watch "pdfSrc", (url) ->
if url
@ -85,6 +98,35 @@ define [
initializePosition()
flashControls()
scope.$watch "highlights", (areas) ->
console.log "UPDATING HIGHLIGHTS", areas
return if !areas?
highlights = for area in areas or []
{
page: area.page - 1
highlight:
left: area.h
top: area.v
height: area.height
width: area.width
}
if highlights.length > 0
first = highlights[0]
pdfListView.setPdfPosition({
page: first.page
offset:
left: first.highlight.left
top: first.highlight.top - 80
}, true)
pdfListView.clearHighlights()
pdfListView.setHighlights(highlights, true)
setTimeout () =>
pdfListView.clearHighlights()
, 1000
scope.fitToHeight = () ->
pdfListView.setToFitHeight()

View file

@ -1,55 +0,0 @@
define [], () ->
class SettingsManager
constructor: (@ide, @$scope) ->
@$scope.settings = window.userSettings
if @$scope.settings.mode not in ["default", "vim", "emacs"]
@$scope.settings.mode = "default"
@$scope.$watch "settings.theme", (theme, oldTheme) =>
if theme != oldTheme
@saveSettings({theme: theme})
@$scope.$watch "settings.fontSize", (fontSize, oldFontSize) =>
if fontSize != oldFontSize
@saveSettings({fontSize: parseInt(fontSize, 10)})
@$scope.$watch "settings.mode", (mode, oldMode) =>
if mode != oldMode
@saveSettings({mode: mode})
@$scope.$watch "settings.autoComplete", (autoComplete, oldAutoComplete) =>
if autoComplete != oldAutoComplete
@saveSettings({autoComplete: autoComplete})
@$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
return if @ignoreUpdates
if oldLanguage? and language != oldLanguage
@saveProjectSettings({spellCheckLanguage: language})
# Also set it as the default for the user
@saveSettings({spellCheckLanguage: language})
@$scope.$watch "project.compiler", (compiler, oldCompiler) =>
return if @ignoreUpdates
if oldCompiler? and compiler != oldCompiler
@saveProjectSettings({compiler: compiler})
@ide.socket.on "compilerUpdated", (compiler) =>
@ignoreUpdates = true
@$scope.$apply () =>
@$scope.project.compiler = compiler
delete @ignoreUpdates
@ide.socket.on "spellCheckLanguageUpdated", (languageCode) =>
@ignoreUpdates = true
@$scope.$apply () =>
@$scope.project.spellCheckLanguage = languageCode
delete @ignoreUpdates
saveSettings: (data) ->
data._csrf = window.csrfToken
@ide.$http.post "/user/settings", data
saveProjectSettings: (data) ->
data._csrf = window.csrfToken
@ide.$http.post "/project/#{@ide.project_id}/settings", data

View file

@ -0,0 +1,51 @@
define [
"base"
], (App) ->
App.controller "SettingsController", ["$scope", "settings", "ide", ($scope, settings, ide) ->
if $scope.settings.mode not in ["default", "vim", "emacs"]
$scope.settings.mode = "default"
$scope.$watch "settings.theme", (theme, oldTheme) =>
if theme != oldTheme
settings.saveSettings({theme: theme})
$scope.$watch "settings.fontSize", (fontSize, oldFontSize) =>
if fontSize != oldFontSize
settings.saveSettings({fontSize: parseInt(fontSize, 10)})
$scope.$watch "settings.mode", (mode, oldMode) =>
if mode != oldMode
settings.saveSettings({mode: mode})
$scope.$watch "settings.autoComplete", (autoComplete, oldAutoComplete) =>
if autoComplete != oldAutoComplete
settings.saveSettings({autoComplete: autoComplete})
$scope.$watch "settings.pdfViewer", (pdfViewer, oldPdfViewer) =>
if pdfViewer != oldPdfViewer
settings.saveSettings({pdfViewer: pdfViewer})
$scope.$watch "project.spellCheckLanguage", (language, oldLanguage) =>
return if @ignoreUpdates
if oldLanguage? and language != oldLanguage
settings.saveProjectSettings({spellCheckLanguage: language})
# Also set it as the default for the user
settings.saveSettings({spellCheckLanguage: language})
$scope.$watch "project.compiler", (compiler, oldCompiler) =>
return if @ignoreUpdates
if oldCompiler? and compiler != oldCompiler
settings.saveProjectSettings({compiler: compiler})
ide.socket.on "compilerUpdated", (compiler) =>
@ignoreUpdates = true
$scope.$apply () =>
$scope.project.compiler = compiler
delete @ignoreUpdates
ide.socket.on "spellCheckLanguageUpdated", (languageCode) =>
@ignoreUpdates = true
$scope.$apply () =>
$scope.project.spellCheckLanguage = languageCode
delete @ignoreUpdates
]

View file

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

View file

@ -0,0 +1,14 @@
define [
"base"
], (App) ->
App.factory "settings", ["ide", (ide) ->
return {
saveSettings: (data) ->
data._csrf = window.csrfToken
ide.$http.post "/user/settings", data
saveProjectSettings: (data) ->
data._csrf = window.csrfToken
ide.$http.post "/project/#{ide.project_id}/settings", data
}
]

View file

@ -0,0 +1,11 @@
define [
"base"
], (App) ->
App.controller "ShareController", ["$scope", "$modal", ($scope, $modal) ->
$scope.openShareProjectModal = () ->
$modal.open(
templateUrl: "shareProjectModalTemplate"
controller: "ShareProjectModalController"
scope: $scope
)
]

View file

@ -0,0 +1,104 @@
define [
"base"
], (App) ->
App.controller "ShareProjectModalController", ["$scope", "$modalInstance", "$timeout", "projectMembers", "$modal", ($scope, $modalInstance, $timeout, projectMembers, $modal) ->
$scope.inputs = {
privileges: "readAndWrite"
email: ""
}
$scope.state = {
error: null
inflight: false
startedFreeTrial: false
}
$modalInstance.opened.then () ->
$timeout () ->
$scope.$broadcast "open"
, 200
INFINITE_COLLABORATORS = -1
$scope.$watch "project.members.length", (noOfMembers) ->
allowedNoOfMembers = $scope.project.features.collaborators
$scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS
$scope.addMember = () ->
console.log "EMAIL", $scope.inputs.email
return if !$scope.inputs.email? or $scope.inputs.email == ""
$scope.state.error = null
$scope.state.inflight = true
projectMembers
.addMember($scope.inputs.email, $scope.inputs.privileges)
.then (user) ->
$scope.state.inflight = false
$scope.inputs.email = ""
console.log "GOT USER", user
$scope.project.members.push user
.catch () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
$scope.removeMember = (member) ->
$scope.state.error = null
$scope.state.inflight = true
projectMembers
.removeMember(member)
.then () ->
$scope.state.inflight = false
index = $scope.project.members.indexOf(member)
return if index == -1
$scope.project.members.splice(index, 1)
.catch () ->
$scope.state.inflight = false
$scope.state.error = "Sorry, something went wrong :("
$scope.startFreeTrial = () ->
ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "projectMembers")
window.open("/user/subscription/plans")
$scope.state.startedFreeTrial = true
$scope.openMakePublicModal = () ->
$modal.open {
templateUrl: "makePublicModalTemplate"
controller: "MakePublicModalController"
scope: $scope
}
$scope.openMakePrivateModal = () ->
$modal.open {
templateUrl: "makePrivateModalTemplate"
controller: "MakePrivateModalController"
scope: $scope
}
$scope.done = () ->
$modalInstance.close()
$scope.cancel = () ->
$modalInstance.dismiss()
]
App.controller "MakePublicModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) ->
$scope.inputs = {
privileges: "readAndWrite"
}
$scope.makePublic = () ->
$scope.project.publicAccesLevel = $scope.inputs.privileges
settings.saveProjectSettings({publicAccessLevel: $scope.inputs.privileges})
$modalInstance.close()
$scope.cancel = () ->
$modalInstance.dismiss()
]
App.controller "MakePrivateModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) ->
$scope.makePrivate = () ->
$scope.project.publicAccesLevel = "private"
settings.saveProjectSettings({publicAccessLevel: "private"})
$modalInstance.close()
$scope.cancel = () ->
$modalInstance.dismiss()
]

View file

@ -0,0 +1,5 @@
define [
"ide/share/controllers/ShareController"
"ide/share/controllers/ShareProjectModalController"
"ide/share/services/projectMembers"
], () ->

View file

@ -0,0 +1,30 @@
define [
"base"
], (App) ->
App.factory "projectMembers", ["ide", "$q", (ide, $q) ->
return {
removeMember: (member) ->
deferred = $q.defer()
ide.socket.emit "removeUserFromProject", member._id, (error) =>
if error?
return deferred.reject(error)
deferred.resolve()
return deferred.promise
addMember: (email, privileges) ->
deferred = $q.defer()
ide.socket.emit "addUserToProject", email, privileges, (error, user) =>
if error?
return deferred.reject(error)
if !user
deferred.reject()
else
deferred.resolve(user)
return deferred.promise
}
]

View file

@ -110,9 +110,9 @@ define [
.get(url)
.success (data) =>
diff.loading = false
{text, annotations} = @_parseDiff(data)
{text, highlights} = @_parseDiff(data)
diff.text = text
diff.annotations = annotations
diff.highlights = highlights
.error () ->
diff.loading = false
diff.error = true
@ -129,7 +129,7 @@ define [
_parseDiff: (diff) ->
row = 0
column = 0
annotations = []
highlights = []
text = ""
for entry, i in diff.diff or []
content = entry.u or entry.i or entry.d
@ -162,19 +162,19 @@ define [
name = "you"
date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
if entry.i?
annotations.push {
highlights.push {
label: "Added by #{name} on #{date}"
highlight: range
hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user.id)
}
else if entry.d?
annotations.push {
highlights.push {
label: "Deleted by #{name} on #{date}"
strikeThrough: range
hue: @ide.onlineUsersManager.getHueForUserId(entry.meta.user.id)
}
return {text, annotations}
return {text, highlights}
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.trackChanges.updates[@$scope.trackChanges.updates.length - 1]

View file

@ -3,6 +3,7 @@
@import "./editor/toolbar.less";
@import "./editor/left-menu.less";
@import "./editor/pdf.less";
@import "./editor/share.less";
.full-size {
position: absolute;
@ -69,6 +70,7 @@
background-position: bottom left;
}
.remote-cursor {
position: absolute;
border-left: 2px solid transparent;
.nubbin {
height: 5px;

View file

@ -1,9 +1,24 @@
.pdf-viewer, .logs {
.pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled {
.full-size;
top: 58px;
}
.pdf-logs, .pdf-errors, .pdf-uncompiled {
padding: @line-height-computed / 2;
}
.pdf-uncompiled {
.fa {
color: @blue;
}
}
.pdf-viewer {
iframe {
width: 100%;
height: 100%;
border: none;
}
.pdfjs-viewer {
.full-size;
background-color: @gray-lighter;
@ -59,20 +74,27 @@
}
.log-btn {
position: relative;
.label {
margin-left: .4em;
padding: .2em .4em .3em;
vertical-align: text-bottom;
line-height: inherit;
position: absolute;
top: 0;
right: 0;
padding: .15em .6em .2em;
font-size: 60%;
}
&.active, &:active {
.label {
display: none;
}
}
}
.logs {
padding: @line-height-computed / 2;
.pdf-logs {
overflow: auto;
.alert {
font-size: 0.9rem;
margin-bottom: @line-height-computed / 2;
cursor: pointer;
.line-no {
float: right;
color: @gray;
@ -88,5 +110,19 @@
//font-family: @font-family-monospace;
}
}
pre {
font-size: 12px;
}
.dropdown {
position: relative;
}
}
.synctex-controls {
padding: 68px 2px 0;
.btn-xs {
line-height: 1.3;
padding: 0 2px 0;
}
}

View file

@ -0,0 +1,50 @@
.modal-body-share {
h3 {
border-bottom: 1px solid @gray-lighter;
padding-bottom: @line-height-computed / 4;
margin: 0;
font-size: 1rem;
}
.project-member, .public-access-level {
padding: (@line-height-computed / 2) 0;
border-bottom: 1px solid @gray-lighter;
font-size: 14px;
}
.public-access-level {
color: @gray;
padding-top: 0;
font-size: 12px;
padding-bottom: @line-height-computed;
}
.project-member {
&:hover {
background-color: @gray-lightest;
}
}
.invite-controls {
.small {
padding: 2px;
}
padding: @line-height-computed / 2;
background-color: @gray-lightest;
margin-top: @line-height-computed / 2;
form {
.form-group {
margin-bottom: @line-height-computed / 2;
&:last-child {
margin-bottom: 0;
}
}
.privileges {
display: inline-block;
width: auto;
height: 30px;
font-size: 14px;
}
}
}
}

View file

@ -126,6 +126,10 @@
.btn-block + .btn-block {
margin-left: 0;
}
.modal-footer-left {
float: left;
}
}
// Scale up the modal

View file

@ -296,6 +296,7 @@ describe "EditorController", ->
@email = "Jane.Doe@example.com"
@priveleges = "readOnly"
@addedUser = { _id: "added-user" }
@ProjectEditorHandler.buildUserModelView = sinon.stub().returns(@addedUser)
@CollaboratorsHandler.addUserToProject = sinon.stub().callsArgWith(3, null, @addedUser)
@EditorRealTimeController.emitToRoom = sinon.stub()
@callback = sinon.stub()
@ -314,9 +315,9 @@ describe "EditorController", ->
@EditorRealTimeController.emitToRoom.calledWith(@project_id, "userAddedToProject", @addedUser).should.equal true
done()
it "should return true to the callback", (done)->
it "should return the user to the callback", (done)->
@EditorController.addUserToProject @project_id, @email, @priveleges, (err, result)=>
result.should.equal true
result.should.equal @addedUser
done()