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

This commit is contained in:
Henry Oswald 2014-03-12 21:33:27 +00:00
commit 52cb1a4dcd
16 changed files with 295 additions and 136 deletions

View file

@ -1,12 +1,20 @@
logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = TrackChangesController =
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
return next(error) if error?
url = settings.apis.trackchanges.url + req.url
logger.log url: url, "proxying to track-changes api"
getReq = request.get(url)
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"

View file

@ -125,6 +125,7 @@ module.exports = class Router
app.get "/project/:Project_id/doc/:doc_id/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.get "/project/:Project_id/doc/:doc_id/diff", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators

View file

@ -140,8 +140,7 @@
button.btn.btn-primary ok
script(type="text/template")#genericModalButtonTemplate
a(href="#",class="btn {{ class }}") {{ text }}
button(class="btn {{ class }}") {{ text }}
script(type="text/template")#editorPanelTemplate
#editorArea(style='display: none;')
@ -441,6 +440,12 @@
div(class='change-description')
div(class='change-date') {{date}}
div {{{users}}}
div(class='restore')
a(href="#") Restore to here
script(type='text/template')#changeListItemUserTemplate
div(class='change-name')
div.color-square(style="background-color: hsl({{hue}}, 100%, 70%);")
span {{name}}

View file

@ -224,7 +224,7 @@ define [
document.on "externalUpdate", () =>
Modal.createModal
title: "Document Updated Externally"
message: "This document was just updated externally (probably via Dropbox). Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
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"
]

View file

