FileTreeManager.js 20 KB

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