Add HistoryV2Manager alongside existing HistoryManager

This commit is contained in:
James Allen 2017-12-13 16:11:12 +00:00
parent 8ea779af58
commit 50b12e88a2
7 changed files with 415 additions and 86 deletions

View file

@ -216,7 +216,7 @@ module.exports = ProjectController =
project: (cb)->
ProjectGetter.getProject(
project_id,
{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1 },
{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, 'overleaf.history.display': 1 },
cb
)
user: (cb)->
@ -351,6 +351,7 @@ module.exports = ProjectController =
themes: THEME_LIST
maxDocLength: Settings.max_doc_length
showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding
useV2History: !!project.overleaf?.history?.display
timer.done()
_buildProjectList: (allProjects, v1Projects = [])->

View file

@ -105,7 +105,7 @@ block requirejs
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
//- and doesn't prematurely end the script tag.
script#data(type="application/json").
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')}
!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History}).replace(/\//g, '\\/')}
script(type="text/javascript").
window.data = JSON.parse($("#data").text());

View file

@ -134,11 +134,13 @@ div#history(ng-show="ui.view == 'history'")
div.description(ng-click="select()")
div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
div.docs(ng-repeat="pathname in update.docs", ng-if="update.docs")
div.docs(ng-repeat="pathname in update.pathnames")
span.doc {{ pathname }}
div.docs(ng-repeat="project_op in update.project_ops", ng-if="update.project_ops")
div.docs(ng-repeat="project_op in update.project_ops")
span(ng-if="project_op.rename")
| Renamed {{ project_op.rename.pathname }} to {{ project_op.rename.newPathname }}
span(ng-if="project_op.add")
| Added {{ project_op.add.pathname }}
div.users
div.user(ng-repeat="update_user in update.meta.users")
.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")

View file

@ -5,6 +5,7 @@ define [
"ide/editor/EditorManager"
"ide/online-users/OnlineUsersManager"
"ide/history/HistoryManager"
"ide/history/HistoryV2Manager"
"ide/permissions/PermissionsManager"
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
@ -44,6 +45,7 @@ define [
EditorManager
OnlineUsersManager
HistoryManager
HistoryV2Manager
PermissionsManager
PdfManager
BinaryFilesManager
@ -137,7 +139,10 @@ define [
ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(ide, $scope)
ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
ide.historyManager = new HistoryManager(ide, $scope)
if window.data.useV2History
ide.historyManager = new HistoryV2Manager(ide, $scope)
else
ide.historyManager = new HistoryManager(ide, $scope)
ide.pdfManager = new PdfManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)

View file

@ -22,8 +22,7 @@ define [
@$scope.$on "entity:selected", (event, entity) =>
if (@$scope.ui.view == "history") and (entity.type == "doc")
# TODO: Set selection.pathname to entity path name
# @$scope.history.selection.doc = entity
@$scope.history.selection.doc = entity
@reloadDiff()
show: () ->
@ -46,6 +45,8 @@ define [
range: {
fromV: null
toV: null
start_ts: null
end_ts: null
}
}
diff: null
@ -74,7 +75,6 @@ define [
.get(url)
.then (response) =>
{ data } = response
console.log "fetchNextBatchOfUpdates", data.updates
@_loadUpdates(data.updates)
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
if !data.nextBeforeTimestamp?
@ -83,34 +83,30 @@ define [
reloadDiff: () ->
diff = @$scope.history.diff
{updates} = @$scope.history.selection
{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
console.log "[reloadDiff] current diff", diff
console.log "[reloadDiff] new diff data", {fromV, toV, pathname}
{updates, doc} = @$scope.history.selection
{fromV, toV, start_ts, end_ts} = @_calculateRangeFromSelection()
return if !pathname?
return if !doc?
return if diff? and
diff.pathname == pathname and
diff.fromV == fromV and
diff.toV == toV
diff.doc == doc and
diff.fromV == fromV and
diff.toV == toV
@$scope.history.diff = diff = {
fromV: fromV
toV: toV
pathname: pathname
start_ts: start_ts
end_ts: end_ts
doc: doc
error: false
}
# TODO: How do we track deleted files now? We can probably show the diffs easily
# with the new system!
if true # !doc.deleted
if !doc.deleted
diff.loading = true
url = "/project/#{@$scope.project_id}/diff"
query = ["pathname=#{encodeURIComponent(pathname)}"]
url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/diff"
if diff.fromV? and diff.toV?
query.push "from=#{diff.fromV}", "to=#{diff.toV}"
url += "?" + query.join("&")
url += "?from=#{diff.fromV}&to=#{diff.toV}"
@ide.$http
.get(url)
@ -193,7 +189,12 @@ define [
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates or []
for update in updates
update.pathnames = [] # Used for display
for doc_id, doc of update.docs or {}
doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
update.pathnames.push doc.entity.name
for user in update.meta.users or []
if user?
user.hue = ColorManager.getHueForUserId(user.id)
@ -214,69 +215,47 @@ define [
@autoSelectRecentUpdates() if firstLoad
_perDocSummaryOfUpdates: (updates) ->
# Track current_pathname -> original_pathname
original_pathnames = {}
docs_summary = {}
_calculateRangeFromSelection: () ->
fromV = toV = start_ts = end_ts = null
# Put updates in ascending chronological order
updates = updates.slice().reverse()
for update in updates
for pathname in update.docs or []
if !original_pathnames[pathname]?
original_pathnames[pathname] = pathname
original_pathname = original_pathnames[pathname]
if !docs_summary[original_pathname]?
docs_summary[original_pathname] = {
fromV: update.fromV, toV: update.toV,
}
else
docs_summary[original_pathname] = {
fromV: Math.min(docs_summary[original_pathname].fromV, update.fromV),
toV: Math.max(docs_summary[original_pathname].toV, update.toV),
}
for project_op in update.project_ops or []
if project_op.rename?
rename = project_op.rename
if !original_pathnames[rename.pathname]?
original_pathnames[rename.pathname] = rename.pathname
original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
delete original_pathnames[rename.pathname]
selected_doc_id = @$scope.history.selection.doc?.id
return docs_summary
for update in @$scope.history.selection.updates or []
for doc_id, doc of update.docs
if doc_id == selected_doc_id
if fromV? and toV?
fromV = Math.min(fromV, doc.fromV)
toV = Math.max(toV, doc.toV)
start_ts = Math.min(start_ts, update.meta.start_ts)
end_ts = Math.max(end_ts, update.meta.end_ts)
else
fromV = doc.fromV
toV = doc.toV
start_ts = update.meta.start_ts
end_ts = update.meta.end_ts
break
_calculateDiffDataFromSelection: () ->
fromV = toV = pathname = null
selected_pathname = @$scope.history.selection.pathname
for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
if pathname == selected_pathname
{fromV, toV} = doc
break
return {fromV, toV, pathname}
return {fromV, toV, start_ts, end_ts}
# Set the track changes selected doc to one of the docs in the range
# of currently selected updates. If we already have a selected doc
# then prefer this one if present.
_selectDocFromUpdates: () ->
affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
affected_docs = {}
for update in @$scope.history.selection.updates
for doc_id, doc of update.docs
affected_docs[doc_id] = doc.entity
selected_pathname = @$scope.history.selection.pathname
if selected_pathname? and affected_docs[selected_pathname]
selected_doc = @$scope.history.selection.doc
if selected_doc? and affected_docs[selected_doc.id]?
# Selected doc is already open
else
# Set to first possible candidate
for pathname, doc of affected_docs
selected_pathname = pathname
for doc_id, doc of affected_docs
selected_doc = doc
break
@$scope.history.selection.pathname = selected_pathname
if selected_pathname?
entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
if entity?
@ide.fileTreeManager.selectEntity(entity)
@$scope.history.selection.doc = selected_doc
@ide.fileTreeManager.selectEntity(selected_doc)
_updateContainsUserId: (update, user_id) ->
for user in update.meta.users

View file

@ -0,0 +1,298 @@
define [
"moment"
"ide/colors/ColorManager"
"ide/history/controllers/HistoryListController"
"ide/history/controllers/HistoryDiffController"
"ide/history/directives/infiniteScroll"
], (moment, ColorManager) ->
class HistoryManager
constructor: (@ide, @$scope) ->
@reset()
@$scope.toggleHistory = () =>
if @$scope.ui.view == "history"
@hide()
else
@show()
@$scope.$watch "history.selection.updates", (updates) =>
if updates? and updates.length > 0
@_selectDocFromUpdates()
@reloadDiff()
@$scope.$on "entity:selected", (event, entity) =>
if (@$scope.ui.view == "history") and (entity.type == "doc")
# TODO: Set selection.pathname to entity path name
# @$scope.history.selection.doc = entity
@reloadDiff()
show: () ->
@$scope.ui.view = "history"
@reset()
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.history = {
updates: []
nextBeforeTimestamp: null
atEnd: false
selection: {
updates: []
doc: null
range: {
fromV: null
toV: null
}
}
diff: null
}
autoSelectRecentUpdates: () ->
return if @$scope.history.updates.length == 0
@$scope.history.updates[0].selectedTo = true
indexOfLastUpdateNotByMe = 0
for update, i in @$scope.history.updates
if @_updateContainsUserId(update, @$scope.user.id)
break
indexOfLastUpdateNotByMe = i
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
BATCH_SIZE: 10
fetchNextBatchOfUpdates: () ->
url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
if @$scope.history.nextBeforeTimestamp?
url += "&before=#{@$scope.history.nextBeforeTimestamp}"
@$scope.history.loading = true
@ide.$http
.get(url)
.then (response) =>
{ data } = response
console.log "fetchNextBatchOfUpdates", data.updates
@_loadUpdates(data.updates)
@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
if !data.nextBeforeTimestamp?
@$scope.history.atEnd = true
@$scope.history.loading = false
reloadDiff: () ->
diff = @$scope.history.diff
{updates} = @$scope.history.selection
{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
console.log "[reloadDiff] current diff", diff
console.log "[reloadDiff] new diff data", {fromV, toV, pathname}
return if !pathname?
return if diff? and
diff.pathname == pathname and
diff.fromV == fromV and
diff.toV == toV
@$scope.history.diff = diff = {
fromV: fromV
toV: toV
pathname: pathname
error: false
}
# TODO: How do we track deleted files now? We can probably show the diffs easily
# with the new system!
if true # !doc.deleted
diff.loading = true
url = "/project/#{@$scope.project_id}/diff"
query = ["pathname=#{encodeURIComponent(pathname)}"]
if diff.fromV? and diff.toV?
query.push "from=#{diff.fromV}", "to=#{diff.toV}"
url += "?" + query.join("&")
@ide.$http
.get(url)
.then (response) =>
{ data } = response
diff.loading = false
{text, highlights} = @_parseDiff(data)
diff.text = text
diff.highlights = highlights
.catch () ->
diff.loading = false
diff.error = true
else
diff.deleted = true
diff.restoreInProgress = false
diff.restoreDeletedSuccess = false
diff.restoredDocNewId = null
restoreDeletedDoc: (doc) ->
url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
@ide.$http.post(url, name: doc.name, _csrf: window.csrfToken)
restoreDiff: (diff) ->
url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/version/#{diff.fromV}/restore"
@ide.$http.post(url, _csrf: window.csrfToken)
_parseDiff: (diff) ->
row = 0
column = 0
highlights = []
text = ""
for entry, i in diff.diff or []
content = entry.u or entry.i or entry.d
content ||= ""
text += content
lines = content.split("\n")
startRow = row
startColumn = column
if lines.length > 1
endRow = startRow + lines.length - 1
endColumn = lines[lines.length - 1].length
else
endRow = startRow
endColumn = startColumn + lines[0].length
row = endRow
column = endColumn
range = {
start:
row: startRow
column: startColumn
end:
row: endRow
column: endColumn
}
if entry.i? or entry.d?
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: ColorManager.getHueForUserId(entry.meta.user?.id)
}
else if entry.d?
highlights.push {
label: "Deleted by #{name} on #{date}"
strikeThrough: range
hue: ColorManager.getHueForUserId(entry.meta.user?.id)
}
return {text, highlights}
_loadUpdates: (updates = []) ->
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
for update in updates or []
for user in update.meta.users or []
if user?
user.hue = ColorManager.getHueForUserId(user.id)
if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
update.meta.first_in_day = true
update.selectedFrom = false
update.selectedTo = false
update.inSelection = false
previousUpdate = update
firstLoad = @$scope.history.updates.length == 0
@$scope.history.updates =
@$scope.history.updates.concat(updates)
@autoSelectRecentUpdates() if firstLoad
_perDocSummaryOfUpdates: (updates) ->
# Track current_pathname -> original_pathname
original_pathnames = {}
# Map of original pathname -> doc summary
docs_summary = {}
updatePathnameWithUpdateVersions = (pathname, update) ->
# docs_summary is indexed by the original pathname the doc
# had at the start, so we have to look this up from the current
# pathname via original_pathname first
if !original_pathnames[pathname]?
original_pathnames[pathname] = pathname
original_pathname = original_pathnames[pathname]
doc_summary = docs_summary[original_pathname] ?= {
fromV: update.fromV, toV: update.toV,
}
doc_summary.fromV = Math.min(
doc_summary.fromV,
update.fromV
)
doc_summary.toV = Math.max(
doc_summary.toV,
update.toV
)
# Put updates in ascending chronological order
updates = updates.slice().reverse()
for update in updates
for pathname in update.pathnames or []
updatePathnameWithUpdateVersions(pathname, update)
for project_op in update.project_ops or []
if project_op.rename?
rename = project_op.rename
updatePathnameWithUpdateVersions(rename.pathname, update)
original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
delete original_pathnames[rename.pathname]
if project_op.add?
add = project_op.add
updatePathnameWithUpdateVersions(add.pathname, update)
return docs_summary
_calculateDiffDataFromSelection: () ->
fromV = toV = pathname = null
selected_pathname = @$scope.history.selection.pathname
console.log "[_calculateDiffDataFromSelection]", @$scope.history.selection.updates
for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
console.log "[_calculateDiffDataFromSelection] doc:", pathname, doc
if pathname == selected_pathname
{fromV, toV} = doc
break
return {fromV, toV, pathname}
# Set the track changes selected doc to one of the docs in the range
# of currently selected updates. If we already have a selected doc
# then prefer this one if present.
_selectDocFromUpdates: () ->
affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
selected_pathname = @$scope.history.selection.pathname
if selected_pathname? and affected_docs[selected_pathname]
# Selected doc is already open
else
# Set to first possible candidate
for pathname, doc of affected_docs
selected_pathname = pathname
break
@$scope.history.selection.pathname = selected_pathname
if selected_pathname?
entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
if entity?
@ide.fileTreeManager.selectEntity(entity)
_updateContainsUserId: (update, user_id) ->
for user in update.meta.users
return true if user?.id == user_id
return false

View file

@ -1,23 +1,23 @@
Path = require 'path'
SandboxedModule = require "sandboxed-module"
modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryManager'
modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryV2Manager'
sinon = require("sinon")
expect = require("chai").expect
describe "HistoryManager", ->
describe "HistoryV2Manager", ->
beforeEach ->
@moment = {}
@ColorManager = {}
SandboxedModule.require modulePath, globals:
"define": (dependencies, builder) =>
@HistoryManager = builder(@moment, @ColorManager)
@HistoryV2Manager = builder(@moment, @ColorManager)
@scope =
$watch: sinon.stub()
$on: sinon.stub()
@ide = {}
@historyManager = new @HistoryManager(@ide, @scope)
@historyManager = new @HistoryV2Manager(@ide, @scope)
it "should setup the history scope on intialization", ->
expect(@scope.history).to.deep.equal({
@ -38,16 +38,16 @@ describe "HistoryManager", ->
describe "_perDocSummaryOfUpdates", ->
it "should return the range of updates for the docs", ->
result = @historyManager._perDocSummaryOfUpdates([{
docs: ["main.tex"]
pathnames: ["main.tex"]
fromV: 7, toV: 9
},{
docs: ["main.tex", "foo.tex"]
pathnames: ["main.tex", "foo.tex"]
fromV: 4, toV: 6
},{
docs: ["main.tex"]
pathnames: ["main.tex"]
fromV: 3, toV: 3
},{
docs: ["foo.tex"]
pathnames: ["foo.tex"]
fromV: 0, toV: 2
}])
@ -58,7 +58,7 @@ describe "HistoryManager", ->
it "should track renames", ->
result = @historyManager._perDocSummaryOfUpdates([{
docs: ["main2.tex"]
pathnames: ["main2.tex"]
fromV: 5, toV: 9
},{
project_ops: [{
@ -69,7 +69,7 @@ describe "HistoryManager", ->
}],
fromV: 4, toV: 4
},{
docs: ["main1.tex"]
pathnames: ["main1.tex"]
fromV: 3, toV: 3
},{
project_ops: [{
@ -80,10 +80,54 @@ describe "HistoryManager", ->
}],
fromV: 2, toV: 2
},{
docs: ["main0.tex"]
pathnames: ["main0.tex"]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
"main0.tex": { fromV: 0, toV: 9 }
})
it "should track single renames", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
rename: {
pathname: "main1.tex",
newPathname: "main2.tex"
}
}],
fromV: 4, toV: 5
}])
expect(result).to.deep.equal({
"main1.tex": { fromV: 4, toV: 5 }
})
it "should track additions", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
add:
pathname: "main.tex"
}]
fromV: 0, toV: 1
}, {
pathnames: ["main.tex"]
fromV: 1, toV: 4
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 0, toV: 4 }
})
it "should track single additions", ->
result = @historyManager._perDocSummaryOfUpdates([{
project_ops: [{
add:
pathname: "main.tex"
}]
fromV: 0, toV: 1
}])
expect(result).to.deep.equal({
"main.tex": { fromV: 0, toV: 1 }
})