Upgrade react-dnd (#16753)

GitOrigin-RevId: 5a62bed823b716a6e0d6d3aa57ee187d161f3346
This commit is contained in:
Alf Eaton
2024-02-06 09:03:31 +00:00
committed by Copybot
parent 34f34c02e3
commit 6dc7ced2df
13 changed files with 311 additions and 277 deletions
@@ -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>
@@ -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,
@@ -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 => {
@@ -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>
)
})
@@ -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 (
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
@@ -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
View File
@@ -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",
@@ -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
@@ -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
@@ -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>
)
})
@@ -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
View File
@@ -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',
},
},