FileTreeManager.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. /* eslint-disable
  2. camelcase,
  3. n/handle-callback-err,
  4. max-len,
  5. no-dupe-class-members,
  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. * DS101: Remove unnecessary use of Array.from
  14. * DS102: Remove unnecessary code created because of implicit returns
  15. * DS103: Rewrite code to no longer use __guard__
  16. * DS202: Simplify dynamic range loops
  17. * DS205: Consider reworking code to avoid use of IIFEs
  18. * DS207: Consider shorter variations of null checks
  19. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  20. */
  21. import './controllers/FileTreeController'
  22. import '../../features/file-tree/controllers/file-tree-controller'
  23. import { debugConsole } from '@/utils/debugging'
  24. let FileTreeManager
  25. export default FileTreeManager = class FileTreeManager {
  26. constructor(ide, $scope) {
  27. this.ide = ide
  28. this.$scope = $scope
  29. this.$scope.$on('project:joined', () => {
  30. this.loadRootFolder()
  31. this.loadDeletedDocs()
  32. return this.$scope.$emit('file-tree:initialized')
  33. })
  34. this.$scope.$on('entities:multiSelected', (_event, data) => {
  35. this.$scope.$apply(() => {
  36. this.$scope.multiSelectedCount = data.count
  37. this.$scope.editor.multiSelectedCount = data.count
  38. })
  39. })
  40. this.$scope.$watch('rootFolder', rootFolder => {
  41. if (rootFolder != null) {
  42. return this.recalculateDocList()
  43. }
  44. })
  45. this._bindToSocketEvents()
  46. this.$scope.multiSelectedCount = 0
  47. $(document).on('click', () => {
  48. this.clearMultiSelectedEntities()
  49. setTimeout(() => this.$scope.$digest(), 0)
  50. })
  51. }
  52. _bindToSocketEvents() {
  53. this.ide.socket.on('reciveNewDoc', (parent_folder_id, doc) => {
  54. const parent_folder =
  55. this.findEntityById(parent_folder_id) || this.$scope.rootFolder
  56. return this.$scope.$apply(() => {
  57. parent_folder.children.push({
  58. name: doc.name,
  59. id: doc._id,
  60. type: 'doc',
  61. })
  62. return this.recalculateDocList()
  63. })
  64. })
  65. this.ide.socket.on(
  66. 'reciveNewFile',
  67. (parent_folder_id, file, source, linkedFileData) => {
  68. const parent_folder =
  69. this.findEntityById(parent_folder_id) || this.$scope.rootFolder
  70. return this.$scope.$apply(() => {
  71. parent_folder.children.push({
  72. name: file.name,
  73. id: file._id,
  74. type: 'file',
  75. linkedFileData,
  76. created: file.created,
  77. })
  78. return this.recalculateDocList()
  79. })
  80. }
  81. )
  82. this.ide.socket.on('reciveNewFolder', (parent_folder_id, folder) => {
  83. const parent_folder =
  84. this.findEntityById(parent_folder_id) || this.$scope.rootFolder
  85. return this.$scope.$apply(() => {
  86. parent_folder.children.push({
  87. name: folder.name,
  88. id: folder._id,
  89. type: 'folder',
  90. children: [],
  91. })
  92. return this.recalculateDocList()
  93. })
  94. })
  95. this.ide.socket.on('reciveEntityRename', (entity_id, name) => {
  96. const entity = this.findEntityById(entity_id)
  97. if (entity == null) {
  98. return
  99. }
  100. return this.$scope.$apply(() => {
  101. entity.name = name
  102. return this.recalculateDocList()
  103. })
  104. })
  105. this.ide.socket.on('removeEntity', entity_id => {
  106. const entity = this.findEntityById(entity_id)
  107. if (entity == null) {
  108. return
  109. }
  110. this.$scope.$apply(() => {
  111. this._deleteEntityFromScope(entity)
  112. return this.recalculateDocList()
  113. })
  114. return this.$scope.$broadcast('entity:deleted', entity)
  115. })
  116. return this.ide.socket.on('reciveEntityMove', (entity_id, folder_id) => {
  117. const entity = this.findEntityById(entity_id)
  118. const folder = this.findEntityById(folder_id)
  119. return this.$scope.$apply(() => {
  120. this._moveEntityInScope(entity, folder)
  121. return this.recalculateDocList()
  122. })
  123. })
  124. }
  125. selectEntity(entity) {
  126. this.selected_entity_id = entity.id // For reselecting after a reconnect
  127. this.ide.fileTreeManager.forEachEntity(entity => (entity.selected = false))
  128. return (entity.selected = true)
  129. }
  130. getMultiSelectedEntities() {
  131. const entities = []
  132. this.forEachEntity(function (e) {
  133. if (e.multiSelected) {
  134. return entities.push(e)
  135. }
  136. })
  137. return entities
  138. }
  139. getFullCount() {
  140. const entities = []
  141. this.forEachEntity(function (e) {
  142. if (!e.deleted) entities.push(e)
  143. })
  144. return entities.length
  145. }
  146. getMultiSelectedEntityChildNodes() {
  147. // use pathnames with a leading slash to avoid
  148. // problems with reserved Object properties
  149. const entities = this.getMultiSelectedEntities()
  150. const paths = {}
  151. for (const entity of Array.from(entities)) {
  152. paths['/' + this.getEntityPath(entity)] = entity
  153. }
  154. const prefixes = {}
  155. for (const path in paths) {
  156. const entity = paths[path]
  157. const parts = path.split('/')
  158. if (parts.length <= 2) {
  159. continue
  160. } else {
  161. // Record prefixes a/b/c.tex -> 'a' and 'a/b'
  162. for (
  163. let i = 1, end = parts.length - 1, asc = end >= 1;
  164. asc ? i <= end : i >= end;
  165. asc ? i++ : i--
  166. ) {
  167. prefixes['/' + parts.slice(0, i).join('/')] = true
  168. }
  169. }
  170. }
  171. const child_entities = []
  172. for (const path in paths) {
  173. // If the path is in the prefixes, then it's a parent folder and
  174. // should be ignore
  175. const entity = paths[path]
  176. if (prefixes[path] == null) {
  177. child_entities.push(entity)
  178. }
  179. }
  180. return child_entities
  181. }
  182. clearMultiSelectedEntities() {
  183. if (this.$scope.multiSelectedCount === 0) {
  184. return
  185. } // Be efficient, this is called a lot on 'click'
  186. this.forEachEntity(entity => (entity.multiSelected = false))
  187. return (this.$scope.multiSelectedCount = 0)
  188. }
  189. existsInFolder(folder_id, name) {
  190. const folder = this.findEntityById(folder_id)
  191. if (folder == null) {
  192. return false
  193. }
  194. const entity = this._findEntityByPathInFolder(folder, name)
  195. return entity != null
  196. }
  197. findSelectedEntity() {
  198. let selected = null
  199. this.forEachEntity(function (entity) {
  200. if (entity.selected) {
  201. return (selected = entity)
  202. }
  203. })
  204. return selected
  205. }
  206. findEntityById(id, options) {
  207. if (options == null) {
  208. options = {}
  209. }
  210. if (this.$scope.rootFolder.id === id) {
  211. return this.$scope.rootFolder
  212. }
  213. let entity = this._findEntityByIdInFolder(this.$scope.rootFolder, id)
  214. if (entity != null) {
  215. return entity
  216. }
  217. if (options.includeDeleted) {
  218. for (entity of Array.from(this.$scope.deletedDocs)) {
  219. if (entity.id === id) {
  220. return entity
  221. }
  222. }
  223. }
  224. return null
  225. }
  226. _findEntityByIdInFolder(folder, id) {
  227. for (const entity of Array.from(folder.children || [])) {
  228. if (entity.id === id) {
  229. return entity
  230. } else if (entity.children != null) {
  231. const result = this._findEntityByIdInFolder(entity, id)
  232. if (result != null) {
  233. return result
  234. }
  235. }
  236. }
  237. return null
  238. }
  239. findEntityByPath(path) {
  240. return this._findEntityByPathInFolder(this.$scope.rootFolder, path)
  241. }
  242. getPreviewByPath(path) {
  243. for (const suffix of [
  244. '',
  245. '.png',
  246. '.jpg',
  247. '.jpeg',
  248. '.pdf',
  249. '.PNG',
  250. '.JPG',
  251. '.JPEG',
  252. '.PDF',
  253. ]) {
  254. const entity = this.findEntityByPath(path + suffix)
  255. if (entity) {
  256. return {
  257. url: `/project/${this.$scope.project._id}/file/${entity.id}`,
  258. extension: entity.name.split('.').pop(),
  259. }
  260. }
  261. }
  262. return null
  263. }
  264. _findEntityByPathInFolder(folder, path) {
  265. if (path == null || folder == null) {
  266. return null
  267. }
  268. if (path === '') {
  269. return folder
  270. }
  271. const parts = path.split('/')
  272. const name = parts.shift()
  273. const rest = parts.join('/')
  274. if (name === '.') {
  275. return this._findEntityByPathInFolder(folder, rest)
  276. }
  277. for (const entity of Array.from(folder.children)) {
  278. if (entity.name === name) {
  279. if (rest === '') {
  280. return entity
  281. } else if (entity.type === 'folder') {
  282. return this._findEntityByPathInFolder(entity, rest)
  283. }
  284. }
  285. }
  286. return null
  287. }
  288. forEachEntity(callback) {
  289. if (callback == null) {
  290. callback = function () {}
  291. }
  292. this._forEachEntityInFolder(this.$scope.rootFolder, null, callback)
  293. return (() => {
  294. const result = []
  295. for (const entity of Array.from(this.$scope.deletedDocs || [])) {
  296. result.push(callback(entity))
  297. }
  298. return result
  299. })()
  300. }
  301. _forEachEntityInFolder(folder, path, callback) {
  302. return (() => {
  303. const result = []
  304. for (const entity of Array.from(folder.children || [])) {
  305. let childPath
  306. if (path != null) {
  307. childPath = path + '/' + entity.name
  308. } else {
  309. childPath = entity.name
  310. }
  311. callback(entity, folder, childPath)
  312. if (entity.children != null) {
  313. result.push(this._forEachEntityInFolder(entity, childPath, callback))
  314. } else {
  315. result.push(undefined)
  316. }
  317. }
  318. return result
  319. })()
  320. }
  321. getEntityPath(entity) {
  322. return this._getEntityPathInFolder(this.$scope.rootFolder, entity)
  323. }
  324. _getEntityPathInFolder(folder, entity) {
  325. for (const child of Array.from(folder.children || [])) {
  326. if (child === entity) {
  327. return entity.name
  328. } else if (child.type === 'folder') {
  329. const path = this._getEntityPathInFolder(child, entity)
  330. if (path != null) {
  331. return child.name + '/' + path
  332. }
  333. }
  334. }
  335. return null
  336. }
  337. getRootDocDirname() {
  338. const rootDoc = this.findEntityById(this.$scope.project.rootDoc_id)
  339. if (rootDoc == null) {
  340. return
  341. }
  342. return this._getEntityDirname(rootDoc)
  343. }
  344. _getEntityDirname(entity) {
  345. const path = this.getEntityPath(entity)
  346. if (path == null) {
  347. return
  348. }
  349. return path.split('/').slice(0, -1).join('/')
  350. }
  351. _findParentFolder(entity) {
  352. const dirname = this._getEntityDirname(entity)
  353. if (dirname == null) {
  354. return
  355. }
  356. return this.findEntityByPath(dirname)
  357. }
  358. loadRootFolder() {
  359. return (this.$scope.rootFolder = this._parseFolder(
  360. __guard__(
  361. this.$scope != null ? this.$scope.project : undefined,
  362. x => x.rootFolder[0]
  363. )
  364. ))
  365. }
  366. _parseFolder(rawFolder) {
  367. const folder = {
  368. name: rawFolder.name,
  369. id: rawFolder._id,
  370. type: 'folder',
  371. children: [],
  372. selected: rawFolder._id === this.selected_entity_id,
  373. }
  374. for (const doc of Array.from(rawFolder.docs || [])) {
  375. folder.children.push({
  376. name: doc.name,
  377. id: doc._id,
  378. type: 'doc',
  379. selected: doc._id === this.selected_entity_id,
  380. })
  381. }
  382. for (const file of Array.from(rawFolder.fileRefs || [])) {
  383. folder.children.push({
  384. name: file.name,
  385. id: file._id,
  386. type: 'file',
  387. selected: file._id === this.selected_entity_id,
  388. linkedFileData: file.linkedFileData,
  389. created: file.created,
  390. })
  391. }
  392. for (const childFolder of Array.from(rawFolder.folders || [])) {
  393. folder.children.push(this._parseFolder(childFolder))
  394. }
  395. return folder
  396. }
  397. loadDeletedDocs() {
  398. this.$scope.deletedDocs = []
  399. return Array.from(this.$scope.project.deletedDocs || []).map(doc =>
  400. this.$scope.deletedDocs.push({
  401. name: doc.name,
  402. id: doc._id,
  403. type: 'doc',
  404. deleted: true,
  405. })
  406. )
  407. }
  408. recalculateDocList() {
  409. this.$scope.docs = []
  410. this.forEachEntity((entity, parentFolder, path) => {
  411. if (entity.type === 'doc' && !entity.deleted) {
  412. return this.$scope.docs.push({
  413. doc: entity,
  414. path,
  415. })
  416. }
  417. })
  418. // Keep list ordered by folders, then name
  419. return this.$scope.docs.sort(function (a, b) {
  420. const aDepth = (a.path.match(/\//g) || []).length
  421. const bDepth = (b.path.match(/\//g) || []).length
  422. if (aDepth - bDepth !== 0) {
  423. return -(aDepth - bDepth) // Deeper path == folder first
  424. } else if (a.path < b.path) {
  425. return -1
  426. } else if (a.path > b.path) {
  427. return 1
  428. } else {
  429. return 0
  430. }
  431. })
  432. }
  433. getCurrentFolder() {
  434. // Return the root folder if nothing is selected
  435. return (
  436. this._getCurrentFolder(this.$scope.rootFolder) || this.$scope.rootFolder
  437. )
  438. }
  439. _getCurrentFolder(startFolder) {
  440. if (startFolder == null) {
  441. startFolder = this.$scope.rootFolder
  442. }
  443. for (const entity of Array.from(startFolder.children || [])) {
  444. // The 'current' folder is either the one selected, or
  445. // the one containing the selected doc/file
  446. if (entity.selected) {
  447. if (entity.type === 'folder') {
  448. return entity
  449. } else {
  450. return startFolder
  451. }
  452. }
  453. if (entity.type === 'folder') {
  454. const result = this._getCurrentFolder(entity)
  455. if (result != null) {
  456. return result
  457. }
  458. }
  459. }
  460. return null
  461. }
  462. projectContainsFolder() {
  463. for (const entity of Array.from(this.$scope.rootFolder.children)) {
  464. if (entity.type === 'folder') {
  465. return true
  466. }
  467. }
  468. return false
  469. }
  470. existsInThisFolder(folder, name) {
  471. for (const entity of Array.from(
  472. (folder != null ? folder.children : undefined) || []
  473. )) {
  474. if (entity.name === name) {
  475. return true
  476. }
  477. }
  478. return false
  479. }
  480. nameExistsError(message) {
  481. if (message == null) {
  482. message = 'already exists'
  483. }
  484. const nameExists = this.ide.$q.defer()
  485. nameExists.reject({ data: message })
  486. return nameExists.promise
  487. }
  488. createDoc(name, parent_folder) {
  489. // check if a doc/file/folder already exists with this name
  490. if (parent_folder == null) {
  491. parent_folder = this.getCurrentFolder()
  492. }
  493. if (this.existsInThisFolder(parent_folder, name)) {
  494. return this.nameExistsError()
  495. }
  496. // We'll wait for the socket.io notification to actually
  497. // add the doc for us.
  498. return this.ide.$http.post(`/project/${this.ide.project_id}/doc`, {
  499. name,
  500. parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
  501. _csrf: window.csrfToken,
  502. })
  503. }
  504. createFolder(name, parent_folder) {
  505. // check if a doc/file/folder already exists with this name
  506. if (parent_folder == null) {
  507. parent_folder = this.getCurrentFolder()
  508. }
  509. if (this.existsInThisFolder(parent_folder, name)) {
  510. return this.nameExistsError()
  511. }
  512. // We'll wait for the socket.io notification to actually
  513. // add the folder for us.
  514. return this.ide.$http.post(`/project/${this.ide.project_id}/folder`, {
  515. name,
  516. parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
  517. _csrf: window.csrfToken,
  518. })
  519. }
  520. createLinkedFile(name, parent_folder, provider, data) {
  521. // check if a doc/file/folder already exists with this name
  522. if (parent_folder == null) {
  523. parent_folder = this.getCurrentFolder()
  524. }
  525. if (this.existsInThisFolder(parent_folder, name)) {
  526. return this.nameExistsError()
  527. }
  528. // We'll wait for the socket.io notification to actually
  529. // add the file for us.
  530. return this.ide.$http.post(
  531. `/project/${this.ide.project_id}/linked_file`,
  532. {
  533. name,
  534. parent_folder_id: parent_folder != null ? parent_folder.id : undefined,
  535. provider,
  536. data,
  537. _csrf: window.csrfToken,
  538. },
  539. {
  540. disableAutoLoginRedirect: true,
  541. }
  542. )
  543. }
  544. refreshLinkedFile(file) {
  545. const parent_folder = this._findParentFolder(file)
  546. const provider =
  547. file.linkedFileData != null ? file.linkedFileData.provider : undefined
  548. if (provider == null) {
  549. debugConsole.warn(`>> no provider for ${file.name}`, file)
  550. return
  551. }
  552. return this.ide.$http.post(
  553. `/project/${this.ide.project_id}/linked_file/${file.id}/refresh`,
  554. {
  555. _csrf: window.csrfToken,
  556. },
  557. {
  558. disableAutoLoginRedirect: true,
  559. }
  560. )
  561. }
  562. renameEntity(entity, name, callback) {
  563. if (callback == null) {
  564. callback = function () {}
  565. }
  566. if (entity.name === name) {
  567. return
  568. }
  569. if (name.length >= 150) {
  570. return
  571. }
  572. // check if a doc/file/folder already exists with this name
  573. const parent_folder = this.getCurrentFolder()
  574. if (this.existsInThisFolder(parent_folder, name)) {
  575. return this.nameExistsError()
  576. }
  577. entity.renamingToName = name
  578. return this.ide.$http
  579. .post(
  580. `/project/${this.ide.project_id}/${entity.type}/${entity.id}/rename`,
  581. {
  582. name,
  583. _csrf: window.csrfToken,
  584. }
  585. )
  586. .then(() => (entity.name = name))
  587. .finally(() => (entity.renamingToName = null))
  588. }
  589. deleteEntity(entity, callback) {
  590. // We'll wait for the socket.io notification to
  591. // delete from scope.
  592. if (callback == null) {
  593. callback = function () {}
  594. }
  595. return this.ide.queuedHttp({
  596. method: 'DELETE',
  597. url: `/project/${this.ide.project_id}/${entity.type}/${entity.id}`,
  598. headers: {
  599. 'X-Csrf-Token': window.csrfToken,
  600. },
  601. })
  602. }
  603. moveEntity(entity, parent_folder) {
  604. // Abort move if the folder being moved (entity) has the parent_folder as child
  605. // since that would break the tree structure.
  606. if (this._isChildFolder(entity, parent_folder)) {
  607. return
  608. }
  609. // check if a doc/file/folder already exists with this name
  610. if (this.existsInThisFolder(parent_folder, entity.name)) {
  611. throw new Error('file exists in this location')
  612. }
  613. // Wait for the http response before doing the move
  614. this.ide.queuedHttp
  615. .post(
  616. `/project/${this.ide.project_id}/${entity.type}/${entity.id}/move`,
  617. {
  618. folder_id: parent_folder.id,
  619. _csrf: window.csrfToken,
  620. }
  621. )
  622. .then(() => {
  623. this._moveEntityInScope(entity, parent_folder)
  624. })
  625. }
  626. _isChildFolder(parent_folder, child_folder) {
  627. const parent_path = this.getEntityPath(parent_folder) || '' // null if root folder
  628. const child_path = this.getEntityPath(child_folder) || '' // null if root folder
  629. // is parent path the beginning of child path?
  630. return child_path.slice(0, parent_path.length) === parent_path
  631. }
  632. _deleteEntityFromScope(entity, options) {
  633. if (options == null) {
  634. options = { moveToDeleted: true }
  635. }
  636. if (entity == null) {
  637. return
  638. }
  639. let parent_folder = null
  640. this.forEachEntity(function (possible_entity, folder) {
  641. if (possible_entity === entity) {
  642. return (parent_folder = folder)
  643. }
  644. })
  645. if (parent_folder != null) {
  646. const index = parent_folder.children.indexOf(entity)
  647. if (index > -1) {
  648. parent_folder.children.splice(index, 1)
  649. }
  650. }
  651. if (entity.type !== 'folder' && entity.selected) {
  652. this.$scope.ui.view = null
  653. }
  654. if (entity.type === 'doc' && options.moveToDeleted) {
  655. entity.deleted = true
  656. return this.$scope.deletedDocs.push(entity)
  657. }
  658. }
  659. _moveEntityInScope(entity, parent_folder) {
  660. if (Array.from(parent_folder.children).includes(entity)) {
  661. return
  662. }
  663. this._deleteEntityFromScope(entity, { moveToDeleted: false })
  664. return parent_folder.children.push(entity)
  665. }
  666. }
  667. function __guard__(value, transform) {
  668. return typeof value !== 'undefined' && value !== null
  669. ? transform(value)
  670. : undefined
  671. }