HistoryV2Manager.js 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. import _ from 'lodash'
  2. /* eslint-disable
  3. camelcase,
  4. max-len,
  5. no-return-assign,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS103: Rewrite code to no longer use __guard__
  15. * DS205: Consider reworking code to avoid use of IIFEs
  16. * DS206: Consider reworking classes to avoid initClass
  17. * DS207: Consider shorter variations of null checks
  18. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  19. */
  20. import moment from 'moment'
  21. import ColorManager from '../colors/ColorManager'
  22. import displayNameForUser from './util/displayNameForUser'
  23. import HistoryViewModes from './util/HistoryViewModes'
  24. import './controllers/HistoryV2ListController'
  25. import './controllers/HistoryV2FileTreeController'
  26. import './controllers/HistoryV2ToolbarController'
  27. import './controllers/HistoryV2AddLabelModalController'
  28. import './controllers/HistoryV2DeleteLabelModalController'
  29. import './directives/infiniteScroll'
  30. import './directives/historyDraggableBoundary'
  31. import './directives/historyDroppableArea'
  32. import './components/historyEntriesList'
  33. import './components/historyEntry'
  34. import './components/historyLabelsList'
  35. import './components/historyLabel'
  36. import './components/historyFileTree'
  37. import './components/historyFileEntity'
  38. import { paywallPrompt } from '../../../../frontend/js/main/account-upgrade'
  39. import getMeta from '../../utils/meta'
  40. let HistoryManager
  41. export default HistoryManager = (function () {
  42. HistoryManager = class HistoryManager {
  43. static initClass() {
  44. this.prototype.MAX_RECENT_UPDATES_TO_SELECT = 5
  45. this.prototype.BATCH_SIZE = 10
  46. }
  47. constructor(ide, $scope, localStorage) {
  48. this.labelCurrentVersion = this.labelCurrentVersion.bind(this)
  49. this.deleteLabel = this.deleteLabel.bind(this)
  50. this._addLabelLocally = this._addLabelLocally.bind(this)
  51. this.ide = ide
  52. this.$scope = $scope
  53. this.localStorage = localStorage
  54. this.$scope.HistoryViewModes = HistoryViewModes
  55. this._localStorageViewModeProjKey = `history.userPrefs.viewMode.${$scope.project_id}`
  56. this._localStorageShowOnlyLabelsProjKey = `history.userPrefs.showOnlyLabels.${$scope.project_id}`
  57. this._previouslySelectedPathname = null
  58. this._loadFileTreeRequestCanceller = null
  59. this.hardReset()
  60. this.$scope.toggleHistory = () => {
  61. if (this.$scope.ui.view === 'history') {
  62. this.hide()
  63. } else {
  64. this.show()
  65. this._handleHistoryUIStateChange()
  66. }
  67. this.ide.$timeout(() => {
  68. this.$scope.$broadcast('history:toggle')
  69. }, 0)
  70. }
  71. this.$scope.isHistoryLoading = () => {
  72. const selection = this.$scope.history.selection
  73. return (
  74. this.$scope.history.loadingFileTree ||
  75. (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME &&
  76. selection.file &&
  77. selection.file.loading) ||
  78. (this.$scope.history.viewMode === HistoryViewModes.COMPARE &&
  79. selection.diff &&
  80. selection.diff.loading)
  81. )
  82. }
  83. }
  84. show() {
  85. this.$scope.ui.view = 'history'
  86. this.hardReset()
  87. if (this.$scope.history.showOnlyLabels) {
  88. this.fetchNextBatchOfUpdates()
  89. }
  90. }
  91. hide() {
  92. this.$scope.ui.view = 'editor'
  93. }
  94. _getViewModeUserPref() {
  95. return (
  96. this.localStorage(this._localStorageViewModeProjKey) ||
  97. HistoryViewModes.POINT_IN_TIME
  98. )
  99. }
  100. _getShowOnlyLabelsUserPref() {
  101. return this.localStorage(this._localStorageShowOnlyLabelsProjKey) || false
  102. }
  103. _setViewModeUserPref(viewModeUserPref) {
  104. if (
  105. viewModeUserPref === HistoryViewModes.POINT_IN_TIME ||
  106. viewModeUserPref === HistoryViewModes.COMPARE
  107. ) {
  108. this.localStorage(this._localStorageViewModeProjKey, viewModeUserPref)
  109. }
  110. }
  111. _setShowOnlyLabelsUserPref(showOnlyLabelsUserPref) {
  112. this.localStorage(
  113. this._localStorageShowOnlyLabelsProjKey,
  114. !!showOnlyLabelsUserPref
  115. )
  116. }
  117. isReact() {
  118. return getMeta('ol-splitTestVariants')?.['history-view'] === 'react'
  119. }
  120. hardReset() {
  121. this.$scope.history = {
  122. isV2: true,
  123. updates: [],
  124. viewMode: this._getViewModeUserPref(),
  125. nextBeforeTimestamp: null,
  126. loading: false,
  127. atEnd: false,
  128. userHasFullFeature: undefined,
  129. freeHistoryLimitHit: false,
  130. selection: {
  131. docs: {},
  132. pathname: null,
  133. range: {
  134. fromV: null,
  135. toV: null,
  136. },
  137. hoveredRange: {
  138. fromV: null,
  139. toV: null,
  140. },
  141. diff: null,
  142. files: [],
  143. file: null,
  144. },
  145. error: null,
  146. showOnlyLabels: this._getShowOnlyLabelsUserPref(),
  147. labels: null,
  148. loadingFileTree: true,
  149. isReact: this.isReact(),
  150. }
  151. const _deregisterFeatureWatcher = this.$scope.$watch(
  152. 'project.features.versioning',
  153. hasVersioning => {
  154. if (hasVersioning != null) {
  155. this.$scope.history.userHasFullFeature = hasVersioning
  156. if (this.$scope.user.isAdmin) {
  157. this.$scope.history.userHasFullFeature = true
  158. }
  159. _deregisterFeatureWatcher()
  160. }
  161. }
  162. )
  163. }
  164. softReset() {
  165. this.$scope.history.viewMode = this._getViewModeUserPref()
  166. this.$scope.history.selection = {
  167. docs: {},
  168. pathname: null,
  169. range: {
  170. fromV: null,
  171. toV: null,
  172. },
  173. hoveredRange: {
  174. fromV: null,
  175. toV: null,
  176. },
  177. diff: null, // When history.viewMode == HistoryViewModes.COMPARE
  178. files: [], // When history.viewMode == HistoryViewModes.COMPARE
  179. file: null,
  180. }
  181. this.$scope.history.error = null
  182. this.$scope.history.showOnlyLabels = this._getShowOnlyLabelsUserPref()
  183. }
  184. toggleHistoryViewMode() {
  185. if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) {
  186. this.softReset()
  187. this.$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
  188. this._setViewModeUserPref(HistoryViewModes.POINT_IN_TIME)
  189. } else {
  190. this.softReset()
  191. this.$scope.history.viewMode = HistoryViewModes.COMPARE
  192. this._setViewModeUserPref(HistoryViewModes.COMPARE)
  193. }
  194. this._handleHistoryUIStateChange()
  195. this.ide.$timeout(() => {
  196. this.$scope.$broadcast('history:toggle')
  197. }, 0)
  198. }
  199. _handleHistoryUIStateChange() {
  200. if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) {
  201. if (this.$scope.history.showOnlyLabels) {
  202. this.autoSelectLabelsForComparison()
  203. } else {
  204. this.autoSelectRecentUpdates()
  205. }
  206. } else {
  207. // Point-in-time mode
  208. if (this.$scope.history.showOnlyLabels) {
  209. this.autoSelectLabelForPointInTime()
  210. } else {
  211. this.autoSelectVersionForPointInTime()
  212. }
  213. }
  214. }
  215. setHoverFrom(fromV) {
  216. const selection = this.$scope.history.selection
  217. selection.hoveredRange.fromV = fromV
  218. selection.hoveredRange.toV = selection.range.toV
  219. this.$scope.history.hoveringOverListSelectors = true
  220. }
  221. setHoverTo(toV) {
  222. const selection = this.$scope.history.selection
  223. selection.hoveredRange.toV = toV
  224. selection.hoveredRange.fromV = selection.range.fromV
  225. this.$scope.history.hoveringOverListSelectors = true
  226. }
  227. resetHover() {
  228. const selection = this.$scope.history.selection
  229. selection.hoveredRange.toV = null
  230. selection.hoveredRange.fromV = null
  231. this.$scope.history.hoveringOverListSelectors = false
  232. }
  233. showAllUpdates() {
  234. if (this.$scope.history.showOnlyLabels) {
  235. this.$scope.history.showOnlyLabels = false
  236. this._setShowOnlyLabelsUserPref(false)
  237. this._handleHistoryUIStateChange()
  238. }
  239. }
  240. showOnlyLabels() {
  241. if (!this.$scope.history.showOnlyLabels) {
  242. this.$scope.history.showOnlyLabels = true
  243. this._setShowOnlyLabelsUserPref(true)
  244. this._handleHistoryUIStateChange()
  245. }
  246. }
  247. restoreFile(version, pathname) {
  248. const url = `/project/${this.$scope.project_id}/restore_file`
  249. return this.ide.$http.post(url, {
  250. version,
  251. pathname,
  252. _csrf: window.csrfToken,
  253. })
  254. }
  255. loadFileTreeForVersion(version) {
  256. return this._loadFileTree(version, version)
  257. }
  258. loadFileTreeDiff(toV, fromV) {
  259. return this._loadFileTree(toV, fromV)
  260. }
  261. _loadFileTree(toV, fromV) {
  262. let url = `/project/${this.$scope.project_id}/filetree/diff`
  263. const selection = this.$scope.history.selection
  264. const query = [`from=${fromV}`, `to=${toV}`]
  265. url += `?${query.join('&')}`
  266. this.$scope.$applyAsync(
  267. () => (this.$scope.history.loadingFileTree = true)
  268. )
  269. selection.file = null
  270. selection.pathname = null
  271. // If `this._loadFileTreeRequestCanceller` is not null, then we have a request inflight
  272. if (this._loadFileTreeRequestCanceller != null) {
  273. // Resolving it will cancel the inflight request (or, rather, ignore its result)
  274. this._loadFileTreeRequestCanceller.resolve()
  275. }
  276. this._loadFileTreeRequestCanceller = this.ide.$q.defer()
  277. return this.ide.$http
  278. .get(url, { timeout: this._loadFileTreeRequestCanceller.promise })
  279. .then(response => {
  280. this.$scope.history.selection.files = response.data.diff
  281. for (const file of this.$scope.history.selection.files) {
  282. if (file.newPathname != null) {
  283. file.oldPathname = file.pathname
  284. file.pathname = file.newPathname
  285. delete file.newPathname
  286. }
  287. }
  288. this._loadFileTreeRequestCanceller = null
  289. this.$scope.history.loadingFileTree = false
  290. this.autoSelectFile()
  291. })
  292. .catch(err => {
  293. if (err.status !== -1) {
  294. this._loadFileTreeRequestCanceller = null
  295. } else {
  296. this.$scope.history.loadingFileTree = false
  297. }
  298. })
  299. }
  300. selectFile(file) {
  301. if (file != null && file.pathname != null) {
  302. this.$scope.history.selection.pathname =
  303. this._previouslySelectedPathname = file.pathname
  304. this.$scope.history.selection.file = file
  305. if (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME) {
  306. this.loadFileAtPointInTime()
  307. } else {
  308. this.reloadDiff()
  309. }
  310. }
  311. }
  312. autoSelectFile() {
  313. const selectedPathname = null
  314. const files = this.$scope.history.selection.files
  315. let fileToSelect = null
  316. let previouslySelectedFile = null
  317. let previouslySelectedFileHasOp = false
  318. const filesWithOps = this._getFilesWithOps()
  319. const orderedOpTypes = ['edited', 'added', 'renamed', 'removed']
  320. if (this._previouslySelectedPathname != null) {
  321. previouslySelectedFile = _.find(files, {
  322. pathname: this._previouslySelectedPathname,
  323. })
  324. previouslySelectedFileHasOp = _.some(filesWithOps, {
  325. pathname: this._previouslySelectedPathname,
  326. })
  327. }
  328. if (previouslySelectedFile != null && previouslySelectedFileHasOp) {
  329. fileToSelect = previouslySelectedFile
  330. } else {
  331. for (const opType of orderedOpTypes) {
  332. const fileWithMatchingOpType = _.find(filesWithOps, {
  333. operation: opType,
  334. })
  335. if (fileWithMatchingOpType != null) {
  336. fileToSelect = _.find(files, {
  337. pathname: fileWithMatchingOpType.pathname,
  338. })
  339. break
  340. }
  341. }
  342. if (fileToSelect == null) {
  343. if (previouslySelectedFile != null) {
  344. fileToSelect = previouslySelectedFile
  345. } else {
  346. const mainFile = _.find(files, function (file) {
  347. return /main\.tex$/.test(file.pathname)
  348. })
  349. if (mainFile != null) {
  350. fileToSelect = mainFile
  351. } else {
  352. const anyTeXFile = _.find(files, function (file) {
  353. return /\.tex$/.test(file.pathname)
  354. })
  355. if (anyTeXFile != null) {
  356. fileToSelect = anyTeXFile
  357. } else {
  358. fileToSelect = files[0]
  359. }
  360. }
  361. }
  362. }
  363. }
  364. this.selectFile(fileToSelect)
  365. }
  366. _getFilesWithOps() {
  367. let filesWithOps
  368. if (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME) {
  369. const currentUpdate = this.getUpdateForVersion(
  370. this.$scope.history.selection.range.toV
  371. )
  372. filesWithOps = []
  373. if (currentUpdate != null) {
  374. for (const pathname of currentUpdate.pathnames) {
  375. filesWithOps.push({
  376. pathname,
  377. operation: 'edited',
  378. })
  379. }
  380. for (const op of currentUpdate.project_ops) {
  381. let fileWithOp
  382. if (op.add != null) {
  383. fileWithOp = {
  384. pathname: op.add.pathname,
  385. operation: 'added',
  386. }
  387. } else if (op.remove != null) {
  388. fileWithOp = {
  389. pathname: op.remove.pathname,
  390. operation: 'removed',
  391. }
  392. } else if (op.rename != null) {
  393. fileWithOp = {
  394. pathname: op.rename.newPathname,
  395. operation: 'renamed',
  396. }
  397. }
  398. if (fileWithOp != null) {
  399. filesWithOps.push(fileWithOp)
  400. }
  401. }
  402. }
  403. } else {
  404. filesWithOps = _.reduce(
  405. this.$scope.history.selection.files,
  406. (curFilesWithOps, file) => {
  407. if (file.operation) {
  408. curFilesWithOps.push({
  409. pathname: file.pathname,
  410. operation: file.operation,
  411. })
  412. }
  413. return curFilesWithOps
  414. },
  415. []
  416. )
  417. }
  418. return filesWithOps
  419. }
  420. autoSelectRecentUpdates() {
  421. if (this.$scope.history.updates.length === 0) {
  422. return
  423. }
  424. const toV = this.$scope.history.updates[0].toV
  425. let fromV = null
  426. let indexOfLastUpdateNotByMe = 0
  427. for (let i = 0; i < this.$scope.history.updates.length; i++) {
  428. const update = this.$scope.history.updates[i]
  429. if (
  430. this._updateContainsUserId(update, this.$scope.user.id) ||
  431. i > this.MAX_RECENT_UPDATES_TO_SELECT
  432. ) {
  433. break
  434. }
  435. indexOfLastUpdateNotByMe = i
  436. }
  437. fromV = this.$scope.history.updates[indexOfLastUpdateNotByMe].fromV
  438. this.selectVersionsForCompare(toV, fromV)
  439. }
  440. autoSelectVersionForPointInTime() {
  441. if (this.$scope.history.updates.length === 0) {
  442. return
  443. }
  444. let versionToSelect = this.$scope.history.updates[0].toV
  445. const range = this.$scope.history.selection.range
  446. if (
  447. range.toV != null &&
  448. range.fromV != null &&
  449. range.toV === range.fromV
  450. ) {
  451. versionToSelect = range.toV
  452. }
  453. this.selectVersionForPointInTime(versionToSelect)
  454. }
  455. autoSelectLastLabel() {
  456. if (
  457. this.$scope.history.labels == null ||
  458. this.$scope.history.labels.length === 0
  459. ) {
  460. return
  461. }
  462. return this.selectLabelForPointInTime(this.$scope.history.labels[0])
  463. }
  464. expandSelectionToVersion(version) {
  465. if (version > this.$scope.history.selection.range.toV) {
  466. this.$scope.history.selection.range.toV = version
  467. } else if (version < this.$scope.history.selection.range.fromV) {
  468. this.$scope.history.selection.range.fromV = version
  469. }
  470. }
  471. selectVersionForPointInTime(version) {
  472. const selection = this.$scope.history.selection
  473. if (
  474. selection.range.toV !== version &&
  475. selection.range.fromV !== version
  476. ) {
  477. selection.range.toV = version
  478. selection.range.fromV = version
  479. this.loadFileTreeForVersion(version)
  480. }
  481. }
  482. selectVersionsForCompare(toV, fromV) {
  483. const range = this.$scope.history.selection.range
  484. if (range.toV !== toV || range.fromV !== fromV) {
  485. range.toV = toV
  486. range.fromV = fromV
  487. this.loadFileTreeDiff(toV, fromV)
  488. }
  489. }
  490. autoSelectLabelForPointInTime() {
  491. const selectedUpdate = this.getUpdateForVersion(
  492. this.$scope.history.selection.range.toV
  493. )
  494. let nSelectedLabels = 0
  495. if (selectedUpdate != null && selectedUpdate.labels != null) {
  496. nSelectedLabels = selectedUpdate.labels.length
  497. }
  498. // If the currently selected update has no labels, select the last one (version-wise)
  499. if (nSelectedLabels === 0) {
  500. this.autoSelectLastLabel()
  501. // If the update has one label, select it
  502. } else if (nSelectedLabels === 1) {
  503. this.selectLabelForPointInTime(selectedUpdate.labels[0])
  504. // If there are multiple labels for the update, select the latest
  505. } else if (nSelectedLabels > 1) {
  506. const sortedLabels = this.ide.$filter('orderBy')(
  507. selectedUpdate.labels,
  508. '-created_at'
  509. )
  510. const lastLabelFromUpdate = sortedLabels[0]
  511. this.selectLabelForPointInTime(lastLabelFromUpdate)
  512. }
  513. }
  514. selectLabelForPointInTime(labelToSelect) {
  515. let updateToSelect = null
  516. if (this._isLabelSelected(labelToSelect)) {
  517. // Label already selected
  518. return
  519. }
  520. for (const update of Array.from(this.$scope.history.updates)) {
  521. if (update.toV === labelToSelect.version) {
  522. updateToSelect = update
  523. break
  524. }
  525. }
  526. if (updateToSelect != null) {
  527. this.selectVersionForPointInTime(updateToSelect.toV)
  528. } else {
  529. const selection = this.$scope.history.selection
  530. selection.range.toV = labelToSelect.version
  531. selection.range.fromV = labelToSelect.version
  532. this.loadFileTreeForVersion(labelToSelect.version)
  533. }
  534. }
  535. getUpdateForVersion(version) {
  536. for (const update of this.$scope.history.updates) {
  537. if (update.toV === version) {
  538. return update
  539. }
  540. }
  541. }
  542. autoSelectLabelsForComparison() {
  543. const labels = this.$scope.history.labels
  544. let nLabels = 0
  545. if (Array.isArray(labels)) {
  546. nLabels = labels.length
  547. }
  548. if (nLabels === 0) {
  549. // TODO better handling
  550. } else if (nLabels === 1) {
  551. this.selectVersionsForCompare(labels[0].version, labels[0].version)
  552. } else if (nLabels > 1) {
  553. this.selectVersionsForCompare(labels[0].version, labels[1].version)
  554. }
  555. }
  556. fetchNextBatchOfUpdates() {
  557. if (this.$scope.history.atEnd) {
  558. return
  559. }
  560. let updatesURL = `/project/${this.ide.project_id}/updates?min_count=${this.BATCH_SIZE}`
  561. if (this.$scope.history.nextBeforeTimestamp != null) {
  562. updatesURL += `&before=${this.$scope.history.nextBeforeTimestamp}`
  563. }
  564. const labelsURL = `/project/${this.ide.project_id}/labels`
  565. const requests = { updates: this.ide.$http.get(updatesURL) }
  566. if (this.$scope.history.labels == null) {
  567. requests.labels = this.ide.$http.get(labelsURL)
  568. }
  569. this.$scope.history.loading = true
  570. return this.ide.$q
  571. .all(requests)
  572. .then(response => {
  573. this.$scope.history.loading = false
  574. const updatesData = response.updates.data
  575. let lastUpdateToV = null
  576. if (updatesData.updates.length > 0) {
  577. lastUpdateToV = updatesData.updates[0].toV
  578. }
  579. if (response.labels != null) {
  580. this.$scope.history.labels = this._loadLabels(
  581. response.labels.data,
  582. lastUpdateToV
  583. )
  584. }
  585. this._loadUpdates(updatesData.updates)
  586. this.$scope.history.nextBeforeTimestamp =
  587. updatesData.nextBeforeTimestamp
  588. if (
  589. updatesData.nextBeforeTimestamp == null ||
  590. this.$scope.history.freeHistoryLimitHit ||
  591. this.$scope.history.updates.length === 0
  592. ) {
  593. this.$scope.history.atEnd = true
  594. }
  595. if (this.$scope.history.updates.length === 0) {
  596. this.$scope.history.loadingFileTree = false
  597. }
  598. })
  599. .catch(error => {
  600. this.$scope.history.loading = false
  601. const { status, statusText } = error
  602. this.$scope.history.error = { status, statusText }
  603. this.$scope.history.atEnd = true
  604. this.$scope.history.loadingFileTree = false
  605. })
  606. }
  607. _loadLabels(labels, lastUpdateToV) {
  608. const sortedLabels = this._sortLabelsByVersionAndDate(labels)
  609. const labelsWithoutPseudoLabel =
  610. this._deletePseudoCurrentStateLabelIfExistent(sortedLabels)
  611. const labelsWithPseudoLabelIfNeeded =
  612. this._addPseudoCurrentStateLabelIfNeeded(
  613. labelsWithoutPseudoLabel,
  614. lastUpdateToV
  615. )
  616. return labelsWithPseudoLabelIfNeeded
  617. }
  618. _deletePseudoCurrentStateLabelIfExistent(labels) {
  619. if (labels.length && labels[0].isPseudoCurrentStateLabel) {
  620. labels.shift()
  621. }
  622. return labels
  623. }
  624. _addPseudoCurrentStateLabelIfNeeded(labels, mostRecentVersion) {
  625. if (
  626. (labels.length && labels[0].version !== mostRecentVersion) ||
  627. labels.length === 0
  628. ) {
  629. const pseudoCurrentStateLabel = {
  630. id: '1',
  631. isPseudoCurrentStateLabel: true,
  632. version: mostRecentVersion,
  633. created_at: new Date().toISOString(),
  634. }
  635. labels.unshift(pseudoCurrentStateLabel)
  636. }
  637. return labels
  638. }
  639. _sortLabelsByVersionAndDate(labels) {
  640. return this.ide.$filter('orderBy')(labels, [
  641. 'isPseudoCurrentStateLabel',
  642. '-version',
  643. '-created_at',
  644. ])
  645. }
  646. loadFileAtPointInTime() {
  647. const toV = this.$scope.history.selection.range.toV
  648. const { pathname } = this.$scope.history.selection
  649. if (toV == null) {
  650. return
  651. }
  652. let url = `/project/${this.$scope.project_id}/diff`
  653. const query = [
  654. `pathname=${encodeURIComponent(pathname)}`,
  655. `from=${toV}`,
  656. `to=${toV}`,
  657. ]
  658. url += `?${query.join('&')}`
  659. this.$scope.history.selection.file.loading = true
  660. return this.ide.$http
  661. .get(url)
  662. .then(response => {
  663. const { text, binary } = this._parseDiff(response.data.diff)
  664. this.$scope.history.selection.file.binary = binary
  665. this.$scope.history.selection.file.text = text
  666. this.$scope.history.selection.file.loading = false
  667. })
  668. .catch(function () {})
  669. }
  670. reloadDiff() {
  671. let { diff } = this.$scope.history.selection
  672. const { range, pathname } = this.$scope.history.selection
  673. const { fromV, toV } = range
  674. if (pathname == null) {
  675. this.$scope.history.selection.diff = null
  676. return
  677. }
  678. if (
  679. diff != null &&
  680. diff.pathname === pathname &&
  681. diff.fromV === fromV &&
  682. diff.toV === toV
  683. ) {
  684. return
  685. }
  686. this.$scope.history.selection.diff = diff = {
  687. fromV,
  688. toV,
  689. pathname,
  690. error: false,
  691. }
  692. diff.loading = true
  693. let url = `/project/${this.$scope.project_id}/diff`
  694. const query = [`pathname=${encodeURIComponent(pathname)}`]
  695. if (diff.fromV != null && diff.toV != null) {
  696. query.push(`from=${diff.fromV}`, `to=${diff.toV}`)
  697. }
  698. url += `?${query.join('&')}`
  699. return this.ide.$http
  700. .get(url)
  701. .then(response => {
  702. const { data } = response
  703. diff.loading = false
  704. const { text, highlights, binary } = this._parseDiff(data.diff)
  705. diff.binary = binary
  706. diff.text = text
  707. diff.highlights = highlights
  708. })
  709. .catch(function () {
  710. diff.loading = false
  711. diff.error = true
  712. })
  713. }
  714. labelCurrentVersion(labelComment) {
  715. return this._labelVersion(
  716. labelComment,
  717. this.$scope.history.selection.range.toV
  718. )
  719. }
  720. deleteLabel(label) {
  721. const url = `/project/${this.$scope.project_id}/labels/${label.id}`
  722. return this.ide
  723. .$http({
  724. url,
  725. method: 'DELETE',
  726. headers: {
  727. 'X-CSRF-Token': window.csrfToken,
  728. },
  729. })
  730. .then(response => {
  731. return this._deleteLabelLocally(label)
  732. })
  733. }
  734. _deleteLabelLocally(labelToDelete) {
  735. for (let i = 0; i < this.$scope.history.updates.length; i++) {
  736. const update = this.$scope.history.updates[i]
  737. if (update.toV === labelToDelete.version) {
  738. update.labels = _.filter(
  739. update.labels,
  740. label => label.id !== labelToDelete.id
  741. )
  742. break
  743. }
  744. }
  745. this.$scope.history.labels = this._loadLabels(
  746. _.filter(
  747. this.$scope.history.labels,
  748. label => label.id !== labelToDelete.id
  749. ),
  750. this.$scope.history.updates[0].toV
  751. )
  752. this._handleHistoryUIStateChange()
  753. }
  754. _isLabelSelected(label) {
  755. if (label) {
  756. return (
  757. label.version <= this.$scope.history.selection.range.toV &&
  758. label.version >= this.$scope.history.selection.range.fromV
  759. )
  760. } else {
  761. return false
  762. }
  763. }
  764. _parseDiff(diff) {
  765. if (diff.binary) {
  766. return { binary: true }
  767. }
  768. let row = 0
  769. let column = 0
  770. const highlights = []
  771. let text = ''
  772. const iterable = diff || []
  773. for (let i = 0; i < iterable.length; i++) {
  774. let endColumn, endRow
  775. const entry = iterable[i]
  776. let content = entry.u || entry.i || entry.d
  777. if (!content) {
  778. content = ''
  779. }
  780. text += content
  781. const lines = content.split('\n')
  782. const startRow = row
  783. const startColumn = column
  784. if (lines.length > 1) {
  785. endRow = startRow + lines.length - 1
  786. endColumn = lines[lines.length - 1].length
  787. } else {
  788. endRow = startRow
  789. endColumn = startColumn + lines[0].length
  790. }
  791. row = endRow
  792. column = endColumn
  793. const range = {
  794. start: {
  795. row: startRow,
  796. column: startColumn,
  797. },
  798. end: {
  799. row: endRow,
  800. column: endColumn,
  801. },
  802. }
  803. if (entry.i != null || entry.d != null) {
  804. const user =
  805. entry.meta.users != null ? entry.meta.users[0] : undefined
  806. const name = displayNameForUser(user)
  807. const date = moment(entry.meta.end_ts).format('Do MMM YYYY, h:mm a')
  808. if (entry.i != null) {
  809. highlights.push({
  810. label: `Added by ${name} on ${date}`,
  811. highlight: range,
  812. hue: ColorManager.getHueForUserId(
  813. user != null ? user.id : undefined
  814. ),
  815. })
  816. } else if (entry.d != null) {
  817. highlights.push({
  818. label: `Deleted by ${name} on ${date}`,
  819. strikeThrough: range,
  820. hue: ColorManager.getHueForUserId(
  821. user != null ? user.id : undefined
  822. ),
  823. })
  824. }
  825. }
  826. }
  827. return { text, highlights }
  828. }
  829. _loadUpdates(updates) {
  830. if (updates == null) {
  831. updates = []
  832. }
  833. let previousUpdate =
  834. this.$scope.history.updates[this.$scope.history.updates.length - 1]
  835. const dateTimeNow = new Date()
  836. const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
  837. let cutOffIndex = null
  838. const iterable = updates || []
  839. for (let i = 0; i < iterable.length; i++) {
  840. const update = iterable[i]
  841. for (const user of Array.from(update.meta.users || [])) {
  842. if (user != null) {
  843. user.hue = ColorManager.getHueForUserId(user.id)
  844. }
  845. }
  846. if (
  847. previousUpdate == null ||
  848. !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, 'day')
  849. ) {
  850. update.meta.first_in_day = true
  851. }
  852. previousUpdate = update
  853. if (
  854. !this.$scope.history.userHasFullFeature &&
  855. update.meta.end_ts < timestamp24hoursAgo
  856. ) {
  857. cutOffIndex = i || 1 // Make sure that we show at least one entry (to allow labelling).
  858. this.$scope.history.freeHistoryLimitHit = true
  859. if (this.$scope.project.owner._id === this.$scope.user.id) {
  860. ga(
  861. 'send',
  862. 'event',
  863. 'subscription-funnel',
  864. 'editor-click-feature',
  865. 'history'
  866. )
  867. paywallPrompt('history')
  868. }
  869. break
  870. }
  871. }
  872. const firstLoad = this.$scope.history.updates.length === 0
  873. if (!this.$scope.history.userHasFullFeature && cutOffIndex != null) {
  874. updates = updates.slice(0, cutOffIndex)
  875. }
  876. this.$scope.history.updates = this.$scope.history.updates.concat(updates)
  877. if (firstLoad) {
  878. this._handleHistoryUIStateChange()
  879. }
  880. }
  881. _labelVersion(comment, version) {
  882. const url = `/project/${this.$scope.project_id}/labels`
  883. return this.ide.$http
  884. .post(url, {
  885. comment,
  886. version,
  887. _csrf: window.csrfToken,
  888. })
  889. .then(response => {
  890. return this._addLabelLocally(response.data)
  891. })
  892. }
  893. _addLabelLocally(label) {
  894. const localUpdate = _.find(
  895. this.$scope.history.updates,
  896. update => update.toV === label.version
  897. )
  898. if (localUpdate != null) {
  899. localUpdate.labels = localUpdate.labels.concat(label)
  900. }
  901. this.$scope.history.labels = this._loadLabels(
  902. this.$scope.history.labels.concat(label),
  903. this.$scope.history.updates[0].toV
  904. )
  905. this._handleHistoryUIStateChange()
  906. }
  907. _updateContainsUserId(update, user_id) {
  908. for (const user of Array.from(update.meta.users)) {
  909. if ((user != null ? user.id : undefined) === user_id) {
  910. return true
  911. }
  912. }
  913. return false
  914. }
  915. }
  916. HistoryManager.initClass()
  917. return HistoryManager
  918. })()
  919. function __guard__(value, transform) {
  920. return typeof value !== 'undefined' && value !== null
  921. ? transform(value)
  922. : undefined
  923. }