Upgrade react-dnd (#16753)

GitOrigin-RevId: 5a62bed823b716a6e0d6d3aa57ee187d161f3346
This commit is contained in:
Alf Eaton 2024-02-05 11:44:05 +00:00 committed by Copybot
parent 34f34c02e3
commit 6dc7ced2df
13 changed files with 311 additions and 277 deletions

147
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,102 +1,87 @@
import { useRef, useEffect, useState, FC } from 'react' import { useEffect, useState, FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles' import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd' import { DndProvider, DragSourceMonitor, useDrag, useDrop } from 'react-dnd'
import { import {
HTML5Backend, HTML5Backend,
getEmptyImage, getEmptyImage,
NativeTypes, NativeTypes,
} from 'react-dnd-html5-backend' } from 'react-dnd-html5-backend'
import { import {
findAllInTreeOrThrow, findAllInTreeOrThrow,
findAllFolderIdsInFolders, findAllFolderIdsInFolders,
} from '../util/find-in-tree' } from '../util/find-in-tree'
import { useFileTreeActionable } from './file-tree-actionable' 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 { useFileTreeSelectable } from '../contexts/file-tree-selectable'
import { useEditorContext } from '../../../shared/context/editor-context' 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)
const DRAGGABLE_TYPE = 'ENTITY' const DRAGGABLE_TYPE = 'ENTITY'
export const FileTreeDraggableProvider: FC = ({ children }) => { export const FileTreeDraggableProvider: FC<{
const DndManager = useRef(DndContext) fileTreeContainer?: HTMLDivElement
}> = ({ fileTreeContainer, children }) => {
const options = useMemo(
() => ({ rootElement: fileTreeContainer }),
[fileTreeContainer]
)
return ( return (
<DndProvider manager={DndManager.current.dragDropManager!}> <DndProvider backend={HTML5Backend} options={options}>
{children} {children}
</DndProvider> </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) { export function useDraggable(draggedEntityId: string) {
const { t } = useTranslation() const { t } = useTranslation()
const { permissionsLevel } = useEditorContext() const { permissionsLevel } = useEditorContext()
const { fileTreeData } = useFileTreeData() const { fileTreeData } = useFileTreeData()
const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable() const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
const { finishMoving } = useFileTreeActionable()
const [isDraggable, setIsDraggable] = useState(true) const [isDraggable, setIsDraggable] = useState(true)
const item = { type: DRAGGABLE_TYPE } const [, dragRef, preview] = useDrag({
const [{ isDragging, draggedEntityIds }, dragRef, preview] = useDrag({ type: DRAGGABLE_TYPE,
item, // required, but overwritten by the return value of `begin` item() {
begin: () => {
const draggedEntityIds = getDraggedEntityIds( const draggedEntityIds = getDraggedEntityIds(
isRootFolderSelected ? new Set() : selectedEntityIds, isRootFolderSelected ? new Set() : selectedEntityIds,
draggedEntityId draggedEntityId
) )
const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds) const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
const title = getDraggedTitle(draggedItems, t)
const forbiddenFolderIds = getForbiddenFolderIds(draggedItems) return {
return { ...item, title, forbiddenFolderIds, draggedEntityIds } 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 // remove the automatic preview as we're using a custom preview via
@ -105,51 +90,49 @@ export function useDraggable(draggedEntityId: string) {
preview(getEmptyImage()) preview(getEmptyImage())
}, [preview]) }, [preview])
return { return { dragRef, setIsDraggable }
dragRef,
isDragging,
setIsDraggable,
draggedEntityIds,
}
} }
export function useDroppable(droppedEntityId: string) { export function useDroppable(targetEntityId: string) {
const { finishMoving, setDroppedFiles, startUploadingDocOrFile } = const { setDroppedFiles, startUploadingDocOrFile } = useFileTreeActionable()
useFileTreeActionable()
const [{ isOver }, dropRef] = useDrop<any, any, any>({ const [{ isOver }, dropRef] = useDrop({
accept: [DRAGGABLE_TYPE, NativeTypes.FILE], accept: [DRAGGABLE_TYPE, NativeTypes.FILE],
canDrop: (item, monitor) => { canDrop(item: DragObject, monitor) {
const isOver = monitor.isOver({ shallow: true }) if (!monitor.isOver({ shallow: true })) {
if (!isOver) return false
if (
item.type === DRAGGABLE_TYPE &&
item.forbiddenFolderIds.has(droppedEntityId)
)
return false return false
return true }
return !(
item.type === DRAGGABLE_TYPE &&
item.forbiddenFolderIds.has(targetEntityId)
)
}, },
drop: (item, monitor) => { drop(item, monitor) {
const didDropInChild = monitor.didDrop() // monitor.didDrop() returns true if the drop was already handled by a nested child
if (didDropInChild) return if (monitor.didDrop()) {
return
}
// item(s) dragged within the file tree
if (item.type === DRAGGABLE_TYPE) { if (item.type === DRAGGABLE_TYPE) {
finishMoving(droppedEntityId, item.draggedEntityIds) return { targetEntityId }
} else { }
getDroppedFiles(item).then(files => {
setDroppedFiles({ files, targetFolderId: droppedEntityId }) // native file(s) dragged in from outside
getDroppedFiles(item as unknown as DataTransfer).then(files => {
setDroppedFiles({ files, targetFolderId: targetEntityId })
startUploadingDocOrFile() startUploadingDocOrFile()
}) })
},
collect(monitor) {
return {
isOver: monitor.canDrop(),
} }
}, },
collect: monitor => ({
isOver: monitor.canDrop(),
}),
}) })
return { return { dropRef, isOver }
dropRef,
isOver,
}
} }
// Get the list of dragged entity ids. If the dragged entity is one of the // Get the list of dragged entity ids. If the dragged entity is one of the

View file

@ -31,7 +31,6 @@ export const FileTree = memo(function FileTree() {
) )
return ( return (
<div className="file-tree">
<FileTreeRoot <FileTreeRoot
refProviders={refProviders} refProviders={refProviders}
reindexReferences={reindexReferences} reindexReferences={reindexReferences}
@ -42,6 +41,5 @@ export const FileTree = memo(function FileTree() {
onSelect={handleFileTreeSelect} onSelect={handleFileTreeSelect}
onDelete={handleFileTreeDelete} onDelete={handleFileTreeDelete}
/> />
</div>
) )
}) })

View file

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

View file

@ -64,6 +64,7 @@ describe('<FileTreeRoot/>', function () {
] ]
cy.mount( cy.mount(
<div style={{ width: 400 }}>
<EditorProviders <EditorProviders
rootFolder={rootFolder as any} rootFolder={rootFolder as any}
projectId="123abc" projectId="123abc"
@ -81,6 +82,7 @@ describe('<FileTreeRoot/>', function () {
isConnected isConnected
/> />
</EditorProviders> </EditorProviders>
</div>
) )
// as a proxy to check that the invalid entity has not been select we start // as a proxy to check that the invalid entity has not been select we start

View file

@ -26,6 +26,7 @@ describe('FileTree Delete Entity Flow', function () {
] ]
cy.mount( cy.mount(
<div style={{ width: 400 }}>
<EditorProviders <EditorProviders
rootFolder={rootFolder as any} rootFolder={rootFolder as any}
projectId="123abc" projectId="123abc"
@ -41,6 +42,7 @@ describe('FileTree Delete Entity Flow', function () {
isConnected isConnected
/> />
</EditorProviders> </EditorProviders>
</div>
) )
cy.findByRole('treeitem', { name: 'main.tex' }).click() cy.findByRole('treeitem', { name: 'main.tex' }).click()
@ -151,6 +153,7 @@ describe('FileTree Delete Entity Flow', function () {
] ]
cy.mount( cy.mount(
<div style={{ width: 400 }}>
<EditorProviders <EditorProviders
rootFolder={rootFolder as any} rootFolder={rootFolder as any}
projectId="123abc" projectId="123abc"
@ -166,6 +169,7 @@ describe('FileTree Delete Entity Flow', function () {
isConnected isConnected
/> />
</EditorProviders> </EditorProviders>
</div>
) )
cy.findByRole('button', { name: 'Expand' }).click() cy.findByRole('button', { name: 'Expand' }).click()
@ -212,6 +216,7 @@ describe('FileTree Delete Entity Flow', function () {
] ]
cy.mount( cy.mount(
<div style={{ width: 400 }}>
<EditorProviders <EditorProviders
rootFolder={rootFolder as any} rootFolder={rootFolder as any}
projectId="123abc" projectId="123abc"
@ -227,6 +232,7 @@ describe('FileTree Delete Entity Flow', function () {
isConnected isConnected
/> />
</EditorProviders> </EditorProviders>
</div>
) )
// select two files // select two files

View file

@ -33,6 +33,7 @@ describe('FileTree Rename Entity Flow', function () {
] ]
cy.mount( cy.mount(
<div style={{ width: 400 }}>
<EditorProviders <EditorProviders
rootFolder={rootFolder as any} rootFolder={rootFolder as any}
projectId="123abc" projectId="123abc"
@ -48,6 +49,7 @@ describe('FileTree Rename Entity Flow', function () {
isConnected isConnected
/> />
</EditorProviders> </EditorProviders>
</div>
) )
}) })

View file

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

View file

@ -240,6 +240,9 @@ module.exports = {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.json'], extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.json'],
fallback: { fallback: {
events: require.resolve('events'), 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',
}, },
}, },