mirror of
https://github.com/overleaf/overleaf.git
synced 2024-10-31 21:21:03 -04:00
f0bd6dda23
[misc] upgrade eslint packages to the latest version everywhere GitOrigin-RevId: f1480d4a171acef82fb26c4aa54be3a6088b0ab3
1006 lines
30 KiB
JavaScript
1006 lines
30 KiB
JavaScript
import _ from 'lodash'
|
|
/* eslint-disable
|
|
camelcase,
|
|
max-len,
|
|
no-return-assign,
|
|
no-unused-vars,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS103: Rewrite code to no longer use __guard__
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
* DS206: Consider reworking classes to avoid initClass
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
import moment from 'moment'
|
|
import ColorManager from '../colors/ColorManager'
|
|
import displayNameForUser from './util/displayNameForUser'
|
|
import HistoryViewModes from './util/HistoryViewModes'
|
|
import './controllers/HistoryV2ListController'
|
|
import './controllers/HistoryV2FileTreeController'
|
|
import './controllers/HistoryV2ToolbarController'
|
|
import './controllers/HistoryV2AddLabelModalController'
|
|
import './controllers/HistoryV2DeleteLabelModalController'
|
|
import './directives/infiniteScroll'
|
|
import './directives/historyDraggableBoundary'
|
|
import './directives/historyDroppableArea'
|
|
import './components/historyEntriesList'
|
|
import './components/historyEntry'
|
|
import './components/historyLabelsList'
|
|
import './components/historyLabel'
|
|
import './components/historyFileTree'
|
|
import './components/historyFileEntity'
|
|
import { paywallPrompt } from '../../../../frontend/js/main/account-upgrade'
|
|
let HistoryManager
|
|
|
|
export default HistoryManager = (function () {
|
|
HistoryManager = class HistoryManager {
|
|
static initClass() {
|
|
this.prototype.MAX_RECENT_UPDATES_TO_SELECT = 5
|
|
this.prototype.BATCH_SIZE = 10
|
|
}
|
|
|
|
constructor(ide, $scope, localStorage) {
|
|
this.labelCurrentVersion = this.labelCurrentVersion.bind(this)
|
|
this.deleteLabel = this.deleteLabel.bind(this)
|
|
this._addLabelLocally = this._addLabelLocally.bind(this)
|
|
this.ide = ide
|
|
this.$scope = $scope
|
|
this.localStorage = localStorage
|
|
this.$scope.HistoryViewModes = HistoryViewModes
|
|
this._localStorageViewModeProjKey = `history.userPrefs.viewMode.${$scope.project_id}`
|
|
this._localStorageShowOnlyLabelsProjKey = `history.userPrefs.showOnlyLabels.${$scope.project_id}`
|
|
this._previouslySelectedPathname = null
|
|
this._loadFileTreeRequestCanceller = null
|
|
this.hardReset()
|
|
|
|
this.$scope.toggleHistory = () => {
|
|
if (this.$scope.ui.view === 'history') {
|
|
this.hide()
|
|
} else {
|
|
this.show()
|
|
this._handleHistoryUIStateChange()
|
|
}
|
|
this.ide.$timeout(() => {
|
|
this.$scope.$broadcast('history:toggle')
|
|
}, 0)
|
|
}
|
|
|
|
this.$scope.isHistoryLoading = () => {
|
|
const selection = this.$scope.history.selection
|
|
return (
|
|
this.$scope.history.loadingFileTree ||
|
|
(this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME &&
|
|
selection.file &&
|
|
selection.file.loading) ||
|
|
(this.$scope.history.viewMode === HistoryViewModes.COMPARE &&
|
|
selection.diff &&
|
|
selection.diff.loading)
|
|
)
|
|
}
|
|
}
|
|
|
|
show() {
|
|
this.$scope.ui.view = 'history'
|
|
this.hardReset()
|
|
if (this.$scope.history.showOnlyLabels) {
|
|
this.fetchNextBatchOfUpdates()
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
this.$scope.ui.view = 'editor'
|
|
}
|
|
|
|
_getViewModeUserPref() {
|
|
return (
|
|
this.localStorage(this._localStorageViewModeProjKey) ||
|
|
HistoryViewModes.POINT_IN_TIME
|
|
)
|
|
}
|
|
|
|
_getShowOnlyLabelsUserPref() {
|
|
return this.localStorage(this._localStorageShowOnlyLabelsProjKey) || false
|
|
}
|
|
|
|
_setViewModeUserPref(viewModeUserPref) {
|
|
if (
|
|
viewModeUserPref === HistoryViewModes.POINT_IN_TIME ||
|
|
viewModeUserPref === HistoryViewModes.COMPARE
|
|
) {
|
|
this.localStorage(this._localStorageViewModeProjKey, viewModeUserPref)
|
|
}
|
|
}
|
|
|
|
_setShowOnlyLabelsUserPref(showOnlyLabelsUserPref) {
|
|
this.localStorage(
|
|
this._localStorageShowOnlyLabelsProjKey,
|
|
!!showOnlyLabelsUserPref
|
|
)
|
|
}
|
|
|
|
hardReset() {
|
|
this.$scope.history = {
|
|
isV2: true,
|
|
updates: [],
|
|
viewMode: this._getViewModeUserPref(),
|
|
nextBeforeTimestamp: null,
|
|
loading: false,
|
|
atEnd: false,
|
|
userHasFullFeature: undefined,
|
|
freeHistoryLimitHit: false,
|
|
selection: {
|
|
docs: {},
|
|
pathname: null,
|
|
range: {
|
|
fromV: null,
|
|
toV: null,
|
|
},
|
|
hoveredRange: {
|
|
fromV: null,
|
|
toV: null,
|
|
},
|
|
diff: null,
|
|
files: [],
|
|
file: null,
|
|
},
|
|
error: null,
|
|
showOnlyLabels: this._getShowOnlyLabelsUserPref(),
|
|
labels: null,
|
|
loadingFileTree: true,
|
|
}
|
|
const _deregisterFeatureWatcher = this.$scope.$watch(
|
|
'project.features.versioning',
|
|
hasVersioning => {
|
|
if (hasVersioning != null) {
|
|
this.$scope.history.userHasFullFeature = hasVersioning
|
|
if (this.$scope.user.isAdmin) {
|
|
this.$scope.history.userHasFullFeature = true
|
|
}
|
|
_deregisterFeatureWatcher()
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
softReset() {
|
|
this.$scope.history.viewMode = this._getViewModeUserPref()
|
|
this.$scope.history.selection = {
|
|
docs: {},
|
|
pathname: null,
|
|
range: {
|
|
fromV: null,
|
|
toV: null,
|
|
},
|
|
hoveredRange: {
|
|
fromV: null,
|
|
toV: null,
|
|
},
|
|
diff: null, // When history.viewMode == HistoryViewModes.COMPARE
|
|
files: [], // When history.viewMode == HistoryViewModes.COMPARE
|
|
file: null,
|
|
}
|
|
this.$scope.history.error = null
|
|
this.$scope.history.showOnlyLabels = this._getShowOnlyLabelsUserPref()
|
|
}
|
|
|
|
toggleHistoryViewMode() {
|
|
if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) {
|
|
this.softReset()
|
|
this.$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
|
|
this._setViewModeUserPref(HistoryViewModes.POINT_IN_TIME)
|
|
} else {
|
|
this.softReset()
|
|
this.$scope.history.viewMode = HistoryViewModes.COMPARE
|
|
this._setViewModeUserPref(HistoryViewModes.COMPARE)
|
|
}
|
|
this._handleHistoryUIStateChange()
|
|
this.ide.$timeout(() => {
|
|
this.$scope.$broadcast('history:toggle')
|
|
}, 0)
|
|
}
|
|
|
|
_handleHistoryUIStateChange() {
|
|
if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) {
|
|
if (this.$scope.history.showOnlyLabels) {
|
|
this.autoSelectLabelsForComparison()
|
|
} else {
|
|
this.autoSelectRecentUpdates()
|
|
}
|
|
} else {
|
|
// Point-in-time mode
|
|
if (this.$scope.history.showOnlyLabels) {
|
|
this.autoSelectLabelForPointInTime()
|
|
} else {
|
|
this.autoSelectVersionForPointInTime()
|
|
}
|
|
}
|
|
}
|
|
|
|
setHoverFrom(fromV) {
|
|
const selection = this.$scope.history.selection
|
|
selection.hoveredRange.fromV = fromV
|
|
selection.hoveredRange.toV = selection.range.toV
|
|
this.$scope.history.hoveringOverListSelectors = true
|
|
}
|
|
|
|
setHoverTo(toV) {
|
|
const selection = this.$scope.history.selection
|
|
selection.hoveredRange.toV = toV
|
|
selection.hoveredRange.fromV = selection.range.fromV
|
|
this.$scope.history.hoveringOverListSelectors = true
|
|
}
|
|
|
|
resetHover() {
|
|
const selection = this.$scope.history.selection
|
|
selection.hoveredRange.toV = null
|
|
selection.hoveredRange.fromV = null
|
|
this.$scope.history.hoveringOverListSelectors = false
|
|
}
|
|
|
|
showAllUpdates() {
|
|
if (this.$scope.history.showOnlyLabels) {
|
|
this.$scope.history.showOnlyLabels = false
|
|
this._setShowOnlyLabelsUserPref(false)
|
|
this._handleHistoryUIStateChange()
|
|
}
|
|
}
|
|
|
|
showOnlyLabels() {
|
|
if (!this.$scope.history.showOnlyLabels) {
|
|
this.$scope.history.showOnlyLabels = true
|
|
this._setShowOnlyLabelsUserPref(true)
|
|
this._handleHistoryUIStateChange()
|
|
}
|
|
}
|
|
|
|
restoreFile(version, pathname) {
|
|
const url = `/project/${this.$scope.project_id}/restore_file`
|
|
|
|
return this.ide.$http.post(url, {
|
|
version,
|
|
pathname,
|
|
_csrf: window.csrfToken,
|
|
})
|
|
}
|
|
|
|
loadFileTreeForVersion(version) {
|
|
return this._loadFileTree(version, version)
|
|
}
|
|
|
|
loadFileTreeDiff(toV, fromV) {
|
|
return this._loadFileTree(toV, fromV)
|
|
}
|
|
|
|
_loadFileTree(toV, fromV) {
|
|
let url = `/project/${this.$scope.project_id}/filetree/diff`
|
|
const selection = this.$scope.history.selection
|
|
const query = [`from=${fromV}`, `to=${toV}`]
|
|
url += `?${query.join('&')}`
|
|
|
|
this.$scope.$applyAsync(
|
|
() => (this.$scope.history.loadingFileTree = true)
|
|
)
|
|
|
|
selection.file = null
|
|
selection.pathname = null
|
|
|
|
// If `this._loadFileTreeRequestCanceller` is not null, then we have a request inflight
|
|
if (this._loadFileTreeRequestCanceller != null) {
|
|
// Resolving it will cancel the inflight request (or, rather, ignore its result)
|
|
this._loadFileTreeRequestCanceller.resolve()
|
|
}
|
|
this._loadFileTreeRequestCanceller = this.ide.$q.defer()
|
|
|
|
return this.ide.$http
|
|
.get(url, { timeout: this._loadFileTreeRequestCanceller.promise })
|
|
.then(response => {
|
|
this.$scope.history.selection.files = response.data.diff
|
|
for (const file of this.$scope.history.selection.files) {
|
|
if (file.newPathname != null) {
|
|
file.oldPathname = file.pathname
|
|
file.pathname = file.newPathname
|
|
delete file.newPathname
|
|
}
|
|
}
|
|
this._loadFileTreeRequestCanceller = null
|
|
this.$scope.history.loadingFileTree = false
|
|
this.autoSelectFile()
|
|
})
|
|
.catch(err => {
|
|
if (err.status !== -1) {
|
|
this._loadFileTreeRequestCanceller = null
|
|
} else {
|
|
this.$scope.history.loadingFileTree = false
|
|
}
|
|
})
|
|
}
|
|
|
|
selectFile(file) {
|
|
if (file != null && file.pathname != null) {
|
|
this.$scope.history.selection.pathname =
|
|
this._previouslySelectedPathname = file.pathname
|
|
this.$scope.history.selection.file = file
|
|
if (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME) {
|
|
this.loadFileAtPointInTime()
|
|
} else {
|
|
this.reloadDiff()
|
|
}
|
|
}
|
|
}
|
|
|
|
autoSelectFile() {
|
|
const selectedPathname = null
|
|
const files = this.$scope.history.selection.files
|
|
let fileToSelect = null
|
|
let previouslySelectedFile = null
|
|
let previouslySelectedFileHasOp = false
|
|
const filesWithOps = this._getFilesWithOps()
|
|
const orderedOpTypes = ['edited', 'added', 'renamed', 'removed']
|
|
|
|
if (this._previouslySelectedPathname != null) {
|
|
previouslySelectedFile = _.find(files, {
|
|
pathname: this._previouslySelectedPathname,
|
|
})
|
|
previouslySelectedFileHasOp = _.some(filesWithOps, {
|
|
pathname: this._previouslySelectedPathname,
|
|
})
|
|
}
|
|
if (previouslySelectedFile != null && previouslySelectedFileHasOp) {
|
|
fileToSelect = previouslySelectedFile
|
|
} else {
|
|
for (const opType of orderedOpTypes) {
|
|
const fileWithMatchingOpType = _.find(filesWithOps, {
|
|
operation: opType,
|
|
})
|
|
if (fileWithMatchingOpType != null) {
|
|
fileToSelect = _.find(files, {
|
|
pathname: fileWithMatchingOpType.pathname,
|
|
})
|
|
break
|
|
}
|
|
}
|
|
if (fileToSelect == null) {
|
|
if (previouslySelectedFile != null) {
|
|
fileToSelect = previouslySelectedFile
|
|
} else {
|
|
const mainFile = _.find(files, function (file) {
|
|
return /main\.tex$/.test(file.pathname)
|
|
})
|
|
if (mainFile != null) {
|
|
fileToSelect = mainFile
|
|
} else {
|
|
const anyTeXFile = _.find(files, function (file) {
|
|
return /\.tex$/.test(file.pathname)
|
|
})
|
|
if (anyTeXFile != null) {
|
|
fileToSelect = anyTeXFile
|
|
} else {
|
|
fileToSelect = files[0]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this.selectFile(fileToSelect)
|
|
}
|
|
|
|
_getFilesWithOps() {
|
|
let filesWithOps
|
|
if (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME) {
|
|
const currentUpdate = this.getUpdateForVersion(
|
|
this.$scope.history.selection.range.toV
|
|
)
|
|
filesWithOps = []
|
|
if (currentUpdate != null) {
|
|
for (const pathname of currentUpdate.pathnames) {
|
|
filesWithOps.push({
|
|
pathname,
|
|
operation: 'edited',
|
|
})
|
|
}
|
|
for (const op of currentUpdate.project_ops) {
|
|
let fileWithOp
|
|
if (op.add != null) {
|
|
fileWithOp = {
|
|
pathname: op.add.pathname,
|
|
operation: 'added',
|
|
}
|
|
} else if (op.remove != null) {
|
|
fileWithOp = {
|
|
pathname: op.remove.pathname,
|
|
operation: 'removed',
|
|
}
|
|
} else if (op.rename != null) {
|
|
fileWithOp = {
|
|
pathname: op.rename.newPathname,
|
|
operation: 'renamed',
|
|
}
|
|
}
|
|
if (fileWithOp != null) {
|
|
filesWithOps.push(fileWithOp)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
filesWithOps = _.reduce(
|
|
this.$scope.history.selection.files,
|
|
(curFilesWithOps, file) => {
|
|
if (file.operation) {
|
|
curFilesWithOps.push({
|
|
pathname: file.pathname,
|
|
operation: file.operation,
|
|
})
|
|
}
|
|
return curFilesWithOps
|
|
},
|
|
[]
|
|
)
|
|
}
|
|
return filesWithOps
|
|
}
|
|
|
|
autoSelectRecentUpdates() {
|
|
if (this.$scope.history.updates.length === 0) {
|
|
return
|
|
}
|
|
|
|
const toV = this.$scope.history.updates[0].toV
|
|
let fromV = null
|
|
|
|
let indexOfLastUpdateNotByMe = 0
|
|
for (let i = 0; i < this.$scope.history.updates.length; i++) {
|
|
const update = this.$scope.history.updates[i]
|
|
if (
|
|
this._updateContainsUserId(update, this.$scope.user.id) ||
|
|
i > this.MAX_RECENT_UPDATES_TO_SELECT
|
|
) {
|
|
break
|
|
}
|
|
indexOfLastUpdateNotByMe = i
|
|
}
|
|
|
|
fromV = this.$scope.history.updates[indexOfLastUpdateNotByMe].fromV
|
|
this.selectVersionsForCompare(toV, fromV)
|
|
}
|
|
|
|
autoSelectVersionForPointInTime() {
|
|
if (this.$scope.history.updates.length === 0) {
|
|
return
|
|
}
|
|
let versionToSelect = this.$scope.history.updates[0].toV
|
|
const range = this.$scope.history.selection.range
|
|
if (
|
|
range.toV != null &&
|
|
range.fromV != null &&
|
|
range.toV === range.fromV
|
|
) {
|
|
versionToSelect = range.toV
|
|
}
|
|
this.selectVersionForPointInTime(versionToSelect)
|
|
}
|
|
|
|
autoSelectLastLabel() {
|
|
if (
|
|
this.$scope.history.labels == null ||
|
|
this.$scope.history.labels.length === 0
|
|
) {
|
|
return
|
|
}
|
|
return this.selectLabelForPointInTime(this.$scope.history.labels[0])
|
|
}
|
|
|
|
expandSelectionToVersion(version) {
|
|
if (version > this.$scope.history.selection.range.toV) {
|
|
this.$scope.history.selection.range.toV = version
|
|
} else if (version < this.$scope.history.selection.range.fromV) {
|
|
this.$scope.history.selection.range.fromV = version
|
|
}
|
|
}
|
|
|
|
selectVersionForPointInTime(version) {
|
|
const selection = this.$scope.history.selection
|
|
if (
|
|
selection.range.toV !== version &&
|
|
selection.range.fromV !== version
|
|
) {
|
|
selection.range.toV = version
|
|
selection.range.fromV = version
|
|
this.loadFileTreeForVersion(version)
|
|
}
|
|
}
|
|
|
|
selectVersionsForCompare(toV, fromV) {
|
|
const range = this.$scope.history.selection.range
|
|
if (range.toV !== toV || range.fromV !== fromV) {
|
|
range.toV = toV
|
|
range.fromV = fromV
|
|
this.loadFileTreeDiff(toV, fromV)
|
|
}
|
|
}
|
|
|
|
autoSelectLabelForPointInTime() {
|
|
const selectedUpdate = this.getUpdateForVersion(
|
|
this.$scope.history.selection.range.toV
|
|
)
|
|
let nSelectedLabels = 0
|
|
|
|
if (selectedUpdate != null && selectedUpdate.labels != null) {
|
|
nSelectedLabels = selectedUpdate.labels.length
|
|
}
|
|
|
|
// If the currently selected update has no labels, select the last one (version-wise)
|
|
if (nSelectedLabels === 0) {
|
|
this.autoSelectLastLabel()
|
|
// If the update has one label, select it
|
|
} else if (nSelectedLabels === 1) {
|
|
this.selectLabelForPointInTime(selectedUpdate.labels[0])
|
|
// If there are multiple labels for the update, select the latest
|
|
} else if (nSelectedLabels > 1) {
|
|
const sortedLabels = this.ide.$filter('orderBy')(
|
|
selectedUpdate.labels,
|
|
'-created_at'
|
|
)
|
|
const lastLabelFromUpdate = sortedLabels[0]
|
|
this.selectLabelForPointInTime(lastLabelFromUpdate)
|
|
}
|
|
}
|
|
|
|
selectLabelForPointInTime(labelToSelect) {
|
|
let updateToSelect = null
|
|
|
|
if (this._isLabelSelected(labelToSelect)) {
|
|
// Label already selected
|
|
return
|
|
}
|
|
|
|
for (const update of Array.from(this.$scope.history.updates)) {
|
|
if (update.toV === labelToSelect.version) {
|
|
updateToSelect = update
|
|
break
|
|
}
|
|
}
|
|
|
|
if (updateToSelect != null) {
|
|
this.selectVersionForPointInTime(updateToSelect.toV)
|
|
} else {
|
|
const selection = this.$scope.history.selection
|
|
selection.range.toV = labelToSelect.version
|
|
selection.range.fromV = labelToSelect.version
|
|
this.loadFileTreeForVersion(labelToSelect.version)
|
|
}
|
|
}
|
|
|
|
getUpdateForVersion(version) {
|
|
for (const update of this.$scope.history.updates) {
|
|
if (update.toV === version) {
|
|
return update
|
|
}
|
|
}
|
|
}
|
|
|
|
autoSelectLabelsForComparison() {
|
|
const labels = this.$scope.history.labels
|
|
let nLabels = 0
|
|
if (Array.isArray(labels)) {
|
|
nLabels = labels.length
|
|
}
|
|
if (nLabels === 0) {
|
|
// TODO better handling
|
|
} else if (nLabels === 1) {
|
|
this.selectVersionsForCompare(labels[0].version, labels[0].version)
|
|
} else if (nLabels > 1) {
|
|
this.selectVersionsForCompare(labels[0].version, labels[1].version)
|
|
}
|
|
}
|
|
|
|
fetchNextBatchOfUpdates() {
|
|
if (this.$scope.history.atEnd) {
|
|
return
|
|
}
|
|
|
|
let updatesURL = `/project/${this.ide.project_id}/updates?min_count=${this.BATCH_SIZE}`
|
|
if (this.$scope.history.nextBeforeTimestamp != null) {
|
|
updatesURL += `&before=${this.$scope.history.nextBeforeTimestamp}`
|
|
}
|
|
const labelsURL = `/project/${this.ide.project_id}/labels`
|
|
|
|
const requests = { updates: this.ide.$http.get(updatesURL) }
|
|
|
|
if (this.$scope.history.labels == null) {
|
|
requests.labels = this.ide.$http.get(labelsURL)
|
|
}
|
|
|
|
this.$scope.history.loading = true
|
|
return this.ide.$q
|
|
.all(requests)
|
|
.then(response => {
|
|
this.$scope.history.loading = false
|
|
const updatesData = response.updates.data
|
|
let lastUpdateToV = null
|
|
|
|
if (updatesData.updates.length > 0) {
|
|
lastUpdateToV = updatesData.updates[0].toV
|
|
}
|
|
if (response.labels != null) {
|
|
this.$scope.history.labels = this._loadLabels(
|
|
response.labels.data,
|
|
lastUpdateToV
|
|
)
|
|
}
|
|
this._loadUpdates(updatesData.updates)
|
|
this.$scope.history.nextBeforeTimestamp =
|
|
updatesData.nextBeforeTimestamp
|
|
if (
|
|
updatesData.nextBeforeTimestamp == null ||
|
|
this.$scope.history.freeHistoryLimitHit ||
|
|
this.$scope.history.updates.length === 0
|
|
) {
|
|
this.$scope.history.atEnd = true
|
|
}
|
|
if (this.$scope.history.updates.length === 0) {
|
|
this.$scope.history.loadingFileTree = false
|
|
}
|
|
})
|
|
.catch(error => {
|
|
this.$scope.history.loading = false
|
|
const { status, statusText } = error
|
|
this.$scope.history.error = { status, statusText }
|
|
this.$scope.history.atEnd = true
|
|
this.$scope.history.loadingFileTree = false
|
|
})
|
|
}
|
|
|
|
_loadLabels(labels, lastUpdateToV) {
|
|
const sortedLabels = this._sortLabelsByVersionAndDate(labels)
|
|
const labelsWithoutPseudoLabel =
|
|
this._deletePseudoCurrentStateLabelIfExistent(sortedLabels)
|
|
const labelsWithPseudoLabelIfNeeded =
|
|
this._addPseudoCurrentStateLabelIfNeeded(
|
|
labelsWithoutPseudoLabel,
|
|
lastUpdateToV
|
|
)
|
|
return labelsWithPseudoLabelIfNeeded
|
|
}
|
|
|
|
_deletePseudoCurrentStateLabelIfExistent(labels) {
|
|
if (labels.length && labels[0].isPseudoCurrentStateLabel) {
|
|
labels.shift()
|
|
}
|
|
return labels
|
|
}
|
|
|
|
_addPseudoCurrentStateLabelIfNeeded(labels, mostRecentVersion) {
|
|
if (
|
|
(labels.length && labels[0].version !== mostRecentVersion) ||
|
|
labels.length === 0
|
|
) {
|
|
const pseudoCurrentStateLabel = {
|
|
id: '1',
|
|
isPseudoCurrentStateLabel: true,
|
|
version: mostRecentVersion,
|
|
created_at: new Date().toISOString(),
|
|
}
|
|
labels.unshift(pseudoCurrentStateLabel)
|
|
}
|
|
return labels
|
|
}
|
|
|
|
_sortLabelsByVersionAndDate(labels) {
|
|
return this.ide.$filter('orderBy')(labels, [
|
|
'isPseudoCurrentStateLabel',
|
|
'-version',
|
|
'-created_at',
|
|
])
|
|
}
|
|
|
|
loadFileAtPointInTime() {
|
|
const toV = this.$scope.history.selection.range.toV
|
|
const { pathname } = this.$scope.history.selection
|
|
if (toV == null) {
|
|
return
|
|
}
|
|
let url = `/project/${this.$scope.project_id}/diff`
|
|
const query = [
|
|
`pathname=${encodeURIComponent(pathname)}`,
|
|
`from=${toV}`,
|
|
`to=${toV}`,
|
|
]
|
|
url += `?${query.join('&')}`
|
|
this.$scope.history.selection.file.loading = true
|
|
return this.ide.$http
|
|
.get(url)
|
|
.then(response => {
|
|
const { text, binary } = this._parseDiff(response.data.diff)
|
|
this.$scope.history.selection.file.binary = binary
|
|
this.$scope.history.selection.file.text = text
|
|
this.$scope.history.selection.file.loading = false
|
|
})
|
|
.catch(function () {})
|
|
}
|
|
|
|
reloadDiff() {
|
|
let { diff } = this.$scope.history.selection
|
|
const { range, pathname } = this.$scope.history.selection
|
|
const { fromV, toV } = range
|
|
|
|
if (pathname == null) {
|
|
this.$scope.history.selection.diff = null
|
|
return
|
|
}
|
|
|
|
if (
|
|
diff != null &&
|
|
diff.pathname === pathname &&
|
|
diff.fromV === fromV &&
|
|
diff.toV === toV
|
|
) {
|
|
return
|
|
}
|
|
|
|
this.$scope.history.selection.diff = diff = {
|
|
fromV,
|
|
toV,
|
|
pathname,
|
|
error: false,
|
|
}
|
|
|
|
diff.loading = true
|
|
let url = `/project/${this.$scope.project_id}/diff`
|
|
const query = [`pathname=${encodeURIComponent(pathname)}`]
|
|
if (diff.fromV != null && diff.toV != null) {
|
|
query.push(`from=${diff.fromV}`, `to=${diff.toV}`)
|
|
}
|
|
url += `?${query.join('&')}`
|
|
return this.ide.$http
|
|
.get(url)
|
|
.then(response => {
|
|
const { data } = response
|
|
diff.loading = false
|
|
const { text, highlights, binary } = this._parseDiff(data.diff)
|
|
diff.binary = binary
|
|
diff.text = text
|
|
diff.highlights = highlights
|
|
})
|
|
.catch(function () {
|
|
diff.loading = false
|
|
diff.error = true
|
|
})
|
|
}
|
|
|
|
labelCurrentVersion(labelComment) {
|
|
return this._labelVersion(
|
|
labelComment,
|
|
this.$scope.history.selection.range.toV
|
|
)
|
|
}
|
|
|
|
deleteLabel(label) {
|
|
const url = `/project/${this.$scope.project_id}/labels/${label.id}`
|
|
|
|
return this.ide
|
|
.$http({
|
|
url,
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-Token': window.csrfToken,
|
|
},
|
|
})
|
|
.then(response => {
|
|
return this._deleteLabelLocally(label)
|
|
})
|
|
}
|
|
|
|
_deleteLabelLocally(labelToDelete) {
|
|
for (let i = 0; i < this.$scope.history.updates.length; i++) {
|
|
const update = this.$scope.history.updates[i]
|
|
if (update.toV === labelToDelete.version) {
|
|
update.labels = _.filter(
|
|
update.labels,
|
|
label => label.id !== labelToDelete.id
|
|
)
|
|
break
|
|
}
|
|
}
|
|
this.$scope.history.labels = this._loadLabels(
|
|
_.filter(
|
|
this.$scope.history.labels,
|
|
label => label.id !== labelToDelete.id
|
|
),
|
|
this.$scope.history.updates[0].toV
|
|
)
|
|
this._handleHistoryUIStateChange()
|
|
}
|
|
|
|
_isLabelSelected(label) {
|
|
if (label) {
|
|
return (
|
|
label.version <= this.$scope.history.selection.range.toV &&
|
|
label.version >= this.$scope.history.selection.range.fromV
|
|
)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
_parseDiff(diff) {
|
|
if (diff.binary) {
|
|
return { binary: true }
|
|
}
|
|
let row = 0
|
|
let column = 0
|
|
const highlights = []
|
|
let text = ''
|
|
const iterable = diff || []
|
|
for (let i = 0; i < iterable.length; i++) {
|
|
let endColumn, endRow
|
|
const entry = iterable[i]
|
|
let content = entry.u || entry.i || entry.d
|
|
if (!content) {
|
|
content = ''
|
|
}
|
|
text += content
|
|
const lines = content.split('\n')
|
|
const startRow = row
|
|
const 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
|
|
|
|
const range = {
|
|
start: {
|
|
row: startRow,
|
|
column: startColumn,
|
|
},
|
|
end: {
|
|
row: endRow,
|
|
column: endColumn,
|
|
},
|
|
}
|
|
|
|
if (entry.i != null || entry.d != null) {
|
|
const user =
|
|
entry.meta.users != null ? entry.meta.users[0] : undefined
|
|
const name = displayNameForUser(user)
|
|
const date = moment(entry.meta.end_ts).format('Do MMM YYYY, h:mm a')
|
|
if (entry.i != null) {
|
|
highlights.push({
|
|
label: `Added by ${name} on ${date}`,
|
|
highlight: range,
|
|
hue: ColorManager.getHueForUserId(
|
|
user != null ? user.id : undefined
|
|
),
|
|
})
|
|
} else if (entry.d != null) {
|
|
highlights.push({
|
|
label: `Deleted by ${name} on ${date}`,
|
|
strikeThrough: range,
|
|
hue: ColorManager.getHueForUserId(
|
|
user != null ? user.id : undefined
|
|
),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return { text, highlights }
|
|
}
|
|
|
|
_loadUpdates(updates) {
|
|
if (updates == null) {
|
|
updates = []
|
|
}
|
|
let previousUpdate =
|
|
this.$scope.history.updates[this.$scope.history.updates.length - 1]
|
|
const dateTimeNow = new Date()
|
|
const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
|
|
let cutOffIndex = null
|
|
|
|
const iterable = updates || []
|
|
for (let i = 0; i < iterable.length; i++) {
|
|
const update = iterable[i]
|
|
for (const user of Array.from(update.meta.users || [])) {
|
|
if (user != null) {
|
|
user.hue = ColorManager.getHueForUserId(user.id)
|
|
}
|
|
}
|
|
if (
|
|
previousUpdate == null ||
|
|
!moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, 'day')
|
|
) {
|
|
update.meta.first_in_day = true
|
|
}
|
|
|
|
previousUpdate = update
|
|
|
|
if (
|
|
!this.$scope.history.userHasFullFeature &&
|
|
update.meta.end_ts < timestamp24hoursAgo
|
|
) {
|
|
cutOffIndex = i || 1 // Make sure that we show at least one entry (to allow labelling).
|
|
this.$scope.history.freeHistoryLimitHit = true
|
|
if (this.$scope.project.owner._id === this.$scope.user.id) {
|
|
ga(
|
|
'send',
|
|
'event',
|
|
'subscription-funnel',
|
|
'editor-click-feature',
|
|
'history'
|
|
)
|
|
paywallPrompt('history')
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const firstLoad = this.$scope.history.updates.length === 0
|
|
|
|
if (!this.$scope.history.userHasFullFeature && cutOffIndex != null) {
|
|
updates = updates.slice(0, cutOffIndex)
|
|
}
|
|
|
|
this.$scope.history.updates = this.$scope.history.updates.concat(updates)
|
|
|
|
if (firstLoad) {
|
|
this._handleHistoryUIStateChange()
|
|
}
|
|
}
|
|
|
|
_labelVersion(comment, version) {
|
|
const url = `/project/${this.$scope.project_id}/labels`
|
|
return this.ide.$http
|
|
.post(url, {
|
|
comment,
|
|
version,
|
|
_csrf: window.csrfToken,
|
|
})
|
|
.then(response => {
|
|
return this._addLabelLocally(response.data)
|
|
})
|
|
}
|
|
|
|
_addLabelLocally(label) {
|
|
const localUpdate = _.find(
|
|
this.$scope.history.updates,
|
|
update => update.toV === label.version
|
|
)
|
|
if (localUpdate != null) {
|
|
localUpdate.labels = localUpdate.labels.concat(label)
|
|
}
|
|
this.$scope.history.labels = this._loadLabels(
|
|
this.$scope.history.labels.concat(label),
|
|
this.$scope.history.updates[0].toV
|
|
)
|
|
this._handleHistoryUIStateChange()
|
|
}
|
|
|
|
_updateContainsUserId(update, user_id) {
|
|
for (const user of Array.from(update.meta.users)) {
|
|
if ((user != null ? user.id : undefined) === user_id) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
HistoryManager.initClass()
|
|
return HistoryManager
|
|
})()
|
|
|
|
function __guard__(value, transform) {
|
|
return typeof value !== 'undefined' && value !== null
|
|
? transform(value)
|
|
: undefined
|
|
}
|