file-tree-open-context.tsx 5.2 KB

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