mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
534 lines
16 KiB
CoffeeScript
534 lines
16 KiB
CoffeeScript
define [
|
|
"moment"
|
|
"ide/colors/ColorManager"
|
|
"ide/history/util/displayNameForUser"
|
|
"ide/history/util/HistoryViewModes"
|
|
"ide/history/controllers/HistoryV2ListController"
|
|
"ide/history/controllers/HistoryV2DiffController"
|
|
"ide/history/controllers/HistoryV2FileTreeController"
|
|
"ide/history/controllers/HistoryV2ToolbarController"
|
|
"ide/history/controllers/HistoryV2AddLabelModalController"
|
|
"ide/history/controllers/HistoryV2DeleteLabelModalController"
|
|
"ide/history/directives/infiniteScroll"
|
|
"ide/history/components/historyEntriesList"
|
|
"ide/history/components/historyEntry"
|
|
"ide/history/components/historyLabelsList"
|
|
"ide/history/components/historyLabel"
|
|
"ide/history/components/historyFileTree"
|
|
"ide/history/components/historyFileEntity"
|
|
], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
|
|
class HistoryManager
|
|
constructor: (@ide, @$scope) ->
|
|
@reset()
|
|
@$scope.HistoryViewModes = HistoryViewModes
|
|
|
|
@$scope.toggleHistory = () =>
|
|
if @$scope.ui.view == "history"
|
|
@hide()
|
|
else
|
|
@show()
|
|
@ide.$timeout () =>
|
|
@$scope.$broadcast "history:toggle"
|
|
, 0
|
|
|
|
@$scope.toggleHistoryViewMode = () =>
|
|
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
|
@reset()
|
|
@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
|
|
else
|
|
@reset()
|
|
@$scope.history.viewMode = HistoryViewModes.COMPARE
|
|
@ide.$timeout () =>
|
|
@$scope.$broadcast "history:toggle"
|
|
, 0
|
|
|
|
@$scope.$watch "history.selection.updates", (updates) =>
|
|
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
|
if updates? and updates.length > 0
|
|
@_selectDocFromUpdates()
|
|
@reloadDiff()
|
|
|
|
@$scope.$watch "history.selection.pathname", (pathname) =>
|
|
if @$scope.history.viewMode == HistoryViewModes.POINT_IN_TIME
|
|
if pathname?
|
|
@loadFileAtPointInTime()
|
|
else
|
|
@reloadDiff()
|
|
|
|
@$scope.$watch "history.showOnlyLabels", (showOnlyLabels, prevVal) =>
|
|
if showOnlyLabels? and showOnlyLabels != prevVal
|
|
if showOnlyLabels
|
|
@selectedLabelFromUpdatesSelection()
|
|
else
|
|
@$scope.history.selection.label = null
|
|
if @$scope.history.selection.updates.length == 0
|
|
@autoSelectLastUpdate()
|
|
|
|
@$scope.$watch "history.updates.length", () =>
|
|
@recalculateSelectedUpdates()
|
|
|
|
show: () ->
|
|
@$scope.ui.view = "history"
|
|
@reset()
|
|
@$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
|
|
|
|
hide: () ->
|
|
@$scope.ui.view = "editor"
|
|
|
|
reset: () ->
|
|
@$scope.history = {
|
|
isV2: true
|
|
updates: []
|
|
viewMode: null
|
|
nextBeforeTimestamp: null
|
|
atEnd: false
|
|
userHasFullFeature: @$scope.project?.features?.versioning or false
|
|
freeHistoryLimitHit: false
|
|
selection: {
|
|
label: null
|
|
updates: []
|
|
docs: {}
|
|
pathname: null
|
|
range: {
|
|
fromV: null
|
|
toV: null
|
|
}
|
|
}
|
|
error: null
|
|
showOnlyLabels: false
|
|
labels: null
|
|
files: []
|
|
diff: null # When history.viewMode == HistoryViewModes.COMPARE
|
|
selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
|
|
}
|
|
|
|
restoreFile: (version, pathname) ->
|
|
url = "/project/#{@$scope.project_id}/restore_file"
|
|
|
|
@ide.$http.post(url, {
|
|
version, pathname,
|
|
_csrf: window.csrfToken
|
|
})
|
|
|
|
loadFileTreeForVersion: (version) ->
|
|
url = "/project/#{@$scope.project_id}/filetree/diff"
|
|
query = [ "from=#{version}", "to=#{version}" ]
|
|
url += "?" + query.join("&")
|
|
@$scope.history.loadingFileTree = true
|
|
@$scope.history.selectedFile = null
|
|
@$scope.history.selection.pathname = null
|
|
@ide.$http
|
|
.get(url)
|
|
.then (response) =>
|
|
@$scope.history.files = response.data.diff
|
|
@$scope.history.loadingFileTree = false
|
|
|
|
MAX_RECENT_UPDATES_TO_SELECT: 5
|
|
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) or i > @MAX_RECENT_UPDATES_TO_SELECT
|
|
break
|
|
indexOfLastUpdateNotByMe = i
|
|
|
|
@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
|
|
|
|
autoSelectLastUpdate: () ->
|
|
return if @$scope.history.updates.length == 0
|
|
@selectUpdate @$scope.history.updates[0]
|
|
|
|
autoSelectLastLabel: () ->
|
|
return if @$scope.history.labels.length == 0
|
|
@selectLabel @$scope.history.labels[0]
|
|
|
|
selectUpdate: (update) ->
|
|
selectedUpdateIndex = @$scope.history.updates.indexOf update
|
|
if selectedUpdateIndex == -1
|
|
selectedUpdateIndex = 0
|
|
for update in @$scope.history.updates
|
|
update.selectedTo = false
|
|
update.selectedFrom = false
|
|
@$scope.history.updates[selectedUpdateIndex].selectedTo = true
|
|
@$scope.history.updates[selectedUpdateIndex].selectedFrom = true
|
|
@recalculateSelectedUpdates()
|
|
@loadFileTreeForVersion @$scope.history.updates[selectedUpdateIndex].toV
|
|
|
|
selectedLabelFromUpdatesSelection: () ->
|
|
# Get the number of labels associated with the currently selected update
|
|
nSelectedLabels = @$scope.history.selection.updates?[0]?.labels?.length
|
|
# If the currently selected update has no labels, select the last one (version-wise)
|
|
if nSelectedLabels == 0
|
|
@autoSelectLastLabel()
|
|
# If the update has one label, select it
|
|
else if nSelectedLabels == 1
|
|
@selectLabel @$scope.history.selection.updates[0].labels[0]
|
|
# If there are multiple labels for the update, select the latest
|
|
else if nSelectedLabels > 1
|
|
sortedLabels = @ide.$filter("orderBy")(@$scope.history.selection.updates[0].labels, '-created_at')
|
|
lastLabelFromUpdate = sortedLabels[0]
|
|
@selectLabel lastLabelFromUpdate
|
|
|
|
selectLabel: (labelToSelect) ->
|
|
updateToSelect = null
|
|
|
|
if @_isLabelSelected labelToSelect
|
|
# Label already selected
|
|
return
|
|
|
|
for update in @$scope.history.updates
|
|
if update.toV == labelToSelect.version
|
|
updateToSelect = update
|
|
break
|
|
|
|
@$scope.history.selection.label = labelToSelect
|
|
if updateToSelect?
|
|
@selectUpdate updateToSelect
|
|
else
|
|
@$scope.history.selection.updates = []
|
|
@loadFileTreeForVersion labelToSelect.version
|
|
|
|
recalculateSelectedUpdates: () ->
|
|
beforeSelection = true
|
|
afterSelection = false
|
|
@$scope.history.selection.updates = []
|
|
for update in @$scope.history.updates
|
|
if update.selectedTo
|
|
inSelection = true
|
|
beforeSelection = false
|
|
|
|
update.beforeSelection = beforeSelection
|
|
update.inSelection = inSelection
|
|
update.afterSelection = afterSelection
|
|
|
|
if inSelection
|
|
@$scope.history.selection.updates.push update
|
|
|
|
if update.selectedFrom
|
|
inSelection = false
|
|
afterSelection = true
|
|
|
|
BATCH_SIZE: 10
|
|
fetchNextBatchOfUpdates: () ->
|
|
updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
|
|
if @$scope.history.nextBeforeTimestamp?
|
|
updatesURL += "&before=#{@$scope.history.nextBeforeTimestamp}"
|
|
labelsURL = "/project/#{@ide.project_id}/labels"
|
|
|
|
@$scope.history.loading = true
|
|
@$scope.history.loadingFileTree = true
|
|
|
|
requests =
|
|
updates: @ide.$http.get updatesURL
|
|
|
|
if !@$scope.history.labels?
|
|
requests.labels = @ide.$http.get labelsURL
|
|
|
|
@ide.$q.all requests
|
|
.then (response) =>
|
|
updatesData = response.updates.data
|
|
if response.labels?
|
|
@$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data
|
|
@_loadUpdates(updatesData.updates)
|
|
@$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp
|
|
if !updatesData.nextBeforeTimestamp? or @$scope.history.freeHistoryLimitHit
|
|
@$scope.history.atEnd = true
|
|
@$scope.history.loading = false
|
|
if @$scope.history.updates.length == 0
|
|
@$scope.history.loadingFileTree = false
|
|
.catch (error) =>
|
|
{ status, statusText } = error
|
|
@$scope.history.error = { status, statusText }
|
|
@$scope.history.loading = false
|
|
@$scope.history.loadingFileTree = false
|
|
|
|
_sortLabelsByVersionAndDate: (labels) ->
|
|
@ide.$filter("orderBy")(labels, [ '-version', '-created_at' ])
|
|
|
|
loadFileAtPointInTime: () ->
|
|
pathname = @$scope.history.selection.pathname
|
|
if @$scope.history.selection.updates?[0]?
|
|
toV = @$scope.history.selection.updates[0].toV
|
|
else if @$scope.history.selection.label?
|
|
toV = @$scope.history.selection.label.version
|
|
|
|
if !toV?
|
|
return
|
|
url = "/project/#{@$scope.project_id}/diff"
|
|
query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"]
|
|
url += "?" + query.join("&")
|
|
@$scope.history.selectedFile =
|
|
loading: true
|
|
@ide.$http
|
|
.get(url)
|
|
.then (response) =>
|
|
{text, binary} = @_parseDiff(response.data.diff)
|
|
@$scope.history.selectedFile.binary = binary
|
|
@$scope.history.selectedFile.text = text
|
|
@$scope.history.selectedFile.loading = false
|
|
.catch () ->
|
|
|
|
reloadDiff: () ->
|
|
diff = @$scope.history.diff
|
|
{updates} = @$scope.history.selection
|
|
{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
|
|
|
|
if !pathname?
|
|
@$scope.history.diff = null
|
|
return
|
|
|
|
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
|
|
}
|
|
|
|
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, binary} = @_parseDiff(data.diff)
|
|
diff.binary = binary
|
|
diff.text = text
|
|
diff.highlights = highlights
|
|
.catch () ->
|
|
diff.loading = false
|
|
diff.error = true
|
|
|
|
labelCurrentVersion: (labelComment) =>
|
|
@_labelVersion labelComment, @$scope.history.selection.updates[0].toV
|
|
|
|
deleteLabel: (label) =>
|
|
url = "/project/#{@$scope.project_id}/labels/#{label.id}"
|
|
|
|
@ide.$http({
|
|
url,
|
|
method: "DELETE"
|
|
headers:
|
|
"X-CSRF-Token": window.csrfToken
|
|
}).then (response) =>
|
|
@_deleteLabelLocally label
|
|
|
|
_isLabelSelected: (label) ->
|
|
label.id == @$scope.history.selection.label?.id
|
|
|
|
_deleteLabelLocally: (labelToDelete) ->
|
|
for update, i in @$scope.history.updates
|
|
if update.toV == labelToDelete.version
|
|
update.labels = _.filter update.labels, (label) ->
|
|
label.id != labelToDelete.id
|
|
break
|
|
@$scope.history.labels = _.filter @$scope.history.labels, (label) ->
|
|
label.id != labelToDelete.id
|
|
|
|
_parseDiff: (diff) ->
|
|
if diff.binary
|
|
return { binary: true }
|
|
row = 0
|
|
column = 0
|
|
highlights = []
|
|
text = ""
|
|
for entry, i in 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?
|
|
user = entry.meta.users?[0]
|
|
name = displayNameForUser(user)
|
|
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(user?.id)
|
|
}
|
|
else if entry.d?
|
|
highlights.push {
|
|
label: "Deleted by #{name} on #{date}"
|
|
strikeThrough: range
|
|
hue: ColorManager.getHueForUserId(user?.id)
|
|
}
|
|
|
|
return {text, highlights}
|
|
|
|
_loadUpdates: (updates = []) ->
|
|
previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
|
|
dateTimeNow = new Date()
|
|
timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
|
|
cutOffIndex = null
|
|
|
|
for update, i 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
|
|
|
|
if !@$scope.history.userHasFullFeature and update.meta.end_ts < timestamp24hoursAgo
|
|
cutOffIndex = i
|
|
@$scope.history.freeHistoryLimitHit = true
|
|
break
|
|
|
|
firstLoad = @$scope.history.updates.length == 0
|
|
|
|
if !@$scope.history.userHasFullFeature and cutOffIndex?
|
|
updates = updates.slice 0, cutOffIndex
|
|
|
|
@$scope.history.updates =
|
|
@$scope.history.updates.concat(updates)
|
|
|
|
if firstLoad
|
|
if @$scope.history.viewMode == HistoryViewModes.COMPARE
|
|
@autoSelectRecentUpdates()
|
|
else
|
|
if @$scope.history.showOnlyLabels
|
|
@autoSelectLastLabel()
|
|
else
|
|
@autoSelectLastUpdate()
|
|
|
|
_labelVersion: (comment, version) ->
|
|
url = "/project/#{@$scope.project_id}/labels"
|
|
@ide.$http
|
|
.post url, {
|
|
comment,
|
|
version,
|
|
_csrf: window.csrfToken
|
|
}
|
|
.then (response) =>
|
|
@_addLabelToLocalUpdate response.data
|
|
|
|
_addLabelToLocalUpdate: (label) =>
|
|
localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version
|
|
if localUpdate?
|
|
localUpdate.labels = @_sortLabelsByVersionAndDate localUpdate.labels.concat label
|
|
@$scope.history.labels = @_sortLabelsByVersionAndDate @$scope.history.labels.concat label
|
|
|
|
_perDocSummaryOfUpdates: (updates) ->
|
|
# Track current_pathname -> original_pathname
|
|
# create bare object for use as Map
|
|
# http://ryanmorr.com/true-hash-maps-in-javascript/
|
|
original_pathnames = Object.create(null)
|
|
|
|
# Map of original pathname -> doc summary
|
|
docs_summary = Object.create(null)
|
|
|
|
updatePathnameWithUpdateVersions = (pathname, update, deletedAtV) ->
|
|
# 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
|
|
)
|
|
if deletedAtV?
|
|
doc_summary.deletedAtV = deletedAtV
|
|
|
|
# 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)
|
|
if project_op.remove?
|
|
remove = project_op.remove
|
|
updatePathnameWithUpdateVersions(remove.pathname, update, project_op.atV)
|
|
|
|
return docs_summary
|
|
|
|
_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
|
|
return {fromV, toV, pathname}
|
|
|
|
return {}
|
|
|
|
# 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)
|
|
@$scope.history.selection.docs = affected_docs
|
|
|
|
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
|
|
|
|
_updateContainsUserId: (update, user_id) ->
|
|
for user in update.meta.users
|
|
return true if user?.id == user_id
|
|
return false
|