Explorar el Código

Upgrade react-dnd (#16753)

GitOrigin-RevId: 5a62bed823b716a6e0d6d3aa57ee187d161f3346
Alf Eaton hace 2 años
padre
commit
6dc7ced2df

+ 83 - 64
package-lock.json

@@ -9813,21 +9813,21 @@
       }
     },
     "node_modules/@react-dnd/asap": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
-      "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==",
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
+      "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
       "dev": true
     },
     "node_modules/@react-dnd/invariant": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
-      "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
+      "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
       "dev": true
     },
     "node_modules/@react-dnd/shallowequal": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
-      "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
+      "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
       "dev": true
     },
     "node_modules/@replit/codemirror-emacs": {
@@ -15638,6 +15638,8 @@
       "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
       "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
       "dev": true,
+      "optional": true,
+      "peer": true,
       "dependencies": {
         "@types/react": "*",
         "hoist-non-react-statics": "^3.3.0"
@@ -22091,14 +22093,14 @@
       "dev": true
     },
     "node_modules/dnd-core": {
-      "version": "11.1.3",
-      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz",
-      "integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==",
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
+      "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
       "dev": true,
       "dependencies": {
-        "@react-dnd/asap": "^4.0.0",
-        "@react-dnd/invariant": "^2.0.0",
-        "redux": "^4.0.4"
+        "@react-dnd/asap": "^5.0.1",
+        "@react-dnd/invariant": "^4.0.1",
+        "redux": "^4.2.0"
       }
     },
     "node_modules/dns-equal": {
@@ -36395,28 +36397,42 @@
       "dev": true
     },
     "node_modules/react-dnd": {
-      "version": "11.1.3",
-      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz",
-      "integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==",
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
+      "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
       "dev": true,
       "dependencies": {
-        "@react-dnd/shallowequal": "^2.0.0",
-        "@types/hoist-non-react-statics": "^3.3.1",
-        "dnd-core": "^11.1.3",
-        "hoist-non-react-statics": "^3.3.0"
+        "@react-dnd/invariant": "^4.0.1",
+        "@react-dnd/shallowequal": "^4.0.1",
+        "dnd-core": "^16.0.1",
+        "fast-deep-equal": "^3.1.3",
+        "hoist-non-react-statics": "^3.3.2"
       },
       "peerDependencies": {
-        "react": ">= 16.9.0",
-        "react-dom": ">= 16.9.0"
+        "@types/hoist-non-react-statics": ">= 3.3.1",
+        "@types/node": ">= 12",
+        "@types/react": ">= 16",
+        "react": ">= 16.14"
+      },
+      "peerDependenciesMeta": {
+        "@types/hoist-non-react-statics": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@types/react": {
+          "optional": true
+        }
       }
     },
     "node_modules/react-dnd-html5-backend": {
-      "version": "11.1.3",
-      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz",
-      "integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==",
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
+      "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
       "dev": true,
       "dependencies": {
-        "dnd-core": "^11.1.3"
+        "dnd-core": "^16.0.1"
       }
     },
     "node_modules/react-docgen": {
@@ -37086,9 +37102,9 @@
       }
     },
     "node_modules/redux": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
-      "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+      "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
       "dev": true,
       "dependencies": {
         "@babel/runtime": "^7.9.2"
@@ -46316,8 +46332,8 @@
         "react-bootstrap": "^0.33.1",
         "react-chartjs-2": "^5.0.1",
         "react-color": "^2.19.3",
-        "react-dnd": "^11.1.3",
-        "react-dnd-html5-backend": "^11.1.3",
+        "react-dnd": "^16.0.1",
+        "react-dnd-html5-backend": "^16.0.1",
         "react-dom": "^17.0.2",
         "react-error-boundary": "^2.3.1",
         "react-google-recaptcha": "^3.1.0",
@@ -55223,8 +55239,8 @@
         "react-bootstrap": "^0.33.1",
         "react-chartjs-2": "^5.0.1",
         "react-color": "^2.19.3",
-        "react-dnd": "^11.1.3",
-        "react-dnd-html5-backend": "^11.1.3",
+        "react-dnd": "^16.0.1",
+        "react-dnd-html5-backend": "^16.0.1",
         "react-dom": "^17.0.2",
         "react-error-boundary": "^2.3.1",
         "react-google-recaptcha": "^3.1.0",
@@ -56954,21 +56970,21 @@
       }
     },
     "@react-dnd/asap": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
