file-tree-root.tsx 4.6 KB

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