file-tree-draggable.tsx 5.2 KB

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