|
|
@@ -0,0 +1,280 @@
|
|
|
+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")
|
|
|
+ @$scope.history.selection.pathname = _ide.fileTreeManager.getEntityPath(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 = {
|
|
|
+ isV2: true
|
|
|
+ updates: []
|
|
|
+ nextBeforeTimestamp: null
|
|
|
+ atEnd: false
|
|
|
+ selection: {
|
|
|
+ updates: []
|
|
|
+ pathname: null
|
|
|
+ range: {
|
|
|
+ fromV: null
|
|
|
+ toV: null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ diff: null
|
|
|
+ }
|
|
|
+
|
|
|
+ MAX_RECENT_UPDATES_TO_SELECT: 2
|
|
|
+ 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
|
|
|
+
|
|
|
+ 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
|
|
|
+ @_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()
|
|
|
+
|
|
|
+ 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} = @_parseDiff(data)
|
|
|
+ diff.text = text
|
|
|
+ diff.highlights = highlights
|
|
|
+ .catch () ->
|
|
|
+ diff.loading = false
|
|
|
+ diff.error = true
|
|
|
+
|
|
|
+ _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
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+ 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
|