use-file-tree.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import { FileRef } from '../../../../../types/file-ref'
  2. import { BinaryFile } from '@/features/file-view/types/binary-file'
  3. import { useSelectFileTreeEntity } from '@/features/ide-react/hooks/use-select-file-tree-entity'
  4. import useScopeValue from '@/shared/hooks/use-scope-value'
  5. import { useCallback, useEffect, useRef, useState } from 'react'
  6. import {
  7. FileTreeDeleteHandler,
  8. FileTreeDocumentFindResult,
  9. FileTreeFileRefFindResult,
  10. FileTreeFindResult,
  11. } from '@/features/ide-react/types/file-tree'
  12. import { debugConsole } from '@/utils/debugging'
  13. import { useProjectContext } from '@/shared/context/project-context'
  14. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  15. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  16. export const useFileTree = () => {
  17. const { rootDocId } = useProjectContext()
  18. const { eventEmitter } = useIdeReactContext()
  19. const {
  20. openDocId: openDocWithId,
  21. currentDocumentId: openDocId,
  22. openInitialDoc,
  23. } = useEditorManagerContext()
  24. const { projectJoined } = useIdeReactContext()
  25. const { selectEntity } = useSelectFileTreeEntity()
  26. const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
  27. const [openEntity, setOpenEntity] = useState<
  28. FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  29. >(null)
  30. const [selectedEntityCount, setSelectedEntityCount] = useState(0)
  31. const [fileTreeReady, setFileTreeReady] = useState(false)
  32. const handleFileTreeInit = useCallback(() => {
  33. setFileTreeReady(true)
  34. }, [])
  35. // Open a document in the editor when one is selected in the file tree
  36. const handleFileTreeSelect = useCallback(
  37. (selectedEntities: FileTreeFindResult[]) => {
  38. debugConsole.log('File tree selection changed', selectedEntities)
  39. setSelectedEntityCount(selectedEntities.length)
  40. if (selectedEntities.length !== 1) {
  41. setOpenEntity(null)
  42. return
  43. }
  44. const [selected] = selectedEntities
  45. if (selected.type === 'folder') {
  46. return
  47. }
  48. setOpenEntity(selected)
  49. if (selected.type === 'doc' && fileTreeReady) {
  50. openDocWithId(selected.entity._id)
  51. }
  52. // Keep openFile scope value in sync with the file tree
  53. const openFile =
  54. selected.type === 'fileRef'
  55. ? convertFileRefToBinaryFile(selected.entity)
  56. : null
  57. setOpenFile(openFile)
  58. if (openFile) {
  59. window.dispatchEvent(new CustomEvent('file-view:file-opened'))
  60. }
  61. },
  62. [fileTreeReady, setOpenFile, openDocWithId]
  63. )
  64. const handleFileTreeDelete: FileTreeDeleteHandler = useCallback(
  65. entity => {
  66. eventEmitter.emit('entity:deleted', entity)
  67. // Select the root document if the current document was deleted
  68. if (entity.entity._id === openDocId) {
  69. openDocWithId(rootDocId)
  70. }
  71. },
  72. [eventEmitter, openDocId, openDocWithId, rootDocId]
  73. )
  74. const openDocIdRef = useRef<typeof openDocId | null>(null)
  75. // Synchronize the file tree when openDoc or openDocId is called on the editor
  76. // manager context from elsewhere. If the file tree does change, it will
  77. // trigger the onSelect handler in this component, which will update the local
  78. // state.
  79. useEffect(() => {
  80. if (openDocId !== openDocIdRef.current) {
  81. debugConsole.log(`openDocId changed to ${openDocId}`)
  82. openDocIdRef.current = openDocId
  83. if (openDocId !== null) {
  84. selectEntity(openDocId)
  85. }
  86. }
  87. }, [openDocId, selectEntity])
  88. // Open a document once the file tree and project are ready
  89. const initialOpenDoneRef = useRef(false)
  90. useEffect(() => {
  91. if (fileTreeReady && projectJoined && !initialOpenDoneRef.current) {
  92. initialOpenDoneRef.current = true
  93. openInitialDoc(rootDocId)
  94. }
  95. }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
  96. return {
  97. selectedEntityCount,
  98. openEntity,
  99. openDocId,
  100. handleFileTreeInit,
  101. handleFileTreeSelect,
  102. handleFileTreeDelete,
  103. }
  104. }
  105. function convertFileRefToBinaryFile(fileRef: FileRef): BinaryFile {
  106. return {
  107. _id: fileRef._id,
  108. name: fileRef.name,
  109. id: fileRef._id,
  110. type: 'file',
  111. selected: true,
  112. linkedFileData: fileRef.linkedFileData,
  113. created: fileRef.created ? new Date(fileRef.created) : new Date(),
  114. }
  115. }
  116. // `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a
  117. // `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
  118. // into which `FileViewHeader` passes its BinaryFile, expects a file object with
  119. // `created` property of type `string`, which is a mismatch. `TPRFileViewInfo`
  120. // is the only one making runtime complaints and it seems that other uses of
  121. // `FileViewHeader` pass in a string for `created`, so that's what this function
  122. // does too.
  123. export function fileViewFile(fileRef: FileRef) {
  124. return {
  125. ...convertFileRefToBinaryFile(fileRef),
  126. created: fileRef.created,
  127. }
  128. }