diff --git a/package-lock.json b/package-lock.json index da49982700..3aa50ebe6b 100644 --- a/package-lock.json +++ b/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" diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-context.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-context.tsx index 0832bce71f..95224aeb64 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-context.tsx +++ b/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<{ > - {children} + + {children} + diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-draggable-preview-layer.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-draggable-preview-layer.jsx index 25785d9b43..fffc4f5ae1 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-draggable-preview-layer.jsx +++ b/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, diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx index 7dfe1832de..506084df27 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder-list.jsx +++ b/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 => { diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx index a39e8b353d..93642db1cf 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-root.tsx +++ b/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(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 ( - - {isConnected ? null :
} - - - - - - - - - - +
+ {fileTreeContainer && ( + + {isConnected ? null :
} + + + + + + + + + + + )} +
) }) diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx index 32fd0974ab..df2c72b1c1 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.tsx +++ b/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 ( - + {children} ) } +type DragObject = { + type: string + title: string + forbiddenFolderIds: Set + draggedEntityIds: Set +} + +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) { + 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({ + const [{ isOver }, dropRef] = useDrop({ accept: [DRAGGABLE_TYPE, NativeTypes.FILE], - canDrop: (item, monitor) => { - const isOver = monitor.isOver({ shallow: true }) - if (!isOver) return false - if ( - item.type === DRAGGABLE_TYPE && - item.forbiddenFolderIds.has(droppedEntityId) - ) + canDrop(item: DragObject, monitor) { + if (!monitor.isOver({ shallow: true })) { return false - return true + } + + return !( + item.type === DRAGGABLE_TYPE && + item.forbiddenFolderIds.has(targetEntityId) + ) }, - 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 diff --git a/services/web/frontend/js/features/ide-react/components/file-tree.tsx b/services/web/frontend/js/features/ide-react/components/file-tree.tsx index b6f26eb86b..26c6e427bc 100644 --- a/services/web/frontend/js/features/ide-react/components/file-tree.tsx +++ b/services/web/frontend/js/features/ide-react/components/file-tree.tsx @@ -31,17 +31,15 @@ export const FileTree = memo(function FileTree() { ) return ( -
- -
+ ) }) diff --git a/services/web/package.json b/services/web/package.json index f91f99aadc..6892d23b9e 100644 --- a/services/web/package.json +++ b/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", diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx index 66d631e0f1..58a1fe98f2 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx +++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx @@ -64,23 +64,25 @@ describe('', function () { ] cy.mount( - - - +
+ + + +
) // as a proxy to check that the invalid entity has not been select we start diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx b/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx index 8de18eaef8..b5b17594ef 100644 --- a/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx +++ b/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx @@ -26,21 +26,23 @@ describe('FileTree Delete Entity Flow', function () { ] cy.mount( - - - +
+ + + +
) cy.findByRole('treeitem', { name: 'main.tex' }).click() @@ -151,21 +153,23 @@ describe('FileTree Delete Entity Flow', function () { ] cy.mount( - - - +
+ + + +
) cy.findByRole('button', { name: 'Expand' }).click() @@ -212,21 +216,23 @@ describe('FileTree Delete Entity Flow', function () { ] cy.mount( - - - +
+ + + +
) // select two files diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx b/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx index 757074437f..35bec4261d 100644 --- a/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx +++ b/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx @@ -33,21 +33,23 @@ describe('FileTree Rename Entity Flow', function () { ] cy.mount( - - - +
+ + + +
) }) diff --git a/services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx b/services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx index 5af4b81333..1fdcee55f1 100644 --- a/services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx +++ b/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 }> = ({ children, refProviders = {} }) => { + const [fileTreeContainer, setFileTreeContainer] = + useState(null) + const propsRef = useRef, 'refProviders'>>() @@ -17,8 +20,16 @@ export const FileTreeProvider: FC<{ } return ( - - <>{children} - +
+ {fileTreeContainer && ( + + <>{children} + + )} +
) } diff --git a/services/web/webpack.config.js b/services/web/webpack.config.js index 93e58f299f..de2b5345da 100644 --- a/services/web/webpack.config.js +++ b/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', }, },