HistoryV2Manager.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. /* eslint-disable
  2. camelcase,
  3. max-len,
  4. no-return-assign,
  5. no-undef,
  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. define([
  21. 'moment',
  22. 'ide/colors/ColorManager',
  23. 'ide/history/util/displayNameForUser',
  24. 'ide/history/util/HistoryViewModes',
  25. 'ide/history/controllers/HistoryV2ListController',
  26. 'ide/history/controllers/HistoryV2FileTreeController',
  27. 'ide/history/controllers/HistoryV2ToolbarController',
  28. 'ide/history/controllers/HistoryV2AddLabelModalController',
  29. 'ide/history/controllers/HistoryV2DeleteLabelModalController',
  30. 'ide/history/directives/infiniteScroll',
  31. 'ide/history/directives/historyDraggableBoundary',
  32. 'ide/history/directives/historyDroppableArea',
  33. 'ide/history/components/historyEntriesList',
  34. 'ide/history/components/historyEntry',
  35. 'ide/history/components/historyLabelsList',
  36. 'ide/history/components/historyLabel',
  37. 'ide/history/components/historyFileTree',
  38. 'ide/history/components/historyFileEntity'
  39. ], function(moment, ColorManager, displayNameForUser, HistoryViewModes) {
  40. let HistoryManager
  41. return (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.${
  56. $scope.project_id
  57. }`
  58. this._localStorageShowOnlyLabelsProjKey = `history.userPrefs.showOnlyLabels.${
  59. $scope.project_id
  60. }`
  61. this._previouslySelectedPathname = null
  62. this._loadFileTreeRequestCanceller = null
  63. this.hardReset()
  64. this.$scope.toggleHistory = () => {
  65. if (this.$scope.ui.view === 'history') {
  66. this.hide()
  67. } else {
  68. this.show()
  69. this._handleHistoryUIStateChange()
  70. }
  71. this.ide.$timeout(() => {
  72. this.$scope.$broadcast('history:toggle')
  73. }, 0)
  74. }
  75. this.$scope.isHistoryLoading = () => {
  76. let selection = this.$scope.history.selection
  77. return (
  78. this.$scope.history.loadingFileTree ||
  79. (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME &&
  80. selection.file &&
  81. selection.file.loading) ||
  82. (this.$scope.history.viewMode === HistoryViewModes.COMPARE &&
  83. selection.diff &&
  84. selection.diff.loading)
  85. )
  86. }
  87. }
  88. show() {
  89. this.$scope.ui.view = 'history'
  90. this.hardReset()
  91. if (this.$scope.history.showOnlyLabels) {
  92. this.fetchNextBatchOfUpdates()
  93. }
  94. }
  95. hide() {
  96. this.$scope.ui.view = 'editor'
  97. }
  98. _getViewModeUserPref() {
  99. return (
  100. this.localStorage(this._localStorageViewModeProjKey) ||
  101. HistoryViewModes.POINT_IN_TIME
  102. )
  103. }
  104. _getShowOnlyLabelsUserPref() {
  105. return (
  106. this.localStorage(this._localStorageShowOnlyLabelsProjKey) || false
  107. )
  108. }
  109. _setViewModeUserPref(viewModeUserPref) {
  110. if (
  111. viewModeUserPref === HistoryViewModes.POINT_IN_TIME ||
  112. viewModeUserPref === HistoryViewModes.COMPARE
  113. ) {
  114. this.localStorage(this._localStorageViewModeProjKey, viewModeUserPref)
  115. }
  116. }
  117. _setShowOnlyLabelsUserPref(showOnlyLabelsUserPref) {
  118. this.localStorage(
  119. this._localStorageShowOnlyLabelsProjKey,
  120. !!showOnlyLabelsUserPref
  121. )
  122. }
  123. hardReset() {
  124. this.$scope.history = {
  125. isV2: true,
  126. updates: [],
  127. viewMode: this._getViewModeUserPref(),
  128. nextBeforeTimestamp: null,
  129. atEnd: false,
  130. userHasFullFeature: undefined,
  131. freeHistoryLimitHit: false,
  132. selection: {
  133. docs: {},
  134. pathname: null,
  135. range: {
  136. fromV: null,
  137. toV: null
  138. },
  139. hoveredRange: {
  140. fromV: null,
  141. toV: null
  142. },
  143. diff: null,
  144. files: [],
  145. file: null
  146. },
  147. error: null,
  148. showOnlyLabels: this._getShowOnlyLabelsUserPref(),
  149. labels: null,
  150. loadingFileTree: true
  151. }
  152. let _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. this.$scope.history.loadingFileTree = true
  185. }
  186. toggleHistoryViewMode() {
  187. if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) {
  188. this.softReset()
  189. this.$scope.history.viewMode = HistoryViewModes.POINT_IN_TIME
  190. this._setViewModeUserPref(HistoryViewModes.POINT_IN_TIME)
  191. } else {
  192. this.softReset()
  193. this.$scope.history.viewMode = HistoryViewModes.COMPARE
  194. this._setViewModeUserPref(HistoryViewModes.COMPARE)
  195. }
  196. this._handleHistoryUIStateChange()
  197. this.ide.$timeout(() => {
  198. this.$scope.$broadcast('history:toggle')
  199. }, 0)
  200. }
  201. _handleHistoryUIStateChange() {
  202. if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) {
  203. if (this.$scope.history.showOnlyLabels) {
  204. this.autoSelectLabelsForComparison()
  205. } else {
  206. this.autoSelectRecentUpdates()
  207. }
  208. } else {
  209. // Point-in-time mode
  210. if (this.$scope.history.showOnlyLabels) {
  211. this.autoSelectLabelForPointInTime()
  212. } else {
  213. this.autoSelectVersionForPointInTime()
  214. }
  215. }
  216. }
  217. setHoverFrom(fromV) {
  218. let selection = this.$scope.history.selection
  219. selection.hoveredRange.fromV = fromV
  220. selection.hoveredRange.toV = selection.range.toV
  221. this.$scope.history.hoveringOverListSelectors = true
  222. }
  223. setHoverTo(toV) {
  224. let selection = this.$scope.history.selection
  225. selection.hoveredRange.toV = toV
  226. selection.hoveredRange.fromV = selection.range.fromV
  227. this.$scope.history.hoveringOverListSelectors = true
  228. }
  229. resetHover() {
  230. let selection = this.$scope.history.selection
  231. selection.hoveredRange.toV = null
  232. selection.hoveredRange.fromV = null
  233. this.$scope.history.hoveringOverListSelectors = false
  234. }
  235. showAllUpdates() {
  236. if (this.$scope.history.showOnlyLabels) {
  237. this.$scope.history.showOnlyLabels = false
  238. this._setShowOnlyLabelsUserPref(false)
  239. this._handleHistoryUIStateChange()
  240. }
  241. }
  242. showOnlyLabels() {
  243. if (!this.$scope.history.showOnlyLabels) {
  244. this.$scope.history.showOnlyLabels = true
  245. this._setShowOnlyLabelsUserPref(true)
  246. this._handleHistoryUIStateChange()
  247. }
  248. }
  249. restoreFile(version, pathname) {
  250. const url = `/project/${this.$scope.project_id}/restore_file`
  251. return this.ide.$http.post(url, {
  252. version,
  253. pathname,
  254. _csrf: window.csrfToken
  255. })
  256. }
  257. loadFileTreeForVersion(version) {
  258. return this._loadFileTree(version, version)
  259. }
  260. loadFileTreeDiff(toV, fromV) {
  261. return this._loadFileTree(toV, fromV)
  262. }
  263. _loadFileTree(toV, fromV) {
  264. let url = `/project/${this.$scope.project_id}/filetree/diff`
  265. let selection = this.$scope.history.selection
  266. const query = [`from=${fromV}`, `to=${toV}`]
  267. url += `?${query.join('&')}`
  268. this.$scope.$applyAsync(
  269. () => (this.$scope.history.loadingFileTree = true)
  270. )
  271. selection.file = null
  272. selection.pathname = null
  273. // If `this._loadFileTreeRequestCanceller` is not null, then we have a request inflight
  274. if (this._loadFileTreeRequestCanceller != null) {
  275. // Resolving it will cancel the inflight request (or, rather, ignore its result)
  276. this._loadFileTreeRequestCanceller.resolve()
  277. }
  278. this._loadFileTreeRequestCanceller = this.ide.$q.defer()
  279. return this.ide.$http
  280. .get(url, { timeout: this._loadFileTreeRequestCanceller.promise })
  281. .then(response => {
  282. this.$scope.history.selection.files = response.data.diff
  283. for (let file of this.$scope.history.selection.files) {
  284. if (file.newPathname != null) {
  285. file.oldPathname = file.pathname
  286. file.pathname = file.newPathname
  287. delete file.newPathname
  288. }
  289. }
  290. this._loadFileTreeRequestCanceller = null
  291. this.$scope.history.loadingFileTree = false
  292. this.autoSelectFile()
  293. })
  294. .catch(err => {
  295. if (err.status !== -1) {
  296. this._loadFileTreeRequestCanceller = null
  297. } else {
  298. this.$scope.history.loadingFileTree = false
  299. }
  300. })
  301. }
  302. selectFile(file) {
  303. if (file != null && file.pathname != null) {
  304. this.$scope.history.selection.pathname = this._previouslySelectedPathname =
  305. file.pathname
  306. this.$scope.history.selection.file = file
  307. if (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME) {
  308. this.loadFileAtPointInTime()
  309. } else {
  310. this.reloadDiff()
  311. }
  312. }
  313. }
  314. autoSelectFile() {
  315. let selectedPathname = null
  316. let files = this.$scope.history.selection.files
  317. let fileToSelect = null
  318. let previouslySelectedFile = null
  319. let previouslySelectedFileHasOp = false
  320. let filesWithOps = this._getFilesWithOps()
  321. const orderedOpTypes = ['edited', 'added', 'renamed', 'removed']
  322. if (this._previouslySelectedPathname != null) {
  323. previouslySelectedFile = _.find(files, {
  324. pathname: this._previouslySelectedPathname
  325. })
  326. previouslySelectedFileHasOp = _.some(filesWithOps, {
  327. pathname: this._previouslySelectedPathname
  328. })
  329. }
  330. if (previouslySelectedFile != null && previouslySelectedFileHasOp) {
  331. fileToSelect = previouslySelectedFile
  332. } else {
  333. for (let opType of orderedOpTypes) {
  334. let fileWithMatchingOpType = _.find(filesWithOps, {
  335. operation: opType
  336. })
  337. if (fileWithMatchingOpType != null) {
  338. fileToSelect = _.find(files, {
  339. pathname: fileWithMatchingOpType.pathname
  340. })
  341. break
  342. }
  343. }
  344. if (fileToSelect == null) {
  345. if (previouslySelectedFile != null) {
  346. fileToSelect = previouslySelectedFile
  347. } else {
  348. let mainFile = _.find(files, function(file) {
  349. return /main\.tex$/.test(file.pathname)
  350. })
  351. if (mainFile != null) {
  352. fileToSelect = mainFile
  353. } else {
  354. let anyTeXFile = _.find(files, function(file) {
  355. return /\.tex$/.test(file.pathname)
  356. })
  357. if (anyTeXFile != null) {
  358. fileToSelect = anyTeXFile
  359. } else {
  360. fileToSelect = files[0]
  361. }
  362. }
  363. }
  364. }
  365. }
  366. this.selectFile(fileToSelect)
  367. }
  368. _getFilesWithOps() {
  369. let filesWithOps
  370. if (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME) {
  371. let currentUpdate = this.getUpdateForVersion(
  372. this.$scope.history.selection.range.toV
  373. )
  374. filesWithOps = []
  375. if (currentUpdate != null) {
  376. for (pathname of currentUpdate.pathnames) {
  377. filesWithOps.push({
  378. pathname: pathname,
  379. operation: 'edited'
  380. })
  381. }
  382. for (op of currentUpdate.project_ops) {
  383. let fileWithOp
  384. if (op.add != null) {
  385. fileWithOp = {
  386. pathname: op.add.pathname,
  387. operation: 'added'
  388. }
  389. } else if (op.remove != null) {
  390. fileWithOp = {
  391. pathname: op.remove.pathname,
  392. operation: 'removed'
  393. }
  394. } else if (op.rename != null) {
  395. fileWithOp = {
  396. pathname: op.rename.newPathname,
  397. operation: 'renamed'
  398. }
  399. }
  400. if (fileWithOp != null) {
  401. filesWithOps.push(fileWithOp)
  402. }
  403. }
  404. }
  405. } else {
  406. filesWithOps = _.reduce(
  407. this.$scope.history.selection.files,
  408. (curFilesWithOps, file) => {
  409. if (file.operation) {
  410. curFilesWithOps.push({
  411. pathname: file.pathname,
  412. operation: file.operation
  413. })
  414. }
  415. return curFilesWithOps
  416. },
  417. []
  418. )
  419. }
  420. return filesWithOps
  421. }
  422. autoSelectRecentUpdates() {
  423. if (this.$scope.history.updates.length === 0) {
  424. return
  425. }
  426. let toV = this.$scope.history.updates[0].toV
  427. let fromV = null
  428. let indexOfLastUpdateNotByMe = 0
  429. for (let i = 0; i < this.$scope.history.updates.length; i++) {
  430. const update = this.$scope.history.updates[i]
  431. if (
  432. this._updateContainsUserId(update, this.$scope.user.id) ||
  433. i > this.MAX_RECENT_UPDATES_TO_SELECT
  434. ) {
  435. break
  436. }
  437. indexOfLastUpdateNotByMe = i
  438. }
  439. fromV = this.$scope.history.updates[indexOfLastUpdateNotByMe].fromV
  440. this.selectVersionsForCompare(toV, fromV)
  441. }
  442. autoSelectVersionForPointInTime() {
  443. if (this.$scope.history.updates.length === 0) {
  444. return
  445. }
  446. let versionToSelect = this.$scope.history.updates[0].toV
  447. let range = this.$scope.history.selection.range
  448. if (
  449. range.toV != null &&
  450. range.fromV != null &&
  451. range.toV === range.fromV
  452. ) {
  453. versionToSelect = range.toV
  454. }
  455. this.selectVersionForPointInTime(versionToSelect)
  456. }
  457. autoSelectLastLabel() {
  458. if (
  459. this.$scope.history.labels == null ||
  460. this.$scope.history.labels.length === 0
  461. ) {
  462. return
  463. }
  464. return this.selectLabelForPointInTime(this.$scope.history.labels[0])
  465. }
  466. expandSelectionToVersion(version) {
  467. if (version > this.$scope.history.selection.range.toV) {
  468. this.$scope.history.selection.range.toV = version
  469. } else if (version < this.$scope.history.selection.range.fromV) {
  470. this.$scope.history.selection.range.fromV = version
  471. }
  472. }
  473. selectVersionForPointInTime(version) {
  474. let selection = this.$scope.history.selection
  475. if (
  476. selection.range.toV !== version &&
  477. selection.range.fromV !== version
  478. ) {
  479. selection.range.toV = version
  480. selection.range.fromV = version
  481. this.loadFileTreeForVersion(version)
  482. }
  483. }
  484. selectVersionsForCompare(toV, fromV) {
  485. let range = this.$scope.history.selection.range
  486. if (range.toV !== toV || range.fromV !== fromV) {
  487. range.toV = toV
  488. range.fromV = fromV
  489. this.loadFileTreeDiff(toV, fromV)
  490. }
  491. }
  492. autoSelectLabelForPointInTime() {
  493. const selectedUpdate = this.getUpdateForVersion(
  494. this.$scope.history.selection.range.toV
  495. )
  496. let nSelectedLabels = 0
  497. if (selectedUpdate != null && selectedUpdate.labels != null) {
  498. nSelectedLabels = selectedUpdate.labels.length
  499. }
  500. // If the currently selected update has no labels, select the last one (version-wise)
  501. if (nSelectedLabels === 0) {
  502. this.autoSelectLastLabel()
  503. // If the update has one label, select it
  504. } else if (nSelectedLabels === 1) {
  505. this.selectLabelForPointInTime(selectedUpdate.labels[0])
  506. // If there are multiple labels for the update, select the latest
  507. } else if (nSelectedLabels > 1) {
  508. const sortedLabels = this.ide.$filter('orderBy')(
  509. selectedUpdate.labels,
  510. '-created_at'
  511. )
  512. const lastLabelFromUpdate = sortedLabels[0]
  513. this.selectLabelForPointInTime(lastLabelFromUpdate)
  514. }
  515. }
  516. selectLabelForPointInTime(labelToSelect) {
  517. let updateToSelect = null
  518. if (this._isLabelSelected(labelToSelect)) {
  519. // Label already selected
  520. return
  521. }
  522. for (let update of Array.from(this.$scope.history.updates)) {
  523. if (update.toV === labelToSelect.version) {
  524. updateToSelect = update
  525. break
  526. }
  527. }
  528. if (updateToSelect != null) {
  529. this.selectVersionForPointInTime(updateToSelect.toV)
  530. } else {
  531. let selection = this.$scope.history.selection
  532. selection.range.toV = labelToSelect.version
  533. selection.range.fromV = labelToSelect.version
  534. this.loadFileTreeForVersion(labelToSelect.version)
  535. }
  536. }
  537. getUpdateForVersion(version) {
  538. for (let update of this.$scope.history.updates) {
  539. if (update.toV === version) {
  540. return update
  541. }
  542. }
  543. }
  544. autoSelectLabelsForComparison() {
  545. let labels = this.$scope.history.labels
  546. let nLabels = 0
  547. if (Array.isArray(labels)) {
  548. nLabels = labels.length
  549. }
  550. if (nLabels === 0) {
  551. // TODO better handling
  552. } else if (nLabels === 1) {
  553. this.selectVersionsForCompare(labels[0].version, labels[0].version)
  554. } else if (nLabels > 1) {
  555. this.selectVersionsForCompare(labels[0].version, labels[1].version)
  556. }
  557. }
  558. fetchNextBatchOfUpdates() {
  559. if (this.$scope.history.atEnd) {
  560. return
  561. }
  562. let updatesURL = `/project/${this.ide.project_id}/updates?min_count=${
  563. this.BATCH_SIZE
  564. }`
  565. if (this.$scope.history.nextBeforeTimestamp != null) {
  566. updatesURL += `&before=${this.$scope.history.nextBeforeTimestamp}`
  567. }
  568. const labelsURL = `/project/${this.ide.project_id}/labels`
  569. const requests = { updates: this.ide.$http.get(updatesURL) }
  570. if (this.$scope.history.labels == null) {
  571. requests.labels = this.ide.$http.get(labelsURL)
  572. }
  573. return this.ide.$q
  574. .all(requests)
  575. .then(response => {
  576. const updatesData = response.updates.data
  577. let lastUpdateToV = null
  578. if (updatesData.updates.length > 0) {
  579. lastUpdateToV = updatesData.updates[0].toV
  580. }
  581. if (response.labels != null) {
  582. this.$scope.history.labels = this._loadLabels(
  583. response.labels.data,
  584. lastUpdateToV
  585. )
  586. }
  587. this._loadUpdates(updatesData.updates)
  588. this.$scope.history.nextBeforeTimestamp =
  589. updatesData.nextBeforeTimestamp
  590. if (
  591. updatesData.nextBeforeTimestamp == null ||
  592. this.$scope.history.freeHistoryLimitHit ||
  593. this.$scope.history.updates.length === 0
  594. ) {
  595. this.$scope.history.atEnd = true
  596. }
  597. if (this.$scope.history.updates.length === 0) {
  598. this.$scope.history.loadingFileTree = false
  599. }
  600. })
  601. .catch(error => {
  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. let sortedLabels = this._sortLabelsByVersionAndDate(labels)
  610. let nLabels = sortedLabels.length
  611. let hasPseudoCurrentStateLabel = false
  612. let needsPseudoCurrentStateLabel = false
  613. if (lastUpdateToV) {
  614. hasPseudoCurrentStateLabel =
  615. nLabels > 0 ? sortedLabels[0].isPseudoCurrentStateLabel : false
  616. if (hasPseudoCurrentStateLabel) {
  617. needsPseudoCurrentStateLabel =
  618. nLabels > 1 ? sortedLabels[1].version !== lastUpdateToV : false
  619. } else {
  620. needsPseudoCurrentStateLabel =
  621. nLabels > 0 ? sortedLabels[0].version !== lastUpdateToV : true
  622. }
  623. if (needsPseudoCurrentStateLabel && !hasPseudoCurrentStateLabel) {
  624. let pseudoCurrentStateLabel = {
  625. id: '1',
  626. isPseudoCurrentStateLabel: true,
  627. version: lastUpdateToV,
  628. created_at: new Date().toISOString()
  629. }
  630. sortedLabels.unshift(pseudoCurrentStateLabel)
  631. } else if (
  632. !needsPseudoCurrentStateLabel &&
  633. hasPseudoCurrentStateLabel
  634. ) {
  635. sortedLabels.shift()
  636. }
  637. }
  638. return sortedLabels
  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. var 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 = this.$scope.history.updates[
  835. this.$scope.history.updates.length - 1
  836. ]
  837. const dateTimeNow = new Date()
  838. const timestamp24hoursAgo = dateTimeNow.setDate(
  839. dateTimeNow.getDate() - 1
  840. )
  841. let cutOffIndex = null
  842. const iterable = updates || []
  843. for (let i = 0; i < iterable.length; i++) {
  844. const update = iterable[i]
  845. for (let user of Array.from(update.meta.users || [])) {
  846. if (user != null) {
  847. user.hue = ColorManager.getHueForUserId(user.id)
  848. }
  849. }
  850. if (
  851. previousUpdate == null ||
  852. !moment(previousUpdate.meta.end_ts).isSame(
  853. update.meta.end_ts,
  854. 'day'
  855. )
  856. ) {
  857. update.meta.first_in_day = true
  858. }
  859. previousUpdate = update
  860. if (
  861. !this.$scope.history.userHasFullFeature &&
  862. update.meta.end_ts < timestamp24hoursAgo
  863. ) {
  864. cutOffIndex = i || 1 // Make sure that we show at least one entry (to allow labelling).
  865. this.$scope.history.freeHistoryLimitHit = true
  866. if (this.$scope.project.owner._id === this.$scope.user.id) {
  867. ga(
  868. 'send',
  869. 'event',
  870. 'subscription-funnel',
  871. 'editor-click-feature',
  872. 'history'
  873. )
  874. }
  875. break
  876. }
  877. }
  878. const firstLoad = this.$scope.history.updates.length === 0
  879. if (!this.$scope.history.userHasFullFeature && cutOffIndex != null) {
  880. updates = updates.slice(0, cutOffIndex)
  881. }
  882. this.$scope.history.updates = this.$scope.history.updates.concat(
  883. updates
  884. )
  885. if (firstLoad) {
  886. this._handleHistoryUIStateChange()
  887. }
  888. }
  889. _labelVersion(comment, version) {
  890. const url = `/project/${this.$scope.project_id}/labels`
  891. return this.ide.$http
  892. .post(url, {
  893. comment,
  894. version,
  895. _csrf: window.csrfToken
  896. })
  897. .then(response => {
  898. return this._addLabelLocally(response.data)
  899. })
  900. }
  901. _addLabelLocally(label) {
  902. const localUpdate = _.find(
  903. this.$scope.history.updates,
  904. update => update.toV === label.version
  905. )
  906. if (localUpdate != null) {
  907. localUpdate.labels = localUpdate.labels.concat(label)
  908. }
  909. this.$scope.history.labels = this._loadLabels(
  910. this.$scope.history.labels.concat(label),
  911. this.$scope.history.updates[0].toV
  912. )
  913. this._handleHistoryUIStateChange()
  914. }
  915. _updateContainsUserId(update, user_id) {
  916. for (let user of Array.from(update.meta.users)) {
  917. if ((user != null ? user.id : undefined) === user_id) {
  918. return true
  919. }
  920. }
  921. return false
  922. }
  923. }
  924. HistoryManager.initClass()
  925. return HistoryManager
  926. })())
  927. })
  928. function __guard__(value, transform) {
  929. return typeof value !== 'undefined' && value !== null
  930. ? transform(value)
  931. : undefined
  932. }