file-tree-root.tsx 4.7 KB

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