file-tree-open-context.tsx 6.1 KB

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