file-tree-root.tsx 4.7 KB

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