HistoryV2Manager.js 30 KB

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