FileTreeController.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. import _ from 'lodash'
  2. /* eslint-disable
  3. camelcase,
  4. node/handle-callback-err,
  5. max-len,
  6. no-return-assign,
  7. no-unused-vars,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS103: Rewrite code to no longer use __guard__
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. import App from '../../../base'
  19. App.controller(
  20. 'FileTreeController',
  21. function ($scope, $modal, ide, $rootScope) {
  22. $scope.openNewDocModal = reactBridgeParentFolderId =>
  23. $modal.open({
  24. templateUrl: 'newFileModalTemplate',
  25. controller: 'NewFileModalController',
  26. size: 'lg',
  27. resolve: {
  28. parent_folder() {
  29. if (reactBridgeParentFolderId) {
  30. return { id: reactBridgeParentFolderId }
  31. }
  32. return ide.fileTreeManager.getCurrentFolder()
  33. },
  34. projectFeatures() {
  35. return ide.$scope.project.features
  36. },
  37. type() {
  38. return 'doc'
  39. },
  40. userFeatures() {
  41. return ide.$scope.user.features
  42. },
  43. },
  44. })
  45. $scope.openNewFolderModal = () =>
  46. $modal.open({
  47. templateUrl: 'newFolderModalTemplate',
  48. controller: 'NewFolderModalController',
  49. resolve: {
  50. parent_folder() {
  51. return ide.fileTreeManager.getCurrentFolder()
  52. },
  53. },
  54. })
  55. $scope.openUploadFileModal = reactBridgeParentFolderId =>
  56. $modal.open({
  57. templateUrl: 'newFileModalTemplate',
  58. controller: 'NewFileModalController',
  59. size: 'lg',
  60. resolve: {
  61. projectFeatures() {
  62. return ide.$scope.project.features
  63. },
  64. parent_folder() {
  65. if (reactBridgeParentFolderId) {
  66. return { id: reactBridgeParentFolderId }
  67. }
  68. return ide.fileTreeManager.getCurrentFolder()
  69. },
  70. type() {
  71. return 'upload'
  72. },
  73. userFeatures() {
  74. return ide.$scope.user.features
  75. },
  76. },
  77. })
  78. if (
  79. window.showReactFileTree &&
  80. !window.FileTreeReactBridgeOpenNewDocModalListened
  81. ) {
  82. window.FileTreeReactBridgeOpenNewDocModalListened = true
  83. window.addEventListener(
  84. 'FileTreeReactBridge.openNewDocModal',
  85. ({ detail }) => {
  86. if (detail.mode === 'upload') {
  87. $scope.openUploadFileModal(detail.parentFolderId)
  88. } else {
  89. $scope.openNewDocModal(detail.parentFolderId)
  90. }
  91. }
  92. )
  93. }
  94. $scope.orderByFoldersFirst = function (entity) {
  95. if ((entity != null ? entity.type : undefined) === 'folder') {
  96. return '0'
  97. }
  98. return '1'
  99. }
  100. $scope.startRenamingSelected = () => $scope.$broadcast('rename:selected')
  101. return ($scope.openDeleteModalForSelected = () =>
  102. $scope.$broadcast('delete:selected'))
  103. }
  104. )
  105. App.controller(
  106. 'NewFolderModalController',
  107. function ($scope, ide, $modalInstance, $timeout, parent_folder) {
  108. $scope.inputs = { name: 'name' }
  109. $scope.state = { inflight: false }
  110. $modalInstance.opened.then(() =>
  111. $timeout(() => $scope.$broadcast('open'), 200)
  112. )
  113. $scope.create = function () {
  114. const { name } = $scope.inputs
  115. if (name == null || name.length === 0) {
  116. return
  117. }
  118. $scope.state.inflight = true
  119. return ide.fileTreeManager
  120. .createFolder(name, $scope.parent_folder)
  121. .then(function () {
  122. $scope.state.inflight = false
  123. return $modalInstance.dismiss('done')
  124. })
  125. .catch(function (response) {
  126. const { data } = response
  127. $scope.error = data
  128. return ($scope.state.inflight = false)
  129. })
  130. }
  131. return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
  132. }
  133. )
  134. App.controller(
  135. 'DuplicateFileModalController',
  136. function ($scope, $modalInstance, fileName) {
  137. $scope.fileName = fileName
  138. $scope.cancel = () => $modalInstance.dismiss('cancel')
  139. }
  140. )
  141. App.controller(
  142. 'NewFileModalController',
  143. function (
  144. $scope,
  145. ide,
  146. type,
  147. parent_folder,
  148. $modalInstance,
  149. eventTracking,
  150. projectFeatures,
  151. userFeatures
  152. ) {
  153. $scope.file_count = ide.fileTreeManager.getFullCount()
  154. $scope.type = type
  155. $scope.parent_folder = parent_folder
  156. $scope.state = {
  157. inflight: false,
  158. valid: true,
  159. }
  160. $scope.cancel = () => $modalInstance.dismiss('cancel')
  161. $scope.create = () => $scope.$broadcast('create')
  162. const hasMendeleyFeature =
  163. (projectFeatures && projectFeatures.references) ||
  164. (projectFeatures && projectFeatures.mendeley) ||
  165. (userFeatures && userFeatures.references) ||
  166. (userFeatures && userFeatures.mendeley)
  167. const hasZoteroFeature =
  168. (projectFeatures && projectFeatures.references) ||
  169. (projectFeatures && projectFeatures.zotero) ||
  170. (userFeatures && userFeatures.references) ||
  171. (userFeatures && userFeatures.zotero)
  172. $scope.$watch('type', function () {
  173. if ($scope.type === 'mendeley' && !hasMendeleyFeature) {
  174. eventTracking.send(
  175. 'subscription-funnel',
  176. 'editor-click-feature',
  177. $scope.type
  178. )
  179. }
  180. if ($scope.type === 'zotero' && !hasZoteroFeature) {
  181. eventTracking.send(
  182. 'subscription-funnel',
  183. 'editor-click-feature',
  184. $scope.type
  185. )
  186. }
  187. })
  188. $scope.$on('done', (e, opts = {}) => {
  189. const isBibFile = opts.name && /^.*\.bib$/.test(opts.name)
  190. if (opts.shouldReindexReferences || isBibFile) {
  191. ide.$scope.$emit('references:should-reindex', {})
  192. }
  193. $modalInstance.dismiss('done')
  194. })
  195. if (
  196. window.showReactFileTree &&
  197. !window.FileTreeReactBridgeOpenNewFileModalListened
  198. ) {
  199. window.FileTreeReactBridgeOpenNewFileModalListened = true
  200. window.addEventListener(
  201. 'FileTreeReactBridge.openNewFileModal',
  202. ({ detail }) => {
  203. if (detail.done) {
  204. ide.$scope.FileTreeReactBridgePromise.resolve()
  205. }
  206. if (detail.error) {
  207. ide.$scope.FileTreeReactBridgePromise.reject(detail)
  208. }
  209. }
  210. )
  211. }
  212. }
  213. )
  214. App.controller('NewDocModalController', function ($scope, ide, $timeout) {
  215. $scope.inputs = { name: 'name.tex' }
  216. $timeout(() => $scope.$broadcast('open'), 200)
  217. return $scope.$on('create', function () {
  218. const { name } = $scope.inputs
  219. if (name == null || name.length === 0) {
  220. return
  221. }
  222. $scope.state.inflight = true
  223. return ide.fileTreeManager
  224. .createDoc(name, $scope.parent_folder)
  225. .then(function () {
  226. $scope.state.inflight = false
  227. return $scope.$emit('done')
  228. })
  229. .catch(function (response) {
  230. const { data } = response
  231. $scope.error = data
  232. $scope.state.inflight = false
  233. })
  234. .finally(function () {
  235. if (!$scope.$$phase) {
  236. $scope.$apply()
  237. }
  238. })
  239. })
  240. })
  241. App.controller(
  242. 'UploadFileModalController',
  243. function ($scope, $rootScope, ide, $timeout, $window) {
  244. $scope.parent_folder_id =
  245. $scope.parent_folder != null ? $scope.parent_folder.id : undefined
  246. $scope.project_id = ide.project_id
  247. $scope.tooManyFiles = false
  248. $scope.rateLimitHit = false
  249. $scope.secondsToRedirect = 10
  250. $scope.notLoggedIn = false
  251. $scope.conflicts = []
  252. $scope.control = {}
  253. const needToLogBackIn = function () {
  254. $scope.notLoggedIn = true
  255. var decreseTimeout = () =>
  256. $timeout(function () {
  257. if ($scope.secondsToRedirect === 0) {
  258. return ($window.location.href = `/login?redir=/project/${ide.project_id}`)
  259. } else {
  260. decreseTimeout()
  261. return ($scope.secondsToRedirect = $scope.secondsToRedirect - 1)
  262. }
  263. }, 1000)
  264. return decreseTimeout()
  265. }
  266. $scope.max_files = 40
  267. $scope.onComplete = (error, name, response) =>
  268. $timeout(function () {
  269. uploadCount--
  270. if (response.success) {
  271. $rootScope.$broadcast('file:upload:complete', response)
  272. }
  273. if (uploadCount === 0 && response != null && response.success) {
  274. return $scope.$emit('done', { name: name })
  275. }
  276. }, 250)
  277. $scope.onValidateBatch = function (files) {
  278. if (files.length > $scope.max_files) {
  279. $timeout(() => ($scope.tooManyFiles = true), 1)
  280. return false
  281. } else {
  282. return true
  283. }
  284. }
  285. $scope.onError = function (id, name, reason) {
  286. console.log(id, name, reason)
  287. if (reason.indexOf('429') !== -1) {
  288. return ($scope.rateLimitHit = true)
  289. } else if (reason.indexOf('403') !== -1) {
  290. return needToLogBackIn()
  291. }
  292. }
  293. let _uploadTimer = null
  294. const uploadIfNoConflicts = function () {
  295. if ($scope.conflicts.length === 0) {
  296. return $scope.doUpload()
  297. }
  298. }
  299. var uploadCount = 0
  300. $scope.onSubmit = function (id, name) {
  301. uploadCount++
  302. if (ide.fileTreeManager.existsInFolder($scope.parent_folder_id, name)) {
  303. $scope.conflicts.push(name)
  304. $scope.$apply()
  305. }
  306. if (_uploadTimer == null) {
  307. _uploadTimer = setTimeout(function () {
  308. _uploadTimer = null
  309. return uploadIfNoConflicts()
  310. }, 0)
  311. }
  312. return true
  313. }
  314. $scope.onCancel = function (id, name) {
  315. uploadCount--
  316. const index = $scope.conflicts.indexOf(name)
  317. if (index > -1) {
  318. $scope.conflicts.splice(index, 1)
  319. }
  320. $scope.$apply()
  321. return uploadIfNoConflicts()
  322. }
  323. return ($scope.doUpload = () =>
  324. __guard__($scope.control != null ? $scope.control.q : undefined, x =>
  325. x.uploadStoredFiles()
  326. ))
  327. }
  328. )
  329. App.controller(
  330. 'ProjectLinkedFileModalController',
  331. function ($scope, ide, $timeout) {
  332. $scope.data = {
  333. projects: null, // or []
  334. selectedProjectId: null,
  335. projectEntities: null, // or []
  336. projectOutputFiles: null, // or []
  337. selectedProjectEntity: null,
  338. selectedProjectOutputFile: null,
  339. buildId: null,
  340. name: null,
  341. }
  342. $scope.state.inFlight = {
  343. projects: false,
  344. entities: false,
  345. compile: false,
  346. }
  347. $scope.state.isOutputFilesMode = false
  348. $scope.state.error = false
  349. $scope.$watch('data.selectedProjectId', function (newVal, oldVal) {
  350. if (!newVal) {
  351. return
  352. }
  353. $scope.data.selectedProjectEntity = null
  354. $scope.data.selectedProjectOutputFile = null
  355. if ($scope.state.isOutputFilesMode) {
  356. return $scope.compileProjectAndGetOutputFiles(
  357. $scope.data.selectedProjectId
  358. )
  359. } else {
  360. return $scope.getProjectEntities($scope.data.selectedProjectId)
  361. }
  362. })
  363. $scope.$watch('state.isOutputFilesMode', function (newVal, oldVal) {
  364. if (!newVal && !oldVal) {
  365. return
  366. }
  367. $scope.data.selectedProjectOutputFile = null
  368. if (newVal === true) {
  369. return $scope.compileProjectAndGetOutputFiles(
  370. $scope.data.selectedProjectId
  371. )
  372. } else {
  373. return $scope.getProjectEntities($scope.data.selectedProjectId)
  374. }
  375. })
  376. // auto-set filename based on selected file
  377. $scope.$watch('data.selectedProjectEntity', function (newVal, oldVal) {
  378. if (!newVal) {
  379. return
  380. }
  381. const fileName = newVal.split('/').reverse()[0]
  382. if (fileName) {
  383. $scope.data.name = fileName
  384. }
  385. })
  386. // auto-set filename based on selected file
  387. $scope.$watch('data.selectedProjectOutputFile', function (newVal, oldVal) {
  388. if (!newVal) {
  389. return
  390. }
  391. if (newVal === 'output.pdf') {
  392. const project = _.find(
  393. $scope.data.projects,
  394. p => p._id === $scope.data.selectedProjectId
  395. )
  396. $scope.data.name =
  397. (project != null ? project.name : undefined) != null
  398. ? `${project.name}.pdf`
  399. : 'output.pdf'
  400. } else {
  401. const fileName = newVal.split('/').reverse()[0]
  402. if (fileName) {
  403. $scope.data.name = fileName
  404. }
  405. }
  406. })
  407. const _setInFlight = type => ($scope.state.inFlight[type] = true)
  408. const _reset = function (opts) {
  409. const isError = opts.err === true
  410. const { inFlight } = $scope.state
  411. inFlight.projects = inFlight.entities = inFlight.compile = false
  412. $scope.state.inflight = false
  413. return ($scope.state.error = isError)
  414. }
  415. $scope.toggleOutputFilesMode = function () {
  416. if (!$scope.data.selectedProjectId) {
  417. return
  418. }
  419. return ($scope.state.isOutputFilesMode = !$scope.state.isOutputFilesMode)
  420. }
  421. $scope.shouldEnableProjectSelect = function () {
  422. const { state, data } = $scope
  423. return !state.inFlight.projects && data.projects
  424. }
  425. $scope.hasNoProjects = function () {
  426. const { state, data } = $scope
  427. return (
  428. !state.inFlight.projects &&
  429. (data.projects == null || data.projects.length === 0)
  430. )
  431. }
  432. $scope.shouldEnableProjectEntitySelect = function () {
  433. const { state, data } = $scope
  434. return (
  435. !state.inFlight.projects &&
  436. !state.inFlight.entities &&
  437. data.projects &&
  438. data.selectedProjectId
  439. )
  440. }
  441. $scope.shouldEnableProjectOutputFileSelect = function () {
  442. const { state, data } = $scope
  443. return (
  444. !state.inFlight.projects &&
  445. !state.inFlight.compile &&
  446. data.projects &&
  447. data.selectedProjectId
  448. )
  449. }
  450. const validate = function () {
  451. const { state } = $scope
  452. const { data } = $scope
  453. $scope.state.valid =
  454. !state.inFlight.projects &&
  455. !state.inFlight.entities &&
  456. data.projects &&
  457. data.selectedProjectId &&
  458. ((!$scope.state.isOutputFilesMode &&
  459. data.projectEntities &&
  460. data.selectedProjectEntity) ||
  461. ($scope.state.isOutputFilesMode &&
  462. data.projectOutputFiles &&
  463. data.selectedProjectOutputFile)) &&
  464. data.name
  465. }
  466. $scope.$watch('state', validate, true)
  467. $scope.$watch('data', validate, true)
  468. $scope.getUserProjects = function () {
  469. _setInFlight('projects')
  470. return ide.$http
  471. .get('/user/projects', {
  472. _csrf: window.csrfToken,
  473. })
  474. .then(function (resp) {
  475. $scope.data.projectEntities = null
  476. $scope.data.projects = resp.data.projects.filter(
  477. p => p._id !== ide.project_id
  478. )
  479. return _reset({ err: false })
  480. })
  481. .catch(err => _reset({ err: true }))
  482. }
  483. $scope.getProjectEntities = project_id => {
  484. _setInFlight('entities')
  485. return ide.$http
  486. .get(`/project/${project_id}/entities`, {
  487. _csrf: window.csrfToken,
  488. })
  489. .then(function (resp) {
  490. if ($scope.data.selectedProjectId === resp.data.project_id) {
  491. $scope.data.projectEntities = resp.data.entities
  492. return _reset({ err: false })
  493. }
  494. })
  495. .catch(err => _reset({ err: true }))
  496. }
  497. $scope.compileProjectAndGetOutputFiles = project_id => {
  498. _setInFlight('compile')
  499. return ide.$http
  500. .post(`/project/${project_id}/compile`, {
  501. check: 'silent',
  502. draft: false,
  503. incrementalCompilesEnabled: false,
  504. _csrf: window.csrfToken,
  505. })
  506. .then(function (resp) {
  507. if (resp.data.status === 'success') {
  508. const filteredFiles = resp.data.outputFiles.filter(f =>
  509. f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/)
  510. )
  511. $scope.data.projectOutputFiles = filteredFiles
  512. $scope.data.buildId = __guard__(
  513. filteredFiles != null ? filteredFiles[0] : undefined,
  514. x => x.build
  515. )
  516. console.log('>> build_id', $scope.data.buildId)
  517. return _reset({ err: false })
  518. } else {
  519. $scope.data.projectOutputFiles = null
  520. return _reset({ err: true })
  521. }
  522. })
  523. .catch(function (err) {
  524. console.error(err)
  525. return _reset({ err: true })
  526. })
  527. }
  528. $scope.init = () => $scope.getUserProjects()
  529. $timeout($scope.init, 0)
  530. return $scope.$on('create', function () {
  531. let payload, provider
  532. const projectId = $scope.data.selectedProjectId
  533. const { name } = $scope.data
  534. if ($scope.state.isOutputFilesMode) {
  535. provider = 'project_output_file'
  536. payload = {
  537. source_project_id: projectId,
  538. source_output_file_path: $scope.data.selectedProjectOutputFile,
  539. build_id: $scope.data.buildId,
  540. }
  541. } else {
  542. provider = 'project_file'
  543. payload = {
  544. source_project_id: projectId,
  545. source_entity_path: $scope.data.selectedProjectEntity,
  546. }
  547. }
  548. _setInFlight('create')
  549. ide.fileTreeManager
  550. .createLinkedFile(name, $scope.parent_folder, provider, payload)
  551. .then(function () {
  552. _reset({ err: false })
  553. return $scope.$emit('done', { name: name })
  554. })
  555. .catch(function (response) {
  556. const { data } = response
  557. $scope.error = data
  558. })
  559. .finally(function () {
  560. if (!$scope.$$phase) {
  561. $scope.$apply()
  562. }
  563. })
  564. })
  565. }
  566. )
  567. export default App.controller(
  568. 'UrlLinkedFileModalController',
  569. function ($scope, ide, $timeout) {
  570. $scope.inputs = {
  571. name: '',
  572. url: '',
  573. }
  574. $scope.nameChangedByUser = false
  575. $timeout(() => $scope.$broadcast('open'), 200)
  576. const validate = function () {
  577. const { name, url } = $scope.inputs
  578. if (name == null || name.length === 0) {
  579. return ($scope.state.valid = false)
  580. } else if (url == null || url.length === 0) {
  581. return ($scope.state.valid = false)
  582. } else {
  583. return ($scope.state.valid = true)
  584. }
  585. }
  586. $scope.$watch('inputs.name', validate)
  587. $scope.$watch('inputs.url', validate)
  588. $scope.$watch('inputs.url', function (url) {
  589. if (url != null && url !== '' && !$scope.nameChangedByUser) {
  590. url = url.replace('://', '') // Ignore http:// etc
  591. const parts = url.split('/').reverse()
  592. if (parts.length > 1) {
  593. // Wait for at one /
  594. return ($scope.inputs.name = parts[0])
  595. }
  596. }
  597. })
  598. return $scope.$on('create', function () {
  599. const { name, url } = $scope.inputs
  600. if (name == null || name.length === 0) {
  601. return
  602. }
  603. if (url == null || url.length === 0) {
  604. return
  605. }
  606. $scope.state.inflight = true
  607. return ide.fileTreeManager
  608. .createLinkedFile(name, $scope.parent_folder, 'url', { url })
  609. .then(function () {
  610. $scope.state.inflight = false
  611. return $scope.$emit('done', { name: name })
  612. })
  613. .catch(function (response) {
  614. const { data } = response
  615. $scope.error = data
  616. return ($scope.state.inflight = false)
  617. })
  618. .finally(function () {
  619. if (!$scope.$$phase) {
  620. $scope.$apply()
  621. }
  622. })
  623. })
  624. }
  625. )
  626. function __guard__(value, transform) {
  627. return typeof value !== 'undefined' && value !== null
  628. ? transform(value)
  629. : undefined
  630. }