file-tree-draggable.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { useEffect, useState, FC, useMemo } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
  4. import { DndProvider, DragSourceMonitor, useDrag, useDrop } from 'react-dnd'
  5. import {
  6. HTML5Backend,
  7. getEmptyImage,
  8. NativeTypes,
  9. } from 'react-dnd-html5-backend'
  10. import {
  11. findAllInTreeOrThrow,
  12. findAllFolderIdsInFolders,
  13. } from '../util/find-in-tree'
  14. import { useFileTreeActionable } from './file-tree-actionable'
  15. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  16. import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
  17. import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
  18. import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
  19. const DRAGGABLE_TYPE = 'ENTITY'
  20. export const FileTreeDraggableProvider: FC<
  21. React.PropsWithChildren<{
  22. fileTreeContainer?: HTMLDivElement
  23. }>
  24. > = ({ fileTreeContainer, children }) => {
  25. const options = useMemo(
  26. () => ({ rootElement: fileTreeContainer }),
  27. [fileTreeContainer]
  28. )
  29. return (
  30. <DndProvider backend={HTML5Backend} options={options}>
  31. {children}
  32. </DndProvider>
  33. )
  34. }
  35. type DragObject = {
  36. type: string
  37. title: string
  38. forbiddenFolderIds: Set<string>
  39. draggedEntityIds: Set<string>
  40. }
  41. type DropResult = {
  42. targetEntityId: string
  43. dropEffect: DataTransfer['dropEffect']
  44. }
  45. export function useDraggable(draggedEntityId: string) {
  46. const { t } = useTranslation()
  47. const { fileTreeData, fileTreeReadOnly } = useFileTreeData()
  48. const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
  49. const { finishMoving } = useFileTreeActionable()
  50. const [isDraggable, setIsDraggable] = useState(true)
  51. const [, dragRef, preview] = useDrag({
  52. type: DRAGGABLE_TYPE,
  53. item() {
  54. const draggedEntityIds = getDraggedEntityIds(
  55. isRootFolderSelected ? new Set() : selectedEntityIds,
  56. draggedEntityId
  57. )
  58. const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
  59. return {
  60. type: DRAGGABLE_TYPE,
  61. title: getDraggedTitle(draggedItems, t),
  62. forbiddenFolderIds: getForbiddenFolderIds(draggedItems),
  63. draggedEntityIds,
  64. }
  65. },
  66. canDrag() {
  67. return !fileTreeReadOnly && isDraggable
  68. },
  69. end(item: DragObject, monitor: DragSourceMonitor<DragObject, DropResult>) {
  70. if (monitor.didDrop()) {
  71. const result = monitor.getDropResult()
  72. if (result) {
  73. finishMoving(result.targetEntityId, item.draggedEntityIds) // TODO: use result.dropEffect
  74. }
  75. }
  76. },
  77. })
  78. // remove the automatic preview as we're using a custom preview via
  79. // FileTreeDraggablePreviewLayer
  80. useEffect(() => {
  81. preview(getEmptyImage())
  82. }, [preview])
  83. return { dragRef, setIsDraggable }
  84. }
  85. export function useDroppable(targetEntityId: string) {
  86. const { setDroppedFiles, startUploadingDocOrFile } = useFileTreeActionable()
  87. const [{ isOver }, dropRef] = useDrop({
  88. accept: [DRAGGABLE_TYPE, NativeTypes.FILE],
  89. canDrop(item: DragObject, monitor) {
  90. if (!monitor.isOver({ shallow: true })) {
  91. return false
  92. }
  93. return !(
  94. item.type === DRAGGABLE_TYPE &&
  95. item.forbiddenFolderIds.has(targetEntityId)
  96. )
  97. },
  98. drop(item, monitor) {
  99. // monitor.didDrop() returns true if the drop was already handled by a nested child
  100. if (monitor.didDrop()) {
  101. return
  102. }
  103. // item(s) dragged within the file tree
  104. if (item.type === DRAGGABLE_TYPE) {
  105. return { targetEntityId }
  106. }
  107. // native file(s) dragged in from outside
  108. getDroppedFiles(item as unknown as DataTransfer)
  109. .then(files =>
  110. files.filter(file =>
  111. // note: getDroppedFiles normalises webkitRelativePath to relativePath
  112. isAcceptableFile(file.name, (file as any).relativePath)
  113. )
  114. )
  115. .then(files => {
  116. setDroppedFiles({ files, targetFolderId: targetEntityId })
  117. startUploadingDocOrFile()
  118. })
  119. },
  120. collect(monitor) {
  121. return {
  122. isOver: monitor.canDrop(),
  123. }
  124. },
  125. })
  126. return { dropRef, isOver }
  127. }
  128. // Get the list of dragged entity ids. If the dragged entity is one of the
  129. // selected entities then all the selected entites are dragged entities,
  130. // otherwise it's the dragged entity only.
  131. function getDraggedEntityIds(
  132. selectedEntityIds: Set<string>,
  133. draggedEntityId: string
  134. ) {
  135. if (selectedEntityIds.size > 1 && selectedEntityIds.has(draggedEntityId)) {
  136. // dragging the multi-selected entities
  137. return new Set(selectedEntityIds)
  138. } else {
  139. // not dragging the selection; only the current item
  140. return new Set([draggedEntityId])
  141. }
  142. }
  143. // Get the draggable title. This is the name of the dragged entities if there's
  144. // only one, otherwise it's the number of dragged entities.
  145. function getDraggedTitle(
  146. draggedItems: Set<any>,
  147. t: (key: string, options: Record<string, any>) => void
  148. ) {
  149. if (draggedItems.size === 1) {
  150. const draggedItem = Array.from(draggedItems)[0]
  151. return draggedItem.entity.name
  152. }
  153. return t('n_items', { count: draggedItems.size })
  154. }
  155. // Get all children folder ids of any of the dragged items.
  156. function getForbiddenFolderIds(draggedItems: Set<FileTreeFindResult>) {
  157. const draggedFoldersArray = Array.from(draggedItems)
  158. .filter(draggedItem => {
  159. return draggedItem.type === 'folder'
  160. })
  161. .map(draggedItem => draggedItem.entity)
  162. const draggedFolders = new Set(draggedFoldersArray)
  163. return findAllFolderIdsInFolders(draggedFolders)
  164. }