@ -145,6 +145,11 @@ define [
path = entity.get("name") + "/" + path
return path
getNameOfEntityId: (entity_id) ->
entity = @getEntity(entity_id)
return if !entity?
return entity.get("name")
# RENAMING
renameSelected: () ->
entity_id = @getSelectedEntityId()

View file

@ -49,4 +49,7 @@ define [
find: (id) ->
@loadedModel ||= {}
return @loadedModel[id]
getAnonymousUser: () ->
return User.findOrBuild("anonymous", { first_name: "Anonymous", email: "anon@sharelatex.com" })
}

View file

@ -143,7 +143,6 @@ define [
@view.showLog()
if outputFiles?
console.log "outputFiles", outputFiles
@view.showOutputFileDownloadLinks(outputFiles)
fetchLogAndUpdateView: (pdfExists) ->

View file

@ -7,7 +7,7 @@ define [
template: $("#changeListTemplate").html()
events:
"scroll" : "loadUntilFull"
"scroll" : () -> @loadUntilFull()
initialize: () ->
@itemViews = []
@ -39,20 +39,13 @@ define [
view.$el.insertBefore(elementAtIndex)
view.on "click", (e, v) =>
@selectedToIndex = index
@selectedFromIndex = index
@resetAllSelectors()
@triggerChangeDiff()
@setSelectionRange(index, index)
view.on "selected:to", (e, v) =>
@selectedToIndex = index
@resetAllSelectors()
@triggerChangeDiff()
@setSelectionRange(@selectedFromIndex, index)
view.on "selected:from", (e, v) =>
@selectedFromIndex = index
@resetAllSelectors()
@triggerChangeDiff()
@setSelectionRange(index, @selectedToIndex)
view.on "mouseenter:to", (e) =>
@hoverToIndex = index
@ -70,18 +63,27 @@ define [
delete @hoverFromIndex
@resetHoverStates()
view.on "click:restore", (e) =>
@trigger "restore", view.model
view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
setSelectionRange: (fromIndex, toIndex) ->
@selectedFromIndex = fromIndex
@selectedToIndex = toIndex
@resetAllSelectors()
@triggerChangeDiff()
resetAllSelectors: () ->
for view, i in @itemViews
view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
resetHoverStates: () ->
if @hoverToIndex?
if @hoverToIndex? and @hoverToIndex != @selectedToIndex
@$("ul").addClass("hover-state")
for view, i in @itemViews
view.resetHoverState(i, @selectedFromIndex, @hoverToIndex)
else if @hoverFromIndex?
else if @hoverFromIndex? and @hoverFromIndex != @selectedFromIndex
@$("ul").addClass("hover-state")
for view, i in @itemViews
view.resetHoverState(i, @hoverFromIndex, @selectedToIndex)
@ -99,23 +101,23 @@ define [
atEndOfListView: ->
@$el.scrollTop() + @$el.height() >= @$(".change-list").height() - 30
loadUntilFull: (e, callback) ->
loadUntilFull: (callback = (error) ->) ->
if (@listShorterThanContainer() or @atEndOfListView()) and not @atEndOfCollection and not @loading
@showLoading()
@hideEmptyMessage()
@collection.fetchNextBatch
error: =>
error: (error) =>
@hideLoading()
@showEmptyMessageIfCollectionEmpty()
callback() if callback?
callback(error)
success: (collection, response) =>
@hideLoading()
if response.updates.length == @collection.batchSize
@loadUntilFull(e, callback)
@loadUntilFull(callback)
else
@atEndOfCollection = true
@showEmptyMessageIfCollectionEmpty()
callback() if callback?
callback()
else
callback() if callback?
@ -152,26 +154,29 @@ define [
@trigger "mouseenter:from", args...
"mouseleave .change-selector-from": (args...) ->
@trigger "mouseleave:from", args...
"click .restore a": "onRestoreClick"
template : $("#changeListItemTemplate").html()
templates:
item: $("#changeListItemTemplate").html()
user: $("#changeListItemUserTemplate").html()
initialize: ->
@render()
render: ->
@$el.html Mustache.to_html(@template, @modelView())
return this
modelView: ->
modelView = {
hue: @model.get("user").hue()
date: moment(parseInt(@model.get("end_ts"), 10)).calendar()
name: @model.get("user").name()
userHtml = for user in @model.get("users")
Mustache.to_html @templates.user, {
hue: user.hue()
name: user.name()
}
# modelView.start_ts = util.formatDate(modelView.start_ts)
# modelView.end_ts = util.formatDate(modelView.end_ts)
return modelView
data = {
date: moment(parseInt(@model.get("end_ts"), 10)).calendar()
users: userHtml.join("")
}
@$el.html Mustache.to_html(@templates.item, data)
return this
onClick: (e) ->
e.preventDefault()
@ -183,6 +188,10 @@ define [
onFromSelectorClick: (e) ->
@trigger "selected:from", e, @
onRestoreClick: (e) ->
e.preventDefault()
@trigger "click:restore", e, @
isSelectedFrom: () ->
@$(".change-selector-from").is(":checked")

View file

@ -2,8 +2,9 @@ define [
"ace/ace"
"ace/mode/latex"
"ace/range"
"moment"
"libs/backbone"
], (Ace, LatexMode, Range)->
], (Ace, LatexMode, Range, moment)->
DiffView = Backbone.View.extend
initialize: () ->
@model.on "change:diff", () => @render()
@ -19,6 +20,7 @@ define [
session.setMode(new LatexMode.Mode())
session.setUseWrapMode(true)
@insertMarkers()
@insertNameTag()
return @
createAceEditor: () ->
@ -44,6 +46,7 @@ define [
insertMarkers: () ->
row = 0
column = 0
@entries = []
for entry, i in @model.get("diff") or []
content = entry.u or entry.i or entry.d
content ||= ""
@ -62,9 +65,12 @@ define [
range = new Range.Range(
startRow, startColumn, endRow, endColumn
)
@addMarker(range, "change-marker-#{i}", entry)
entry.range = range
@addMarker(range, entry)
if entry.i? or entry.d?
@entries.push entry
addMarker: (range, id, entry) ->
addMarker: (range, entry) ->
session = @aceEditor.getSession()
markerBackLayer = @aceEditor.renderer.$markerBack
markerFrontLayer = @aceEditor.renderer.$markerFront
@ -72,10 +78,9 @@ define [
if entry.i? or entry.d?
hue = entry.meta.user.hue()
if entry.i?
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
@_addMarkerWithCustomStyle session, markerBackLayer, range, "inserted-change-background", false, """
background-color : hsl(#{hue}, 70%, 85%);
"""
tag = "Added by #{entry.meta.user.name()}"
if entry.d?
@_addMarkerWithCustomStyle session, markerBackLayer, range, "deleted-change-background", false, """
background-color : hsl(#{hue}, 70%, 95%);
@ -84,13 +89,6 @@ define [
height: #{Math.round(lineHeight/2) - 1}px;
border-bottom: 2px solid hsl(#{hue}, 70%, 40%);
"""
tag = "Deleted by #{entry.meta.user.name()}"
date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm:ss a")
tag += " on #{date}"
@_addNameTag session, id, range, tag, """
background-color : hsl(#{hue}, 70%, 95%);
"""
_addMarkerWithCustomStyle: (session, markerLayer, range, klass, foreground, style) ->
session.addMarker range, klass, (html, range, left, top, config) ->
@ -100,42 +98,65 @@ define [
markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
, foreground
_addNameTag: (session, id, range, content, style) ->
@nameMarkers ||= []
@nameMarkers.push
range: range
id: id
startRange = new Range.Range(
range.start.row, range.start.column
range.start.row, range.start.column + 1
)
session.addMarker startRange, "change-name-marker", (html, range, left, top, config) ->
html.push """
<div
id = '#{id}'
class = 'change-name-marker'
style = '
height: #{config.lineHeight}px;
top: #{top}px;
left: #{left}px;
'
>
<div
class="name" style="
display: none;
bottom: #{config.lineHeight + 3}px;
#{style}
">#{content}</div>
</div>
"""
, true
insertNameTag: () ->
@$ace = $(@aceEditor.renderer.container).find(".ace_scroller")
@$nameTagEl = $("<div class='change-name-marker'></div>")
@$nameTagEl.css({
position: "absolute"
})
@$nameTagEl.hide()
@$ace.append(@$nameTagEl)
_drawNameTag: (entry, position) ->
@$nameTagEl.show()
if entry.i?
text = "Added by #{entry.meta.user.name()}"
else if entry.d?
text = "Deleted by #{entry.meta.user.name()}"
date = moment(parseInt(entry.meta.end_ts, 10)).format("Do MMM YYYY, h:mm a")
text += " on #{date}"
@$nameTagEl.text(text)
position = @aceEditor.renderer.textToScreenCoordinates(position.row, position.column)
offset = @$ace.offset()
position.pageX = position.pageX - offset.left
position.pageY = position.pageY - offset.top
height = @$ace.height()
hue = entry.meta.user.hue()
css = {
"background-color" : "hsl(#{hue}, 70%, 90%)";
}
if position.pageX + @$nameTagEl.width() < @$ace.width()
css["left"] = position.pageX
css["right"] = "auto"
else
css["right"] = 0
css["left"] = "auto"
if position.pageY > 2 * @$nameTagEl.height()
css["bottom"] = height - position.pageY
css["top"] = "auto"
else
css["top"] = position.pageY + @aceEditor.renderer.lineHeight
css["bottom"] = "auto"
@$nameTagEl.css css
_hideNameTag: () ->
@$nameTagEl.hide()
updateVisibleNames: (e) ->
for marker in @nameMarkers or []
if marker.range.contains(e.position.row, e.position.column)
$("##{marker.id}").find(".name").show()
else
$("##{marker.id}").find(".name").hide()
visibleName = false
for entry in @entries or []
if entry.range.contains(e.position.row, e.position.column)
@_drawNameTag(entry, e.position)
visibleName = true
break
if !visibleName
@_hideNameTag()
return DiffView

View file

@ -3,21 +3,23 @@ define [
"track-changes/models/Diff"
"track-changes/ChangeListView"
"track-changes/DiffView"
], (ChangeList, Diff, ChangeListView, DiffView) ->
"utils/Modal"
"moment"
], (ChangeList, Diff, ChangeListView, DiffView, Modal, moment) ->
class TrackChangesManager
template: $("#trackChangesPanelTemplate").html()
constructor: (@ide) ->
@$el = $(@template)
$("#editorWrapper").append(@$el)
@hideEl()
@hide()
@ide.editor.on "change:doc", () =>
@hideEl()
@hide()
@$el.find(".track-changes-close").on "click", (e) =>
e.preventDefault
@hideEl()
@hide()
show: () ->
@project_id = window.userSettings.project_id
@ -29,14 +31,43 @@ define [
el : @$el.find(".change-list-area")
)
@changeListView.render()
@changeListView.loadUntilFull()
@changeListView.loadUntilFull (error) =>
@autoSelectDiff()
@changeListView.on "change_diff", (fromModel, toModel) =>
@showDiff(fromModel, toModel)
@changeListView.on "restore", (change) =>
@restore(change)
@showEl()
autoSelectDiff: () ->
if @changes.models.length == 0
return
# Find all change until the last one we made
fromIndex = null
for change, i in @changes.models
if ide.user in change.get("users")
if i > 0
fromIndex = i - 1
else
fromIndex = 0
break
fromIndex = 0 if !fromIndex
toChange = @changes.models[0]
fromChange = @changes.models[fromIndex]
@showDiff(fromChange, toChange)
@changeListView.setSelectionRange(fromIndex, 0)
showDiff: (fromModel, toModel) ->
@diff = new Diff({
project_id: @project_id
doc_id: @doc_id
from: fromModel.get("version")
to: toModel.get("version")
from: fromModel.get("fromVersion")
to: toModel.get("toVersion")
})
@diffView = new DiffView(
model: @diff
@ -44,14 +75,45 @@ define [
)
@diff.fetch()
@showEl()
showEl: ->
@ide.editor.hide()
@$el.show()
hideEl: () ->
hide: () ->
@ide.editor.show()
@$el.hide()
restore: (change) ->
name = @ide.fileTreeManager.getNameOfEntityId(@doc_id)
date = moment(change.get("start_ts")).format("Do MMM YYYY, h:mm:ss a")
modal = new Modal({
title: "Restore document"
message: "Are you sure you want to restore <strong>#{name}</strong> to before the changes on #{date}?"
buttons: [{
text: "Cancel"
}, {
text: "Restore"
class: "btn-success"
close: false
callback: ($button) =>
$button.text("Restoring...")
$button.prop("disabled", true)
@doRestore change.get("version"), (error) =>
modal.remove()
@hide()
}]
})
doRestore: (version, callback = (error) ->) ->
$.ajax {
url: "/project/#{@project_id}/doc/#{@doc_id}/version/#{version}/restore"
type: "POST"
headers:
"X-CSRF-Token": window.csrfToken
success: () ->
callback()
error: (error) ->
callback(error)
}
return TrackChangesManager

View file

@ -4,9 +4,15 @@ define [
], (User)->
Change = Backbone.Model.extend
parse: (change) ->
return {
model = {
start_ts: change.meta.start_ts
end_ts: change.meta.end_ts
user: User.findOrBuild(change.meta.user.id, change.meta.user)
version: change.v
fromVersion: change.fromV
toVersion: change.toV
}
model.users = []
for user in change.meta.users or []
model.users.push User.findOrBuild(user.id, user)
if model.users.length == 0
model.users.push User.getAnonymousUser()
return model

View file

@ -12,7 +12,7 @@ define [
url = "/project/#{@options.project_id}/doc/#{@options.doc_id}/updates?limit=#{@batchSize}"
if @models.length > 0
last = @models[@models.length - 1]
url += "&to=#{last.get("version") - 1}"
url += "&to=#{last.get("fromVersion") - 1}"
return url
parse: (json) ->

View file

@ -8,6 +8,9 @@ define [
parse: (diff) ->
for entry in diff.diff
if entry.meta? and entry.meta.user?
if entry.meta?
if entry.meta.user?
entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
else
entry.meta.user = User.getAnonymousUser()
return diff

View file

@ -19,7 +19,8 @@ define [
button.on "click", (e) ->
e.preventDefault()
if buttonOptions.callback?
buttonOptions.callback()
buttonOptions.callback(button)
if !buttonOptions.close? or buttonOptions.close
self.remove()
@$el.modal
@ -39,7 +40,6 @@ define [
@$el.find('input').focus()
remove: () ->
@$el.modal("hide")
Backbone.View.prototype.remove.call(this)

View file

@ -16,6 +16,9 @@
left: 0;
right: 0;
bottom: 0;
.ace_active-line, .ace_cursor-layer, .ace_gutter-active-line {
display: none;
}
}
}
@ -28,7 +31,7 @@
background-color: white;
.track-changes-header {
background-color: black;
background-color: #282828;
h3 {
color: white;
padding-left: 8px;
@ -53,29 +56,26 @@
}
}
.deleted-change-background, .deleted-change-foreground, .inserted-change, .change-name-marker {
.deleted-change-background, .deleted-change-foreground, .inserted-change-background, .change-name-marker {
position: absolute;
z-index: 2;
}
.change-name-marker {
.name {
font-size: 0.8em;
padding: 2px 6px;
.border-radius(3px 3px 3px 3px);
position: absolute;
border: 1px solid #999;
left: 0;
}
white-space: pre;
}
ul.change-list {
li {
padding: 6px 4px;
position: relative;
border-bottom: 1px solid #ccc;
cursor: pointer;
min-height: 38px;
.change-selectors {
.change-selector-from {
position: absolute;
@ -98,7 +98,8 @@
}
}
.change-description {
padding-left: 26px;
padding: 6px 4px 6px 30px;
min-height: 38px;
}
.change-name {
font-size: 0.9em;
@ -112,10 +113,22 @@
margin-right: 4px;
margin-bottom: -1px;
}
.restore {
a {
display: block;
padding: 4px;
text-align: center;
border-top: 1px solid #ccc;
}
display: none;
}
&:hover {
background-color: #eaeaea;
}
}
li.loading-changes, li.empty-message {
padding: 6px;
}
li.selected {
background-color: #eaeaea;
.change-selectors {
@ -137,12 +150,16 @@
li.selected-from {
.change-selectors {
.range {
bottom: 10px;
bottom: 37px;
}
.change-selector-from {
opacity: 1;
bottom: 32px;
}
}
.restore {
display: block;
}
}
}
ul.change-list.hover-state {
@ -182,5 +199,12 @@
}
}
}
li.selected-from.hover-selected-from {
.change-selectors {
.range {
bottom: 37px;
}
}
}
}
}

View file

@ -7,15 +7,17 @@ SandboxedModule = require('sandboxed-module')
describe "TrackChangesController", ->
beforeEach ->
@TrackChangesController = SandboxedModule.require modulePath, requires:
"request" : @request = {}
"request" : @request = sinon.stub()
"settings-sharelatex": @settings = {}
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
"../Authentication/AuthenticationController": @AuthenticationController = {}
describe "proxyToTrackChangesApi", ->
beforeEach ->
@req = { url: "/mock/url" }
@req = { url: "/mock/url", method: "POST" }
@res = "mock-res"
@next = sinon.stub()
@user_id = "user-id-123"
@settings.apis =
trackchanges:
url: "http://trackchanges.example.com"
@ -23,13 +25,24 @@ describe "TrackChangesController", ->
events: {}
pipe: sinon.stub()
on: (event, handler) -> @events[event] = handler
@request.get = sinon.stub().returns @proxy
@request.returns @proxy
@AuthenticationController.getLoggedInUserId = sinon.stub().callsArgWith(1, null, @user_id)
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
describe "successfully", ->
it "should get the user id", ->
@AuthenticationController.getLoggedInUserId
.calledWith(@req)
.should.equal true
it "should call the track changes api", ->
@request.get
.calledWith("#{@settings.apis.trackchanges.url}#{@req.url}")
@request
.calledWith({
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
method: @req.method
headers:
"X-User-Id": @user_id
})
.should.equal true
it "should pipe the response to the client", ->