Allow files to be dragged from the file tree into the editor (#15028)

GitOrigin-RevId: f926666c032d1398a0e3f72a298116a3c7a9cd75
This commit is contained in:
Alf Eaton 2024-05-08 11:39:18 +01:00 committed by Copybot
parent cb297e342a
commit c1c098e2f9
12 changed files with 194 additions and 20 deletions

View file

@ -9,9 +9,11 @@ import iconTypeFromName from '../util/icon-type-from-name'
import classnames from 'classnames'
function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
const type = isFile ? 'file' : 'doc'
const { isSelected, props: selectableEntityProps } = useSelectableEntity(
id,
isFile ? 'file' : 'doc'
type
)
return (
@ -26,6 +28,7 @@ function FileTreeDoc({ name, id, isFile, isLinkedFile }) {
<FileTreeItemInner
id={id}
name={name}
type={type}
isSelected={isSelected}
icons={<FileTreeIcon isLinkedFile={isLinkedFile} name={name} />}
/>

View file

@ -80,6 +80,7 @@ function FileTreeFolder({ name, id, folders, docs, files }) {
<FileTreeItemInner
id={id}
name={name}
type="folder"
isSelected={isSelected}
icons={icons}
/>

View file

@ -15,11 +15,13 @@ import { useDragDropManager } from 'react-dnd'
function FileTreeItemInner({
id,
name,
type,
isSelected,
icons,
}: {
id: string
name: string
type: string
isSelected: boolean
icons?: ReactNode
}) {
@ -76,6 +78,8 @@ function FileTreeItemInner({
ref={dragRef}
draggable={!isRenaming}
onContextMenu={handleContextMenu}
data-file-id={id}
data-file-type={type}
>
<div
className="entity-name entity-name-react"

View file

@ -17,6 +17,7 @@ import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
import FileTreeInner from './file-tree-inner'
import { useDragLayer } from 'react-dnd'
import classnames from 'classnames'
import { pathInFolder } from '@/features/file-tree/util/path'
const FileTreeRoot = React.memo<{
onSelect: () => void
@ -43,6 +44,41 @@ const FileTreeRoot = React.memo<{
const { fileTreeData } = useFileTreeData()
const isReady = Boolean(projectId && fileTreeData)
useEffect(() => {
if (fileTreeContainer) {
const listener = (event: DragEvent) => {
if (event.dataTransfer) {
// store the dragged entity in dataTransfer
const { dataset } = event.target as HTMLDivElement
if (
dataset.fileId &&
dataset.fileType &&
dataset.fileType !== 'folder'
) {
event.dataTransfer.setData(
'application/x-overleaf-file-id',
dataset.fileId
)
const filePath = pathInFolder(fileTreeData, dataset.fileId)
if (filePath) {
event.dataTransfer.setData(
'application/x-overleaf-file-path',
filePath
)
}
}
}
}
fileTreeContainer.addEventListener('dragstart', listener)
return () => {
fileTreeContainer.removeEventListener('dragstart', listener)
}
}
}, [fileTreeContainer, fileTreeData])
useEffect(() => {
if (isReady) onInit()
}, [isReady, onInit])

View file

@ -22,6 +22,7 @@ type FigureModalState = {
includeLabel: boolean
error?: string
pastedImageData?: PastedImageData
selectedItemId?: string
}
type FigureModalStateUpdate = Partial<FigureModalState>

View file

@ -70,8 +70,16 @@ const FigureModalContent = () => {
const listener = useCallback(
(event: Event) => {
const { detail: source } = event as CustomEvent<FigureModalSource>
dispatch({ source })
const { detail } = event as CustomEvent<{
source: FigureModalSource
fileId?: string
filePath?: string
}>
dispatch({
source: detail.source,
selectedItemId: detail.fileId,
getPath: detail.filePath ? async () => detail.filePath! : undefined,
})
},
[dispatch]
)

View file

@ -12,7 +12,7 @@ export const FigureModalCurrentProjectSource: FC = () => {
() => filterFiles(rootFolder)?.filter(isImageFile),
[rootFolder]
)
const { dispatch } = useFigureModalContext()
const { dispatch, selectedItemId } = useFigureModalContext()
const noFiles = files?.length === 0
return (
<Select
@ -20,6 +20,11 @@ export const FigureModalCurrentProjectSource: FC = () => {
itemToString={file => (file ? file.name : '')}
itemToSubtitle={item => item?.path ?? ''}
itemToKey={item => item.id}
defaultItem={
files && selectedItemId
? files.find(item => item.id === selectedItemId)
: undefined
}
defaultText={
noFiles
? t('no_image_files_found')

View file

@ -16,7 +16,7 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
emitToolbarEvent(view, `toolbar-figure-modal-${sourceName}`)
window.dispatchEvent(
new CustomEvent('figure-modal:open', {
detail: source,
detail: { source },
})
)
},

View file

@ -0,0 +1,89 @@
import { EditorView } from '@codemirror/view'
import { hasImageExtension } from '@/features/source-editor/utils/file'
import { FigureModalSource } from '@/features/source-editor/components/figure-modal/figure-modal-context'
import { EditorSelection } from '@codemirror/state'
export const fileTreeItemDrop = () =>
EditorView.domEventHandlers({
dragover(event) {
// TODO: detect a drag from the file tree?
if (event.dataTransfer) {
event.preventDefault()
}
},
drop(event, view) {
if (event.dataTransfer) {
const fileId = event.dataTransfer.getData(
'application/x-overleaf-file-id'
)
const filePath = event.dataTransfer.getData(
'application/x-overleaf-file-path'
)
if (fileId && filePath) {
event.preventDefault()
const pos = view.posAtCoords(event)
if (pos !== null) {
handleDroppedFile(view, pos, fileId, filePath)
}
}
}
},
})
const withoutExtension = (filename: string) =>
filename.substring(0, filename.lastIndexOf('.'))
const handleDroppedFile = (
view: EditorView,
pos: number,
fileId: string,
filePath: string
) => {
if (filePath.endsWith('.bib')) {
view.focus()
const insert = `\\bibliography{${withoutExtension(filePath)}}`
view.dispatch({
changes: { from: pos, insert },
selection: EditorSelection.cursor(pos + insert.length),
})
return
}
if (filePath.endsWith('.tex')) {
view.focus()
const insert = `\\input{${withoutExtension(filePath)}}`
view.dispatch({
changes: { from: pos, insert },
selection: EditorSelection.cursor(pos + insert.length),
})
return
}
if (hasImageExtension(filePath)) {
view.focus()
view.dispatch({
selection: EditorSelection.cursor(pos),
})
window.dispatchEvent(
new CustomEvent('figure-modal:open', {
detail: {
source: FigureModalSource.FILE_TREE,
fileId,
filePath,
},
})
)
return
}
return null
}

View file

@ -46,7 +46,8 @@ import { effectListeners } from './effect-listeners'
import { highlightSpecialChars } from './highlight-special-chars'
import { toolbarPanel } from './toolbar/toolbar-panel'
import { geometryChangeEvent } from './geometry-change-event'
import { docName } from '@/features/source-editor/extensions/doc-name'
import { docName } from './doc-name'
import { fileTreeItemDrop } from './file-tree-item-drop'
const moduleExtensions: Array<() => Extension> = importOverleafModules(
'sourceEditorExtensions'
@ -138,4 +139,5 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
thirdPartyExtensions(),
effectListeners(),
geometryChangeEvent(),
fileTreeItemDrop(),
]

View file

@ -49,8 +49,8 @@ export const filterFolders = filterByType('folder')
const IMAGE_FILE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'pdf']
export function isImageFile(file: File) {
const parts = file.name.split('.')
export const hasImageExtension = (filename: string) => {
const parts = filename.split('.')
if (parts.length < 2) {
return false
}
@ -58,11 +58,10 @@ export function isImageFile(file: File) {
return IMAGE_FILE_EXTENSIONS.includes(extension)
}
export function isImageEntity(file: Entity | OutputEntity) {
const parts = file.path.split('.')
if (parts.length < 2) {
return false
}
const extension = parts[parts.length - 1].toLowerCase()
return IMAGE_FILE_EXTENSIONS.includes(extension)
export function isImageFile(file: File) {
return hasImageExtension(file.name)
}
export function isImageEntity(file: Entity | OutputEntity) {
return hasImageExtension(file.path)
}

View file

@ -9,7 +9,13 @@ describe('<FileTreeitemInner />', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected={false} />,
<FileTreeitemInner
id="123abc"
name="bar.tex"
isSelected={false}
type="doc"
/>
,
</FileTreeProvider>
</EditorProviders>
)
@ -23,7 +29,12 @@ describe('<FileTreeitemInner />', function () {
cy.mount(
<EditorProviders permissionsLevel="readOnly">
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeitemInner
id="123abc"
name="bar.tex"
isSelected
type="doc"
/>
<FileTreeContextMenu />
</FileTreeProvider>
</EditorProviders>
@ -37,7 +48,12 @@ describe('<FileTreeitemInner />', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeitemInner
id="123abc"
name="bar.tex"
isSelected
type="doc"
/>
<FileTreeContextMenu />
</FileTreeProvider>
</EditorProviders>
@ -60,7 +76,12 @@ describe('<FileTreeitemInner />', function () {
cy.mount(
<EditorProviders>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeitemInner
id="123abc"
name="bar.tex"
isSelected
type="doc"
/>
</FileTreeProvider>
</EditorProviders>
)
@ -83,7 +104,12 @@ describe('<FileTreeitemInner />', function () {
cy.mount(
<EditorProviders rootDocId="123abc" rootFolder={rootFolder as any}>
<FileTreeProvider>
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeitemInner
id="123abc"
name="bar.tex"
isSelected
type="doc"
/>
<FileTreeContextMenu />
</FileTreeProvider>
</EditorProviders>