HistoryV2Manager.js 30 KB

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