file-tree-root.tsx 4.9 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 FileTreeToolbarNew from '@/features/ide-redesign/components/file-tree/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. const FileTreeRoot = React.memo<{
  24. onSelect: () => void
  25. onDelete: () => void
  26. onInit: () => void
  27. isConnected: boolean
  28. setRefProviderEnabled: () => void
  29. setStartedFreeTrial: () => void
  30. refProviders: Record<string, boolean>
  31. }>(function FileTreeRoot({
  32. refProviders,
  33. setRefProviderEnabled,
  34. setStartedFreeTrial,
  35. onSelect,
  36. onInit,
  37. onDelete,
  38. isConnected,
  39. }) {
  40. const [fileTreeContainer, setFileTreeContainer] =
  41. useState<HTMLDivElement | null>(null)
  42. const { projectId } = useProjectContext()
  43. const { fileTreeData } = useFileTreeData()
  44. const isReady = Boolean(projectId && fileTreeData)
  45. const newEditor = useIsNewEditorEnabled()
  46. useEffect(() => {
  47. if (fileTreeContainer) {
  48. const listener = (event: DragEvent) => {
  49. if (event.dataTransfer) {
  50. // store the dragged entity in dataTransfer
  51. const { dataset } = event.target as HTMLDivElement
  52. if (
  53. dataset.fileId &&
  54. dataset.fileType &&
  55. dataset.fileType !== 'folder'
  56. ) {
  57. event.dataTransfer.setData(
  58. 'application/x-overleaf-file-id',
  59. dataset.fileId
  60. )
  61. const filePath = pathInFolder(fileTreeData, dataset.fileId)
  62. if (filePath) {
  63. event.dataTransfer.setData(
  64. 'application/x-overleaf-file-path',
  65. filePath
  66. )
  67. }
  68. }
  69. }
  70. }
  71. fileTreeContainer.addEventListener('dragstart', listener)
  72. return () => {
  73. fileTreeContainer.removeEventListener('dragstart', listener)
  74. }
  75. }
  76. }, [fileTreeContainer, fileTreeData])
  77. useEffect(() => {
  78. if (isReady) onInit()
  79. }, [isReady, onInit])
  80. if (!isReady) return null
  81. return (
  82. <div
  83. className="file-tree"
  84. data-testid="file-tree"
  85. ref={setFileTreeContainer}
  86. >
  87. {fileTreeContainer && (
  88. <FileTreeContext
  89. refProviders={refProviders}
  90. setRefProviderEnabled={setRefProviderEnabled}
  91. setStartedFreeTrial={setStartedFreeTrial}
  92. onSelect={onSelect}
  93. fileTreeContainer={fileTreeContainer}
  94. >
  95. {isConnected ? null : <div className="disconnected-overlay" />}
  96. {newEditor ? <FileTreeToolbarNew /> : <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}
  132. dataTestId="file-tree-list-root"
  133. />
  134. </>
  135. )
  136. }
  137. export default withErrorBoundary(FileTreeRoot, FileTreeError)