file-tree-open-context.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import {
  2. createContext,
  3. FC,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. } from 'react'
  11. import { useProjectContext } from '@/shared/context/project-context'
  12. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  13. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  14. import {
  15. FileTreeDocumentFindResult,
  16. FileTreeFileRefFindResult,
  17. FileTreeFindResult,
  18. } from '@/features/ide-react/types/file-tree'
  19. import { debugConsole } from '@/utils/debugging'
  20. import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
  21. import { sendMB } from '@/infrastructure/event-tracking'
  22. import { FileRef } from '../../../../../types/file-ref'
  23. import { useLayoutContext } from '@/shared/context/layout-context'
  24. const FileTreeOpenContext = createContext<
  25. | {
  26. selectedEntityCount: number
  27. openEntity: FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  28. handleFileTreeInit: () => void
  29. handleFileTreeSelect: (selectedEntities: FileTreeFindResult[]) => void
  30. handleFileTreeDelete: (entity: FileTreeFindResult) => void
  31. fileTreeExpanded: boolean
  32. toggleFileTreeExpanded: () => void
  33. }
  34. | undefined
  35. >(undefined)
  36. export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
  37. children,
  38. }) => {
  39. const { rootDocId, owner } = useProjectContext()
  40. const { eventEmitter, projectJoined } = useIdeReactContext()
  41. const { openDocWithId, currentDocumentId, openInitialDoc } =
  42. useEditorManagerContext()
  43. const { setOpenFile } = useLayoutContext()
  44. const [openEntity, setOpenEntity] = useState<
  45. FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  46. >(null)
  47. const [selectedEntityCount, setSelectedEntityCount] = useState(0)
  48. const [fileTreeReady, setFileTreeReady] = useState(false)
  49. // NOTE: Only used in editor redesign
  50. const [fileTreeExpanded, setFileTreeExpanded] = useState(true)
  51. const toggleFileTreeExpanded = useCallback(() => {
  52. setFileTreeExpanded(prev => !prev)
  53. }, [])
  54. const handleFileTreeInit = useCallback(() => {
  55. setFileTreeReady(true)
  56. }, [])
  57. // Open a document in the editor when one is selected in the file tree
  58. const handleFileTreeSelect = useCallback(
  59. (selectedEntities: FileTreeFindResult[]) => {
  60. debugConsole.log('File tree selection changed', selectedEntities)
  61. setSelectedEntityCount(selectedEntities.length)
  62. if (selectedEntities.length !== 1) {
  63. setOpenEntity(null)
  64. return
  65. }
  66. const [selected] = selectedEntities
  67. if (selected.type === 'folder') {
  68. return
  69. }
  70. setOpenEntity(selected)
  71. if (selected.type === 'doc' && fileTreeReady) {
  72. openDocWithId(selected.entity._id, { keepCurrentView: true })
  73. if (selected.entity.name.endsWith('.bib')) {
  74. sendMB('open-bib-file', {
  75. projectOwner: owner._id,
  76. isSampleFile: selected.entity.name === 'sample.bib',
  77. linkedFileProvider: null,
  78. })
  79. }
  80. }
  81. // Keep openFile scope value in sync with the file tree
  82. const openFile =
  83. selected.type === 'fileRef'
  84. ? convertFileRefToBinaryFile(selected.entity)
  85. : null
  86. setOpenFile(openFile)
  87. if (openFile) {
  88. if (selected?.entity?.name?.endsWith('.bib')) {
  89. sendMB('open-bib-file', {
  90. projectOwner: owner._id,
  91. isSampleFile: false,
  92. linkedFileProvider: (selected.entity as FileRef).linkedFileData
  93. ?.provider,
  94. })
  95. }
  96. window.dispatchEvent(new CustomEvent('file-view:file-opened'))
  97. }
  98. },
  99. [fileTreeReady, setOpenFile, openDocWithId, owner]
  100. )
  101. const handleFileTreeDelete = useCallback(
  102. (entity: FileTreeFindResult) => {
  103. eventEmitter.emit('entity:deleted', entity)
  104. // Select the root document if the current document was deleted
  105. if (entity.entity._id === currentDocumentId) {
  106. openDocWithId(rootDocId!)
  107. }
  108. },
  109. [eventEmitter, currentDocumentId, openDocWithId, rootDocId]
  110. )
  111. // Open a document once the file tree and project are ready
  112. const initialOpenDoneRef = useRef(false)
  113. useEffect(() => {
  114. if (
  115. rootDocId &&
  116. fileTreeReady &&
  117. projectJoined &&
  118. !initialOpenDoneRef.current
  119. ) {
  120. initialOpenDoneRef.current = true
  121. openInitialDoc(rootDocId)
  122. }
  123. }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
  124. const value = useMemo(() => {
  125. return {
  126. selectedEntityCount,
  127. openEntity,
  128. handleFileTreeInit,
  129. handleFileTreeSelect,
  130. handleFileTreeDelete,
  131. fileTreeExpanded,
  132. toggleFileTreeExpanded,
  133. }
  134. }, [
  135. handleFileTreeDelete,
  136. handleFileTreeInit,
  137. handleFileTreeSelect,
  138. openEntity,
  139. selectedEntityCount,
  140. fileTreeExpanded,
  141. toggleFileTreeExpanded,
  142. ])
  143. return (
  144. <FileTreeOpenContext.Provider value={value}>
  145. {children}
  146. </FileTreeOpenContext.Provider>
  147. )
  148. }
  149. export const useFileTreeOpenContext = () => {
  150. const context = useContext(FileTreeOpenContext)
  151. if (!context) {
  152. throw new Error(
  153. 'useFileTreeOpenContext is only available inside FileTreeOpenProvider'
  154. )
  155. }
  156. return context
  157. }