file-tree-open-context.tsx 5.5 KB

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