mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Add in restore button to track changes
This commit is contained in:
parent
e364fd9c5f
commit
afb8bb6a42
12 changed files with 113 additions and 27 deletions
|
@ -6,7 +6,7 @@ module.exports = TrackChangesController =
|
||||||
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
|
||||||
url = settings.apis.trackchanges.url + req.url
|
url = settings.apis.trackchanges.url + req.url
|
||||||
logger.log url: url, "proxying to track-changes api"
|
logger.log url: url, "proxying to track-changes api"
|
||||||
getReq = request.get(url)
|
getReq = request(url: url, method: req.method)
|
||||||
getReq.pipe(res)
|
getReq.pipe(res)
|
||||||
getReq.on "error", (error) ->
|
getReq.on "error", (error) ->
|
||||||
logger.error err: error, "track-changes API error"
|
logger.error err: error, "track-changes API error"
|
||||||
|
|
|
@ -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/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
|
||||||
app.get "/project/:Project_id/doc/:doc_id/diff", 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.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
|
||||||
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
|
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
|
||||||
|
|
|
@ -140,8 +140,7 @@
|
||||||
button.btn.btn-primary ok
|
button.btn.btn-primary ok
|
||||||
|
|
||||||
script(type="text/template")#genericModalButtonTemplate
|
script(type="text/template")#genericModalButtonTemplate
|
||||||
a(href="#",class="btn {{ class }}") {{ text }}
|
button(class="btn {{ class }}") {{ text }}
|
||||||
|
|
||||||
|
|
||||||
script(type="text/template")#editorPanelTemplate
|
script(type="text/template")#editorPanelTemplate
|
||||||
#editorArea(style='display: none;')
|
#editorArea(style='display: none;')
|
||||||
|
@ -445,6 +444,9 @@
|
||||||
div.color-square(style="background-color: hsl({{hue}}, 100%, 70%);")
|
div.color-square(style="background-color: hsl({{hue}}, 100%, 70%);")
|
||||||
span {{name}}
|
span {{name}}
|
||||||
|
|
||||||
|
div(class='restore')
|
||||||
|
a(href="#") Restore to here
|
||||||
|
|
||||||
script(type='text/template')#changeListTemplate
|
script(type='text/template')#changeListTemplate
|
||||||
ul.change-list.nav.nav-pills.nav-stacked
|
ul.change-list.nav.nav-pills.nav-stacked
|
||||||
li.loading-changes Loading...
|
li.loading-changes Loading...
|
||||||
|
|
|
@ -145,6 +145,11 @@ define [
|
||||||
path = entity.get("name") + "/" + path
|
path = entity.get("name") + "/" + path
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
getNameOfEntityId: (entity_id) ->
|
||||||
|
entity = @getEntity(entity_id)
|
||||||
|
return if !entity?
|
||||||
|
return entity.get("name")
|
||||||
|
|
||||||
# RENAMING
|
# RENAMING
|
||||||
renameSelected: () ->
|
renameSelected: () ->
|
||||||
entity_id = @getSelectedEntityId()
|
entity_id = @getSelectedEntityId()
|
||||||
|
|
|
@ -49,4 +49,7 @@ define [
|
||||||
find: (id) ->
|
find: (id) ->
|
||||||
@loadedModel ||= {}
|
@loadedModel ||= {}
|
||||||
return @loadedModel[id]
|
return @loadedModel[id]
|
||||||
|
|
||||||
|
getAnonymousUser: () ->
|
||||||
|
return User.findOrBuild("anonymous", { first_name: "Anonymous", email: "anon@sharelatex.com" })
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,9 @@ define [
|
||||||
delete @hoverFromIndex
|
delete @hoverFromIndex
|
||||||
@resetHoverStates()
|
@resetHoverStates()
|
||||||
|
|
||||||
|
view.on "click:restore", (e) =>
|
||||||
|
@trigger "restore", view.model
|
||||||
|
|
||||||
view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
|
view.resetSelector(index, @selectedFromIndex, @selectedToIndex)
|
||||||
|
|
||||||
resetAllSelectors: () ->
|
resetAllSelectors: () ->
|
||||||
|
@ -77,11 +80,11 @@ define [
|
||||||
view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
|
view.resetSelector(i, @selectedFromIndex, @selectedToIndex)
|
||||||
|
|
||||||
resetHoverStates: () ->
|
resetHoverStates: () ->
|
||||||
if @hoverToIndex?
|
if @hoverToIndex? and @hoverToIndex != @selectedToIndex
|
||||||
@$("ul").addClass("hover-state")
|
@$("ul").addClass("hover-state")
|
||||||
for view, i in @itemViews
|
for view, i in @itemViews
|
||||||
view.resetHoverState(i, @selectedFromIndex, @hoverToIndex)
|
view.resetHoverState(i, @selectedFromIndex, @hoverToIndex)
|
||||||
else if @hoverFromIndex?
|
else if @hoverFromIndex? and @hoverFromIndex != @selectedFromIndex
|
||||||
@$("ul").addClass("hover-state")
|
@$("ul").addClass("hover-state")
|
||||||
for view, i in @itemViews
|
for view, i in @itemViews
|
||||||
view.resetHoverState(i, @hoverFromIndex, @selectedToIndex)
|
view.resetHoverState(i, @hoverFromIndex, @selectedToIndex)
|
||||||
|
@ -152,6 +155,7 @@ define [
|
||||||
@trigger "mouseenter:from", args...
|
@trigger "mouseenter:from", args...
|
||||||
"mouseleave .change-selector-from": (args...) ->
|
"mouseleave .change-selector-from": (args...) ->
|
||||||
@trigger "mouseleave:from", args...
|
@trigger "mouseleave:from", args...
|
||||||
|
"click .restore a": "onRestoreClick"
|
||||||
|
|
||||||
|
|
||||||
template : $("#changeListItemTemplate").html()
|
template : $("#changeListItemTemplate").html()
|
||||||
|
@ -183,6 +187,10 @@ define [
|
||||||
onFromSelectorClick: (e) ->
|
onFromSelectorClick: (e) ->
|
||||||
@trigger "selected:from", e, @
|
@trigger "selected:from", e, @
|
||||||
|
|
||||||
|
onRestoreClick: (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
@trigger "click:restore", e, @
|
||||||
|
|
||||||
isSelectedFrom: () ->
|
isSelectedFrom: () ->
|
||||||
@$(".change-selector-from").is(":checked")
|
@$(".change-selector-from").is(":checked")
|
||||||
|
|
||||||
|
|
|
@ -3,21 +3,23 @@ define [
|
||||||
"track-changes/models/Diff"
|
"track-changes/models/Diff"
|
||||||
"track-changes/ChangeListView"
|
"track-changes/ChangeListView"
|
||||||
"track-changes/DiffView"
|
"track-changes/DiffView"
|
||||||
], (ChangeList, Diff, ChangeListView, DiffView) ->
|
"utils/Modal"
|
||||||
|
"moment"
|
||||||
|
], (ChangeList, Diff, ChangeListView, DiffView, Modal, moment) ->
|
||||||
class TrackChangesManager
|
class TrackChangesManager
|
||||||
template: $("#trackChangesPanelTemplate").html()
|
template: $("#trackChangesPanelTemplate").html()
|
||||||
|
|
||||||
constructor: (@ide) ->
|
constructor: (@ide) ->
|
||||||
@$el = $(@template)
|
@$el = $(@template)
|
||||||
$("#editorWrapper").append(@$el)
|
$("#editorWrapper").append(@$el)
|
||||||
@hideEl()
|
@hide()
|
||||||
|
|
||||||
@ide.editor.on "change:doc", () =>
|
@ide.editor.on "change:doc", () =>
|
||||||
@hideEl()
|
@hide()
|
||||||
|
|
||||||
@$el.find(".track-changes-close").on "click", (e) =>
|
@$el.find(".track-changes-close").on "click", (e) =>
|
||||||
e.preventDefault
|
e.preventDefault
|
||||||
@hideEl()
|
@hide()
|
||||||
|
|
||||||
show: () ->
|
show: () ->
|
||||||
@project_id = window.userSettings.project_id
|
@project_id = window.userSettings.project_id
|
||||||
|
@ -44,14 +46,50 @@ define [
|
||||||
)
|
)
|
||||||
@diff.fetch()
|
@diff.fetch()
|
||||||
|
|
||||||
|
@changeListView.on "restore", (change) =>
|
||||||
|
@restore(change)
|
||||||
|
|
||||||
@showEl()
|
@showEl()
|
||||||
|
|
||||||
showEl: ->
|
showEl: ->
|
||||||
@ide.editor.hide()
|
@ide.editor.hide()
|
||||||
@$el.show()
|
@$el.show()
|
||||||
|
|
||||||
hideEl: () ->
|
hide: () ->
|
||||||
@ide.editor.show()
|
@ide.editor.show()
|
||||||
@$el.hide()
|
@$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
|
return TrackChangesManager
|
||||||
|
|
|
@ -4,9 +4,13 @@ define [
|
||||||
], (User)->
|
], (User)->
|
||||||
Change = Backbone.Model.extend
|
Change = Backbone.Model.extend
|
||||||
parse: (change) ->
|
parse: (change) ->
|
||||||
return {
|
model = {
|
||||||
start_ts: change.meta.start_ts
|
start_ts: change.meta.start_ts
|
||||||
end_ts: change.meta.end_ts
|
end_ts: change.meta.end_ts
|
||||||
user: User.findOrBuild(change.meta.user.id, change.meta.user)
|
|
||||||
version: change.v
|
version: change.v
|
||||||
}
|
}
|
||||||
|
if change.meta.user?
|
||||||
|
model.user = User.findOrBuild(change.meta.user.id, change.meta.user)
|
||||||
|
else
|
||||||
|
model.user = User.getAnonymousUser()
|
||||||
|
return model
|
|
@ -8,6 +8,9 @@ define [
|
||||||
|
|
||||||
parse: (diff) ->
|
parse: (diff) ->
|
||||||
for entry in diff.diff
|
for entry in diff.diff
|
||||||
if entry.meta? and entry.meta.user?
|
if entry.meta?
|
||||||
entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
|
if entry.meta.user?
|
||||||
|
entry.meta.user = User.findOrBuild(entry.meta.user.id, entry.meta.user)
|
||||||
|
else
|
||||||
|
entry.meta.user = User.getAnonymousUser()
|
||||||
return diff
|
return diff
|
||||||
|
|
|
@ -19,8 +19,9 @@ define [
|
||||||
button.on "click", (e) ->
|
button.on "click", (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if buttonOptions.callback?
|
if buttonOptions.callback?
|
||||||
buttonOptions.callback()
|
buttonOptions.callback(button)
|
||||||
self.remove()
|
if !buttonOptions.close? or buttonOptions.close
|
||||||
|
self.remove()
|
||||||
|
|
||||||
@$el.modal
|
@$el.modal
|
||||||
# make sure we control when the modal is hidden
|
# make sure we control when the modal is hidden
|
||||||
|
@ -39,7 +40,6 @@ define [
|
||||||
|
|
||||||
@$el.find('input').focus()
|
@$el.find('input').focus()
|
||||||
|
|
||||||
|
|
||||||
remove: () ->
|
remove: () ->
|
||||||
@$el.modal("hide")
|
@$el.modal("hide")
|
||||||
Backbone.View.prototype.remove.call(this)
|
Backbone.View.prototype.remove.call(this)
|
||||||
|
|
|
@ -71,11 +71,9 @@
|
||||||
|
|
||||||
ul.change-list {
|
ul.change-list {
|
||||||
li {
|
li {
|
||||||
padding: 6px 4px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 38px;
|
|
||||||
.change-selectors {
|
.change-selectors {
|
||||||
.change-selector-from {
|
.change-selector-from {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -98,7 +96,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.change-description {
|
.change-description {
|
||||||
padding-left: 26px;
|
padding: 6px 4px 6px 30px;
|
||||||
|
min-height: 38px;
|
||||||
}
|
}
|
||||||
.change-name {
|
.change-name {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
@ -112,6 +111,15 @@
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
|
.restore {
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
padding: 4px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #eaeaea;
|
background-color: #eaeaea;
|
||||||
}
|
}
|
||||||
|
@ -137,12 +145,16 @@
|
||||||
li.selected-from {
|
li.selected-from {
|
||||||
.change-selectors {
|
.change-selectors {
|
||||||
.range {
|
.range {
|
||||||
bottom: 10px;
|
bottom: 37px;
|
||||||
}
|
}
|
||||||
.change-selector-from {
|
.change-selector-from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
bottom: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.restore {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul.change-list.hover-state {
|
ul.change-list.hover-state {
|
||||||
|
@ -182,5 +194,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
li.selected-from.hover-selected-from {
|
||||||
|
.change-selectors {
|
||||||
|
.range {
|
||||||
|
bottom: 37px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,13 +7,13 @@ SandboxedModule = require('sandboxed-module')
|
||||||
describe "TrackChangesController", ->
|
describe "TrackChangesController", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@TrackChangesController = SandboxedModule.require modulePath, requires:
|
@TrackChangesController = SandboxedModule.require modulePath, requires:
|
||||||
"request" : @request = {}
|
"request" : @request = sinon.stub()
|
||||||
"settings-sharelatex": @settings = {}
|
"settings-sharelatex": @settings = {}
|
||||||
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
|
"logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()}
|
||||||
|
|
||||||
describe "proxyToTrackChangesApi", ->
|
describe "proxyToTrackChangesApi", ->
|
||||||
beforeEach ->
|
beforeEach ->
|
||||||
@req = { url: "/mock/url" }
|
@req = { url: "/mock/url", method: "POST" }
|
||||||
@res = "mock-res"
|
@res = "mock-res"
|
||||||
@next = sinon.stub()
|
@next = sinon.stub()
|
||||||
@settings.apis =
|
@settings.apis =
|
||||||
|
@ -23,13 +23,16 @@ describe "TrackChangesController", ->
|
||||||
events: {}
|
events: {}
|
||||||
pipe: sinon.stub()
|
pipe: sinon.stub()
|
||||||
on: (event, handler) -> @events[event] = handler
|
on: (event, handler) -> @events[event] = handler
|
||||||
@request.get = sinon.stub().returns @proxy
|
@request.returns @proxy
|
||||||
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
|
@TrackChangesController.proxyToTrackChangesApi @req, @res, @next
|
||||||
|
|
||||||
describe "successfully", ->
|
describe "successfully", ->
|
||||||
it "should call the track changes api", ->
|
it "should call the track changes api", ->
|
||||||
@request.get
|
@request
|
||||||
.calledWith("#{@settings.apis.trackchanges.url}#{@req.url}")
|
.calledWith({
|
||||||
|
url: "#{@settings.apis.trackchanges.url}#{@req.url}"
|
||||||
|
method: @req.method
|
||||||
|
})
|
||||||
.should.equal true
|
.should.equal true
|
||||||
|
|
||||||
it "should pipe the response to the client", ->
|
it "should pipe the response to the client", ->
|
||||||
|
|
Loading…
Reference in a new issue