-      "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==",
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
+      "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
       "dev": true
     },
     "@react-dnd/invariant": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
-      "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
+      "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
       "dev": true
     },
     "@react-dnd/shallowequal": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
-      "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
+      "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
       "dev": true
     },
     "@replit/codemirror-emacs": {
@@ -61339,6 +61355,8 @@
       "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
       "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
       "dev": true,
+      "optional": true,
+      "peer": true,
       "requires": {
         "@types/react": "*",
         "hoist-non-react-statics": "^3.3.0"
@@ -66412,14 +66430,14 @@
       "dev": true
     },
     "dnd-core": {
-      "version": "11.1.3",
-      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz",
-      "integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==",
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
+      "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
       "dev": true,
       "requires": {
-        "@react-dnd/asap": "^4.0.0",
-        "@react-dnd/invariant": "^2.0.0",
-        "redux": "^4.0.4"
+        "@react-dnd/asap": "^5.0.1",
+        "@react-dnd/invariant": "^4.0.1",
+        "redux": "^4.2.0"
       }
     },
     "dns-equal": {
@@ -78279,24 +78297,25 @@
       "dev": true
     },
     "react-dnd": {
-      "version": "11.1.3",
-      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz",
-      "integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==",
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
+      "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
       "dev": true,
       "requires": {
-        "@react-dnd/shallowequal": "^2.0.0",
-        "@types/hoist-non-react-statics": "^3.3.1",
-        "dnd-core": "^11.1.3",
-        "hoist-non-react-statics": "^3.3.0"
+        "@react-dnd/invariant": "^4.0.1",
+        "@react-dnd/shallowequal": "^4.0.1",
+        "dnd-core": "^16.0.1",
+        "fast-deep-equal": "^3.1.3",
+        "hoist-non-react-statics": "^3.3.2"
       }
     },
     "react-dnd-html5-backend": {
-      "version": "11.1.3",
-      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz",
-      "integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==",
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
+      "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
       "dev": true,
       "requires": {
-        "dnd-core": "^11.1.3"
+        "dnd-core": "^16.0.1"
       }
     },
     "react-docgen": {
@@ -78807,9 +78826,9 @@
       }
     },
     "redux": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
