HistoryV2Manager.js 30 KB

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