file-tree-root.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import React, { useEffect, useState } from 'react'
  2. import withErrorBoundary from '../../../infrastructure/error-boundary'
  3. import { useProjectContext } from '../../../shared/context/project-context'
  4. import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
  5. import FileTreeContext from './file-tree-context'
  6. import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer'
  7. import FileTreeFolderList from './file-tree-folder-list'
  8. import FileTreeToolbar from './file-tree-toolbar'
  9. import FileTreeToolbarNew from '@/features/file-tree/components/file-tree-toolbar'
  10. import FileTreeModalDelete from './modals/file-tree-modal-delete'
  11. import FileTreeModalCreateFolder from './modals/file-tree-modal-create-folder'
  12. import FileTreeModalError from './modals/file-tree-modal-error'
  13. import FileTreeContextMenu from './file-tree-context-menu'
  14. import FileTreeError from './file-tree-error'
  15. import { useDroppable } from '../contexts/file-tree-draggable'
  16. import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
  17. import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
  18. import FileTreeInner from './file-tree-inner'
  19. import { useDragLayer } from 'react-dnd'
  20. import classnames from 'classnames'
  21. import { pathInFolder } from '@/features/file-tree/util/path'
  22. import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
  23. import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
  24. const FileTreeRoot = React.memo<{
  25. onSelect: () => void
  26. onDelete: () => void
  27. onInit: () => void
  28. isConnected: boolean
  29. setRefProviderEnabled: () => void
  30. setStartedFreeTrial: () => void
  31. refProviders: Record<string, boolean>
  32. }>(function FileTreeRoot({
  33. refProviders,
  34. setRefProviderEnabled,
  35. setStartedFreeTrial,
  36. onSelect,
  37. onInit,
  38. onDelete,
  39. isConnected,
  40. }) {
  41. const [fileTreeContainer, setFileTreeContainer] =
  42. useState<HTMLDivElement | null>(null)
  43. const { projectId } = useProjectContext()
  44. const { fileTreeData } = useFileTreeData()
  45. const isReady = Boolean(projectId && fileTreeData)
  46. const newEditor = useIsNewEditorEnabled()
  47. useEffect(() => {
  48. if (fileTreeContainer) {
  49. const listener = (event: DragEvent) => {
  50. if (event.dataTransfer) {
  51. // store the dragged entity in dataTransfer
  52. const { dataset } = event.target as HTMLDivElement
  53. if (
  54. dataset.fileId &&
  55. dataset.fileType &&
  56. dataset.fileType !== 'folder'
  57. ) {
  58. event.dataTransfer.setData(
  59. 'application/x-overleaf-file-id',
  60. dataset.fileId
  61. )
  62. const filePath = pathInFolder(fileTreeData, dataset.fileId)
  63. if (filePath) {
  64. event.dataTransfer.setData(
  65. 'application/x-overleaf-file-path',
  66. filePath
  67. )
  68. }
  69. }
  70. }
  71. }
  72. fileTreeContainer.addEventListener('dragstart', listener)
  73. return () => {
  74. fileTreeContainer.removeEventListener('dragstart', listener)
  75. }
  76. }
  77. }, [fileTreeContainer, fileTreeData])
  78. useEffect(() => {
  79. if (isReady) onInit()
  80. }, [isReady, onInit])
  81. if (!isReady) return null
  82. return (
  83. <div
  84. className="file-tree"
  85. data-testid="file-tree"
  86. ref={setFileTreeContainer}
  87. >
  88. {fileTreeContainer && (
  89. <FileTreeContext
  90. refProviders={refProviders}
  91. setRefProviderEnabled={setRefProviderEnabled}
  92. setStartedFreeTrial={setStartedFreeTrial}
  93. onSelect={onSelect}
  94. fileTreeContainer={fileTreeContainer}
  95. >
  96. {isConnected ? null : <div className="disconnected-overlay" />}
  97. {newEditor ? <FileTreeToolbarNew /> : <FileTreeToolbar />}
  98. <FileTreeContextMenu />
  99. <FileTreeInner>
  100. <FileTreeRootFolder onDelete={onDelete} />
  101. </FileTreeInner>
  102. <FileTreeModalDelete />
  103. <FileTreeModalCreateFile />
  104. <FileTreeModalCreateFolder />
  105. <FileTreeModalError />
  106. </FileTreeContext>
  107. )}
  108. </div>
  109. )
  110. })
  111. function FileTreeRootFolder({
  112. onDelete,
  113. }: {
  114. onDelete: (entity: FileTreeFindResult, isFileRestore?: boolean) => void
  115. }) {
  116. useFileTreeSocketListener(onDelete)
  117. const { fileTreeData } = useFileTreeData()
  118. const { isOver, dropRef } = useDroppable(fileTreeData._id)
  119. const dragLayer = useDragLayer(monitor => ({
  120. isDragging: monitor.isDragging(),
  121. item: monitor.getItem(),
  122. clientOffset: monitor.getClientOffset(),
  123. }))
  124. return (
  125. <>
  126. <FileTreeDraggablePreviewLayer isOver={isOver} {...dragLayer} />
  127. <FileTreeFolderList
  128. folders={fileTreeData.folders}
  129. docs={fileTreeData.docs}
  130. files={fileTreeData.fileRefs}
  131. classes={{
  132. root: classnames('file-tree-list', {
  133. 'file-tree-dragging': dragLayer.isDragging,
  134. }),
  135. }}
  136. dropRef={dropRef}
  137. dataTestId="file-tree-list-root"
  138. />
  139. </>
  140. )
  141. }
  142. export default withErrorBoundary(FileTreeRoot, FileTreeError)