-      "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+      "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
       "dev": true,
       "requires": {
         "@babel/runtime": "^7.9.2"

+ 5 - 1
services/web/frontend/js/features/file-tree/components/file-tree-context.tsx

@@ -15,12 +15,14 @@ const FileTreeContext: FC<{
   setRefProviderEnabled: (provider: string, value: boolean) => void
   setStartedFreeTrial: (value: boolean) => void
   onSelect: () => void
+  fileTreeContainer?: HTMLDivElement
 }> = ({
   refProviders,
   reindexReferences,
   setRefProviderEnabled,
   setStartedFreeTrial,
   onSelect,
+  fileTreeContainer,
   children,
 }) => {
   return (
@@ -32,7 +34,9 @@ const FileTreeContext: FC<{
     >
       <FileTreeSelectableProvider onSelect={onSelect}>
         <FileTreeActionableProvider reindexReferences={reindexReferences}>
-          <FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
+          <FileTreeDraggableProvider fileTreeContainer={fileTreeContainer}>
+            {children}
+          </FileTreeDraggableProvider>
         </FileTreeActionableProvider>
       </FileTreeSelectableProvider>
     </FileTreeMainProvider>

+ 1 - 1
services/web/frontend/js/features/file-tree/components/file-tree-draggable-preview-layer.jsx

@@ -40,7 +40,7 @@ FileTreeDraggablePreviewLayer.propTypes = {
   isOver: PropTypes.bool.isRequired,
   isDragging: PropTypes.bool.isRequired,
   item: PropTypes.shape({
-    title: PropTypes.string.isRequired,
+    title: PropTypes.string,
   }),
   clientOffset: PropTypes.shape({
     x: PropTypes.number,

+ 0 - 1
services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx

@@ -22,7 +22,6 @@ function FileTreeFolderList({
       className={classNames('list-unstyled', classes.root)}
       role="tree"
       ref={dropRef}
-      dnd-container="true"
       data-testid={dataTestId}
     >
       {folders.sort(compareFunction).map(folder => {

+ 26 - 19
services/web/frontend/js/features/file-tree/components/file-tree-root.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react'
+import React, { useEffect, useState } from 'react'
 import withErrorBoundary from '../../../infrastructure/error-boundary'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
@@ -37,6 +37,8 @@ const FileTreeRoot = React.memo<{
   onDelete,
   isConnected,
 }) {
+  const [fileTreeContainer, setFileTreeContainer] =
+    useState<HTMLDivElement | null>(null)
   const { _id: projectId } = useProjectContext()
   const { fileTreeData } = useFileTreeData()
   const isReady = Boolean(projectId && fileTreeData)
@@ -47,24 +49,29 @@ const FileTreeRoot = React.memo<{
   if (!isReady) return null
 
   return (
-    <FileTreeContext
-      refProviders={refProviders}
-      setRefProviderEnabled={setRefProviderEnabled}
-      setStartedFreeTrial={setStartedFreeTrial}
-      reindexReferences={reindexReferences}
-      onSelect={onSelect}
-    >
-      {isConnected ? null : <div className="disconnected-overlay" />}
-      <FileTreeToolbar />
-      <FileTreeContextMenu />
-      <FileTreeInner>
-        <FileTreeRootFolder onDelete={onDelete} />
-      </FileTreeInner>
-      <FileTreeModalDelete />
-      <FileTreeModalCreateFile />
-      <FileTreeModalCreateFolder />
-      <FileTreeModalError />
-    </FileTreeContext>
+    <div className="file-tree" ref={setFileTreeContainer}>
+      {fileTreeContainer && (
+        <FileTreeContext
+          refProviders={refProviders}
+          setRefProviderEnabled={setRefProviderEnabled}
+          setStartedFreeTrial={setStartedFreeTrial}
+          reindexReferences={reindexReferences}
+          onSelect={onSelect}
+          fileTreeContainer={fileTreeContainer}
+        >
+          {isConnected ? null : <div className="disconnected-overlay" />}
+          <FileTreeToolbar />
+          <FileTreeContextMenu />
+          <FileTreeInner>
+            <FileTreeRootFolder onDelete={onDelete} />
+          </FileTreeInner>
+          <FileTreeModalDelete />
+          <FileTreeModalCreateFile />
+          <FileTreeModalCreateFolder />
+          <FileTreeModalError />
+        </FileTreeContext>
+      )}
+    </div>
   )
 })
 

+ 78 - 95
services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx

@@ -1,102 +1,87 @@
-import { useRef, useEffect, useState, FC } from 'react'
+import { useEffect, useState, FC, useMemo } from 'react'
 import { useTranslation } from 'react-i18next'
 import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
-import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
+import { DndProvider, DragSourceMonitor, useDrag, useDrop } from 'react-dnd'
 import {
   HTML5Backend,
   getEmptyImage,
   NativeTypes,
 } from 'react-dnd-html5-backend'
-
 import {
   findAllInTreeOrThrow,
   findAllFolderIdsInFolders,
 } from '../util/find-in-tree'
-
 import { useFileTreeActionable } from './file-tree-actionable'
-import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
-import { useEditorContext } from '../../../shared/context/editor-context'
-
-// HACK ALERT
-// DnD binds drag and drop events on window and stop propagation if the dragged
-// item is not a DnD element. This break other drag and drop interfaces; in
-// particular in rich text.
-// This is a hacky workaround to avoid calling the DnD listeners when the
-// draggable or droppable element is not within a `dnd-container` element.
-const ModifiedBackend = (...args: any[]) => {
-  function isDndChild(elt: Element): boolean {
-    if (elt.getAttribute && elt.getAttribute('dnd-container')) return true
-    if (!elt.parentNode) return false
-    return isDndChild(elt.parentNode as Element)
-  }
-  // @ts-ignore
-  const instance = new HTML5Backend(...args)
-
-  const dragDropListeners = [
-    'handleTopDragStart',
-    'handleTopDragStartCapture',
-    'handleTopDragEndCapture',
-    'handleTopDragEnter',
-    'handleTopDragEnterCapture',
-    'handleTopDragLeaveCapture',
-    'handleTopDragOver',
-    'handleTopDragOverCapture',
-    'handleTopDrop',
-    'handleTopDropCapture',
-  ]
-
-  dragDropListeners.forEach(dragDropListener => {
-    const originalListener = instance[dragDropListener]
-    instance[dragDropListener] = (ev: Event, ...extraArgs: any[]) => {
-      if (isDndChild(ev.target as Element)) originalListener(ev, ...extraArgs)
-    }
-  })
-
-  return instance
-}
-
-const DndContext = createDndContext(ModifiedBackend)
+import { useEditorContext } from '@/shared/context/editor-context'
 
 const DRAGGABLE_TYPE = 'ENTITY'
-export const FileTreeDraggableProvider: FC = ({ children }) => {
-  const DndManager = useRef(DndContext)
+export const FileTreeDraggableProvider: FC<{
+  fileTreeContainer?: HTMLDivElement
+}> = ({ fileTreeContainer, children }) => {
+  const options = useMemo(
+    () => ({ rootElement: fileTreeContainer }),
+    [fileTreeContainer]
+  )
 
   return (
-    <DndProvider manager={DndManager.current.dragDropManager!}>
+    <DndProvider backend={HTML5Backend} options={options}>
       {children}
     </DndProvider>
   )
 }
 
+type DragObject = {
+  type: string
+  title: string
+  forbiddenFolderIds: Set<string>
+  draggedEntityIds: Set<string>
+}
+
+type DropResult = {
+  targetEntityId: string
+  dropEffect: DataTransfer['dropEffect']
+}
+
 export function useDraggable(draggedEntityId: string) {
   const { t } = useTranslation()
 
   const { permissionsLevel } = useEditorContext()
   const { fileTreeData } = useFileTreeData()
   const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
+  const { finishMoving } = useFileTreeActionable()
 
   const [isDraggable, setIsDraggable] = useState(true)
 
-  const item = { type: DRAGGABLE_TYPE }
-  const [{ isDragging, draggedEntityIds }, dragRef, preview] = useDrag({
-    item, // required, but overwritten by the return value of `begin`
-    begin: () => {
+  const [, dragRef, preview] = useDrag({
+    type: DRAGGABLE_TYPE,
+    item() {
       const draggedEntityIds = getDraggedEntityIds(
         isRootFolderSelected ? new Set() : selectedEntityIds,
         draggedEntityId
       )
+
       const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
-      const title = getDraggedTitle(draggedItems, t)
-      const forbiddenFolderIds = getForbiddenFolderIds(draggedItems)
-      return { ...item, title, forbiddenFolderIds, draggedEntityIds }
+
+      return {
+        type: DRAGGABLE_TYPE,
+        title: getDraggedTitle(draggedItems, t),
+        forbiddenFolderIds: getForbiddenFolderIds(draggedItems),
+        draggedEntityIds,
+      }
+    },
+    canDrag() {
+      return permissionsLevel !== 'readOnly' && isDraggable
+    },
+    end(item: DragObject, monitor: DragSourceMonitor<DragObject, DropResult>) {
+      if (monitor.didDrop()) {
+        const result = monitor.getDropResult()
+        if (result) {
+          finishMoving(result.targetEntityId, item.draggedEntityIds) // TODO: use result.dropEffect
+        }
+      }
     },
-    collect: monitor => ({
-      isDragging: !!monitor.isDragging(),
-      draggedEntityIds: monitor.getItem()?.draggedEntityIds,
-    }),
-    canDrag: () => permissionsLevel !== 'readOnly' && isDraggable,
-    end: () => item,
   })
 
   // remove the automatic preview as we're using a custom preview via
@@ -105,51 +90,49 @@ export function useDraggable(draggedEntityId: string) {
     preview(getEmptyImage())
   }, [preview])
 
-  return {
-    dragRef,
-    isDragging,
-    setIsDraggable,
-    draggedEntityIds,
-  }
+  return { dragRef, setIsDraggable }
 }
 
-export function useDroppable(droppedEntityId: string) {
-  const { finishMoving, setDroppedFiles, startUploadingDocOrFile } =
-    useFileTreeActionable()
+export function useDroppable(targetEntityId: string) {
+  const { setDroppedFiles, startUploadingDocOrFile } = useFileTreeActionable()
 
-  const [{ isOver }, dropRef] = useDrop<any, any, any>({
+  const [{ isOver }, dropRef] = useDrop({
     accept: [DRAGGABLE_TYPE, NativeTypes.FILE],
-    canDrop: (item, monitor) => {
-      const isOver = monitor.isOver({ shallow: true })
-      if (!isOver) return false
-      if (
+    canDrop(item: DragObject, monitor) {
+      if (!monitor.isOver({ shallow: true })) {
+        return false
+      }
+
+      return !(
         item.type === DRAGGABLE_TYPE &&
-        item.forbiddenFolderIds.has(droppedEntityId)
+        item.forbiddenFolderIds.has(targetEntityId)
       )
-        return false
-      return true
     },
-    drop: (item, monitor) => {
-      const didDropInChild = monitor.didDrop()
-      if (didDropInChild) return
+    drop(item, monitor) {
+      // monitor.didDrop() returns true if the drop was already handled by a nested child
+      if (monitor.didDrop()) {
+        return
+      }
+
+      // item(s) dragged within the file tree
       if (item.type === DRAGGABLE_TYPE) {
-        finishMoving(droppedEntityId, item.draggedEntityIds)
-      } else {
-        getDroppedFiles(item).then(files => {
-          setDroppedFiles({ files, targetFolderId: droppedEntityId })
-          startUploadingDocOrFile()
-        })
+        return { targetEntityId }
+      }
+
+      // native file(s) dragged in from outside
+      getDroppedFiles(item as unknown as DataTransfer).then(files => {
+        setDroppedFiles({ files, targetFolderId: targetEntityId })
+        startUploadingDocOrFile()
+      })
+    },
+    collect(monitor) {
+      return {
+        isOver: monitor.canDrop(),
       }
     },
-    collect: monitor => ({
-      isOver: monitor.canDrop(),
-    }),
   })
 
-  return {
-    dropRef,
-    isOver,
-  }
+  return { dropRef, isOver }
 }
 
 // Get the list of dragged entity ids. If the dragged entity is one of the

+ 10 - 12
services/web/frontend/js/features/ide-react/components/file-tree.tsx

@@ -31,17 +31,15 @@ export const FileTree = memo(function FileTree() {
   )
 
   return (
-    <div className="file-tree">
-      <FileTreeRoot
-        refProviders={refProviders}
-        reindexReferences={reindexReferences}
-        setRefProviderEnabled={setRefProviderEnabled}
-        setStartedFreeTrial={setStartedFreeTrial}
-        isConnected={isConnected || connectionState.reconnectAt !== null}
-        onInit={handleFileTreeInit}
-        onSelect={handleFileTreeSelect}
-        onDelete={handleFileTreeDelete}
-      />
-    </div>
+    <FileTreeRoot
+      refProviders={refProviders}
+      reindexReferences={reindexReferences}
+      setRefProviderEnabled={setRefProviderEnabled}
+      setStartedFreeTrial={setStartedFreeTrial}
+      isConnected={isConnected || connectionState.reconnectAt !== null}
+      onInit={handleFileTreeInit}
+      onSelect={handleFileTreeSelect}
+      onDelete={handleFileTreeDelete}
+    />
   )
 })

+ 2 - 2
services/web/package.json

@@ -324,8 +324,8 @@
     "react-bootstrap": "^0.33.1",
     "react-chartjs-2": "^5.0.1",
     "react-color": "^2.19.3",
-    "react-dnd": "^11.1.3",
-    "react-dnd-html5-backend": "^11.1.3",
+    "react-dnd": "^16.0.1",
+    "react-dnd-html5-backend": "^16.0.1",
     "react-dom": "^17.0.2",
     "react-error-boundary": "^2.3.1",
     "react-google-recaptcha": "^3.1.0",

+ 19 - 17
services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx

@@ -64,23 +64,25 @@ describe('<FileTreeRoot/>', function () {
     ]
 
     cy.mount(
-      <EditorProviders
-        rootFolder={rootFolder as any}
-        projectId="123abc"
-        rootDocId="456def"
-        features={{} as any}
-        permissionsLevel="owner"
-      >
-        <FileTreeRoot
-          refProviders={{}}
-          reindexReferences={cy.stub()}
-          setRefProviderEnabled={cy.stub()}
-          setStartedFreeTrial={cy.stub()}
-          onSelect={cy.stub()}
-          onInit={cy.stub()}
-          isConnected
-        />
-      </EditorProviders>
+      <div style={{ width: 400 }}>
+        <EditorProviders
+          rootFolder={rootFolder as any}
+          projectId="123abc"
+          rootDocId="456def"
+          features={{} as any}
+          permissionsLevel="owner"
+        >
+          <FileTreeRoot
+            refProviders={{}}
+            reindexReferences={cy.stub()}
+            setRefProviderEnabled={cy.stub()}
+            setStartedFreeTrial={cy.stub()}
+            onSelect={cy.stub()}
+            onInit={cy.stub()}
+            isConnected
+          />
+        </EditorProviders>
+      </div>
     )
 
     // as a proxy to check that the invalid entity has not been select we start

+ 51 - 45
services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx

@@ -26,21 +26,23 @@ describe('FileTree Delete Entity Flow', function () {
       ]
 
       cy.mount(
-        <EditorProviders
-          rootFolder={rootFolder as any}
-          projectId="123abc"
-          socket={new MockedSocket()}
-        >
-          <FileTreeRoot
-            refProviders={{}}
-            reindexReferences={cy.stub().as('reindexReferences')}
-            setRefProviderEnabled={cy.stub()}
-            setStartedFreeTrial={cy.stub()}
-            onSelect={cy.stub()}
-            onInit={cy.stub()}
-            isConnected
-          />
-        </EditorProviders>
+        <div style={{ width: 400 }}>
+          <EditorProviders
+            rootFolder={rootFolder as any}
+            projectId="123abc"
+            socket={new MockedSocket()}
+          >
+            <FileTreeRoot
+              refProviders={{}}
+              reindexReferences={cy.stub().as('reindexReferences')}
+              setRefProviderEnabled={cy.stub()}
+              setStartedFreeTrial={cy.stub()}
+              onSelect={cy.stub()}
+              onInit={cy.stub()}
+              isConnected
+            />
+          </EditorProviders>
+        </div>
       )
 
       cy.findByRole('treeitem', { name: 'main.tex' }).click()
@@ -151,21 +153,23 @@ describe('FileTree Delete Entity Flow', function () {
       ]
 
       cy.mount(
-        <EditorProviders
-          rootFolder={rootFolder as any}
-          projectId="123abc"
-          socket={new MockedSocket()}
-        >
-          <FileTreeRoot
-            refProviders={{}}
-            reindexReferences={cy.stub().as('reindexReferences')}
-            setRefProviderEnabled={cy.stub()}
-            setStartedFreeTrial={cy.stub()}
-            onSelect={cy.stub()}
-            onInit={cy.stub()}
-            isConnected
-          />
-        </EditorProviders>
+        <div style={{ width: 400 }}>
+          <EditorProviders
+            rootFolder={rootFolder as any}
+            projectId="123abc"
+            socket={new MockedSocket()}
+          >
+            <FileTreeRoot
+              refProviders={{}}
+              reindexReferences={cy.stub().as('reindexReferences')}
+              setRefProviderEnabled={cy.stub()}
+              setStartedFreeTrial={cy.stub()}
+              onSelect={cy.stub()}
+              onInit={cy.stub()}
+              isConnected
+            />
+          </EditorProviders>
+        </div>
       )
 
       cy.findByRole('button', { name: 'Expand' }).click()
@@ -212,21 +216,23 @@ describe('FileTree Delete Entity Flow', function () {
       ]
 
       cy.mount(
-        <EditorProviders
-          rootFolder={rootFolder as any}
-          projectId="123abc"
-          socket={new MockedSocket()}
-        >
-          <FileTreeRoot
-            refProviders={{}}
-            reindexReferences={cy.stub().as('reindexReferences')}
-            setRefProviderEnabled={cy.stub()}
-            setStartedFreeTrial={cy.stub()}
-            onSelect={cy.stub()}
-            onInit={cy.stub()}
-            isConnected
-          />
-        </EditorProviders>
+        <div style={{ width: 400 }}>
+          <EditorProviders
+            rootFolder={rootFolder as any}
+            projectId="123abc"
+            socket={new MockedSocket()}
+          >
+            <FileTreeRoot
+              refProviders={{}}
+              reindexReferences={cy.stub().as('reindexReferences')}
+              setRefProviderEnabled={cy.stub()}
+              setStartedFreeTrial={cy.stub()}
+              onSelect={cy.stub()}
+              onInit={cy.stub()}
+              isConnected
+            />
+          </EditorProviders>
+        </div>
       )
 
       // select two files

+ 17 - 15
services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx

@@ -33,21 +33,23 @@ describe('FileTree Rename Entity Flow', function () {
     ]
 
     cy.mount(
-      <EditorProviders
-        rootFolder={rootFolder as any}
-        projectId="123abc"
-        socket={new MockedSocket()}
-      >
-        <FileTreeRoot
-          refProviders={{}}
-          reindexReferences={cy.stub()}
-          setRefProviderEnabled={cy.stub()}
-          setStartedFreeTrial={cy.stub()}
-          onSelect={cy.stub().as('onSelect')}
-          onInit={cy.stub()}
-          isConnected
-        />
-      </EditorProviders>
+      <div style={{ width: 400 }}>
+        <EditorProviders
+          rootFolder={rootFolder as any}
+          projectId="123abc"
+          socket={new MockedSocket()}
+        >
+          <FileTreeRoot
+            refProviders={{}}
+            reindexReferences={cy.stub()}
+            setRefProviderEnabled={cy.stub()}
+            setStartedFreeTrial={cy.stub()}
+            onSelect={cy.stub().as('onSelect')}
+            onInit={cy.stub()}
+            isConnected
+          />
+        </EditorProviders>
+      </div>
     )
   })
 

+ 15 - 4
services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx

@@ -1,9 +1,12 @@
-import { ComponentProps, FC, useRef } from 'react'
+import { ComponentProps, FC, useRef, useState } from 'react'
 import FileTreeContext from '@/features/file-tree/components/file-tree-context'
 
 export const FileTreeProvider: FC<{
   refProviders?: Record<string, boolean>
 }> = ({ children, refProviders = {} }) => {
+  const [fileTreeContainer, setFileTreeContainer] =
+    useState<HTMLDivElement | null>(null)
+
   const propsRef =
     useRef<Omit<ComponentProps<typeof FileTreeContext>, 'refProviders'>>()
 
@@ -17,8 +20,16 @@ export const FileTreeProvider: FC<{
   }
 
   return (
-    <FileTreeContext refProviders={refProviders} {...propsRef.current}>
-      <>{children}</>
-    </FileTreeContext>
+    <div ref={setFileTreeContainer}>
+      {fileTreeContainer && (
+        <FileTreeContext
+          refProviders={refProviders}
+          fileTreeContainer={fileTreeContainer}
+          {...propsRef.current}
+        >
+          <>{children}</>
+        </FileTreeContext>
+      )}
+    </div>
   )
 }

+ 3 - 0
services/web/webpack.config.js

@@ -240,6 +240,9 @@ module.exports = {
     extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.json'],
     fallback: {
       events: require.resolve('events'),
+      // for react-dnd + React 17
+      'react/jsx-runtime': 'react/jsx-runtime.js',
+      'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js',
     },
   },