HistoryV2Manager.js 30 KB

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