mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-09 06:06:01 +00:00
Merge pull request #3717 from overleaf/ae-file-tree-context-minimal
Wrap file tree context functions in useCallback GitOrigin-RevId: 86fbaf4f168bbe31f474bdea38d2cd09b69c950f
This commit is contained in:
parent
8001e55b88
commit
623d77cef6
4 changed files with 270 additions and 196 deletions
|
@ -25,17 +25,17 @@ function FileTreeContext({
|
|||
projectId={projectId}
|
||||
hasWritePermissions={hasWritePermissions}
|
||||
>
|
||||
<FileTreeActionableProvider hasWritePermissions={hasWritePermissions}>
|
||||
<FileTreeMutableProvider rootFolder={rootFolder}>
|
||||
<FileTreeSelectableProvider
|
||||
hasWritePermissions={hasWritePermissions}
|
||||
rootDocId={rootDocId}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<FileTreeMutableProvider rootFolder={rootFolder}>
|
||||
<FileTreeSelectableProvider
|
||||
hasWritePermissions={hasWritePermissions}
|
||||
rootDocId={rootDocId}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<FileTreeActionableProvider hasWritePermissions={hasWritePermissions}>
|
||||
<FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
|
||||
</FileTreeSelectableProvider>
|
||||
</FileTreeMutableProvider>
|
||||
</FileTreeActionableProvider>
|
||||
</FileTreeActionableProvider>
|
||||
</FileTreeSelectableProvider>
|
||||
</FileTreeMutableProvider>
|
||||
</FileTreeMainProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import React, { createContext, useReducer, useContext } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useReducer,
|
||||
useContext
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { mapSeries } from '../../../infrastructure/promise'
|
||||
|
@ -131,47 +136,50 @@ export function useFileTreeActionable() {
|
|||
const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeMutable()
|
||||
const { selectedEntityIds } = useFileTreeSelectable()
|
||||
|
||||
function startRenaming() {
|
||||
const startRenaming = useCallback(() => {
|
||||
dispatch({ type: ACTION_TYPES.START_RENAME })
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
// update the entity with the new name immediately in the tree, but revert to
|
||||
// the old name if the sync fails
|
||||
function finishRenaming(newName) {
|
||||
const selectedEntityId = Array.from(selectedEntityIds)[0]
|
||||
const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
|
||||
const oldName = found.entity.name
|
||||
if (newName === oldName) {
|
||||
return dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
}
|
||||
|
||||
const error = validateRename(fileTreeData, found, newName)
|
||||
if (error) return dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
dispatchRename(selectedEntityId, newName)
|
||||
return syncRename(projectId, found.type, found.entity._id, newName).catch(
|
||||
error => {
|
||||
dispatchRename(selectedEntityId, oldName)
|
||||
// The state from this error action isn't used anywhere right now
|
||||
// but we need to handle the error for linting
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
const finishRenaming = useCallback(
|
||||
newName => {
|
||||
const selectedEntityId = Array.from(selectedEntityIds)[0]
|
||||
const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
|
||||
const oldName = found.entity.name
|
||||
if (newName === oldName) {
|
||||
return dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const error = validateRename(fileTreeData, found, newName)
|
||||
if (error) return dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
dispatchRename(selectedEntityId, newName)
|
||||
return syncRename(projectId, found.type, found.entity._id, newName).catch(
|
||||
error => {
|
||||
dispatchRename(selectedEntityId, oldName)
|
||||
// The state from this error action isn't used anywhere right now
|
||||
// but we need to handle the error for linting
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
}
|
||||
)
|
||||
},
|
||||
[dispatch, dispatchRename, fileTreeData, projectId, selectedEntityIds]
|
||||
)
|
||||
|
||||
// init deletion flow (this will open the delete modal).
|
||||
// A copy of the selected entities is set as `actionedEntities` so it is kept
|
||||
// unchanged as the entities are deleted and the selection is updated
|
||||
function startDeleting() {
|
||||
const startDeleting = useCallback(() => {
|
||||
const actionedEntities = Array.from(selectedEntityIds).map(
|
||||
entityId => findInTreeOrThrow(fileTreeData, entityId).entity
|
||||
)
|
||||
dispatch({ type: ACTION_TYPES.START_DELETE, actionedEntities })
|
||||
}
|
||||
}, [dispatch, fileTreeData, selectedEntityIds])
|
||||
|
||||
// deletes entities in serie. Tree will be updated via the socket event
|
||||
function finishDeleting() {
|
||||
const finishDeleting = useCallback(() => {
|
||||
dispatch({ type: ACTION_TYPES.DELETING })
|
||||
|
||||
return mapSeries(Array.from(selectedEntityIds), id => {
|
||||
|
@ -192,73 +200,86 @@ export function useFileTreeActionable() {
|
|||
// set an error and allow user to retry
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
})
|
||||
}
|
||||
}, [dispatch, fileTreeData, projectId, selectedEntityIds])
|
||||
|
||||
// moves entities. Tree is updated immediately and data are sync'd after.
|
||||
function finishMoving(toFolderId, draggedEntityIds) {
|
||||
dispatch({ type: ACTION_TYPES.MOVING })
|
||||
const finishMoving = useCallback(
|
||||
(toFolderId, draggedEntityIds) => {
|
||||
dispatch({ type: ACTION_TYPES.MOVING })
|
||||
|
||||
// find entities and filter out no-ops
|
||||
const founds = Array.from(draggedEntityIds)
|
||||
.map(draggedEntityId => findInTreeOrThrow(fileTreeData, draggedEntityId))
|
||||
.filter(found => found.parentFolderId !== toFolderId)
|
||||
// find entities and filter out no-ops
|
||||
const founds = Array.from(draggedEntityIds)
|
||||
.map(draggedEntityId =>
|
||||
findInTreeOrThrow(fileTreeData, draggedEntityId)
|
||||
)
|
||||
.filter(found => found.parentFolderId !== toFolderId)
|
||||
|
||||
// make sure all entities can be moved, return early otherwise
|
||||
const isMoveToRoot = toFolderId === fileTreeData._id
|
||||
const validationError = founds
|
||||
.map(found => validateMove(fileTreeData, toFolderId, found, isMoveToRoot))
|
||||
.find(error => error)
|
||||
if (validationError) {
|
||||
return dispatch({ type: ACTION_TYPES.ERROR, error: validationError })
|
||||
}
|
||||
// make sure all entities can be moved, return early otherwise
|
||||
const isMoveToRoot = toFolderId === fileTreeData._id
|
||||
const validationError = founds
|
||||
.map(found =>
|
||||
validateMove(fileTreeData, toFolderId, found, isMoveToRoot)
|
||||
)
|
||||
.find(error => error)
|
||||
if (validationError) {
|
||||
return dispatch({ type: ACTION_TYPES.ERROR, error: validationError })
|
||||
}
|
||||
|
||||
// dispatch moves immediately
|
||||
founds.forEach(found => dispatchMove(found.entity._id, toFolderId))
|
||||
// dispatch moves immediately
|
||||
founds.forEach(found => dispatchMove(found.entity._id, toFolderId))
|
||||
|
||||
// sync dispatched moves after
|
||||
return mapSeries(founds, found =>
|
||||
syncMove(projectId, found.type, found.entity._id, toFolderId)
|
||||
)
|
||||
.then(() => {
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
})
|
||||
}
|
||||
// sync dispatched moves after
|
||||
return mapSeries(founds, found =>
|
||||
syncMove(projectId, found.type, found.entity._id, toFolderId)
|
||||
)
|
||||
.then(() => {
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
})
|
||||
},
|
||||
[dispatch, dispatchMove, fileTreeData, projectId]
|
||||
)
|
||||
|
||||
function startCreatingFolder() {
|
||||
const startCreatingFolder = useCallback(() => {
|
||||
dispatch({ type: ACTION_TYPES.START_CREATE_FOLDER })
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
function finishCreatingEntity(entity) {
|
||||
const parentFolderId = getSelectedParentFolderId(
|
||||
fileTreeData,
|
||||
selectedEntityIds
|
||||
)
|
||||
const finishCreatingEntity = useCallback(
|
||||
entity => {
|
||||
const parentFolderId = getSelectedParentFolderId(
|
||||
fileTreeData,
|
||||
selectedEntityIds
|
||||
)
|
||||
|
||||
// check for duplicates and throw
|
||||
if (isNameUniqueInFolder(fileTreeData, parentFolderId, entity.name)) {
|
||||
return syncCreateEntity(projectId, parentFolderId, entity)
|
||||
} else {
|
||||
return Promise.reject(new DuplicateFilenameError())
|
||||
}
|
||||
}
|
||||
// check for duplicates and throw
|
||||
if (isNameUniqueInFolder(fileTreeData, parentFolderId, entity.name)) {
|
||||
return syncCreateEntity(projectId, parentFolderId, entity)
|
||||
} else {
|
||||
return Promise.reject(new DuplicateFilenameError())
|
||||
}
|
||||
},
|
||||
[fileTreeData, projectId, selectedEntityIds]
|
||||
)
|
||||
|
||||
function finishCreatingFolder(name) {
|
||||
dispatch({ type: ACTION_TYPES.CREATING_FOLDER })
|
||||
return finishCreatingEntity({ endpoint: 'folder', name })
|
||||
.then(() => {
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
})
|
||||
}
|
||||
const finishCreatingFolder = useCallback(
|
||||
name => {
|
||||
dispatch({ type: ACTION_TYPES.CREATING_FOLDER })
|
||||
return finishCreatingEntity({ endpoint: 'folder', name })
|
||||
.then(() => {
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
})
|
||||
},
|
||||
[dispatch, finishCreatingEntity]
|
||||
)
|
||||
|
||||
// bypass React file tree entirely; requesting the Angular new doc or file
|
||||
// modal instead
|
||||
function startCreatingDocOrFile() {
|
||||
const startCreatingDocOrFile = useCallback(() => {
|
||||
const parentFolderId = getSelectedParentFolderId(
|
||||
fileTreeData,
|
||||
selectedEntityIds
|
||||
|
@ -271,9 +292,9 @@ export function useFileTreeActionable() {
|
|||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [fileTreeData, selectedEntityIds])
|
||||
|
||||
function startUploadingDocOrFile() {
|
||||
const startUploadingDocOrFile = useCallback(() => {
|
||||
const parentFolderId = getSelectedParentFolderId(
|
||||
fileTreeData,
|
||||
selectedEntityIds
|
||||
|
@ -287,47 +308,55 @@ export function useFileTreeActionable() {
|
|||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [fileTreeData, selectedEntityIds])
|
||||
|
||||
function finishCreatingDocOrFile(entity) {
|
||||
return finishCreatingEntity(entity)
|
||||
.then(() => {
|
||||
// dispatch FileTreeReactBridge event to update the Angular modal
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('FileTreeReactBridge.openNewFileModal', {
|
||||
detail: {
|
||||
done: true
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(error => {
|
||||
// dispatch FileTreeReactBridge event to update the Angular modal with
|
||||
// an error
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('FileTreeReactBridge.openNewFileModal', {
|
||||
detail: {
|
||||
error: true,
|
||||
data: error.message
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
const finishCreatingDocOrFile = useCallback(
|
||||
entity =>
|
||||
finishCreatingEntity(entity)
|
||||
.then(() => {
|
||||
// dispatch FileTreeReactBridge event to update the Angular modal
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('FileTreeReactBridge.openNewFileModal', {
|
||||
detail: {
|
||||
done: true
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(error => {
|
||||
// dispatch FileTreeReactBridge event to update the Angular modal with
|
||||
// an error
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('FileTreeReactBridge.openNewFileModal', {
|
||||
detail: {
|
||||
error: true,
|
||||
data: error.message
|
||||
}
|
||||
})
|
||||
)
|
||||
}),
|
||||
[finishCreatingEntity]
|
||||
)
|
||||
|
||||
function finishCreatingDoc(entity) {
|
||||
entity.endpoint = 'doc'
|
||||
return finishCreatingDocOrFile(entity)
|
||||
}
|
||||
const finishCreatingDoc = useCallback(
|
||||
entity => {
|
||||
entity.endpoint = 'doc'
|
||||
return finishCreatingDocOrFile(entity)
|
||||
},
|
||||
[finishCreatingDocOrFile]
|
||||
)
|
||||
|
||||
function finishCreatingLinkedFile(entity) {
|
||||
entity.endpoint = 'linked_file'
|
||||
return finishCreatingDocOrFile(entity)
|
||||
}
|
||||
const finishCreatingLinkedFile = useCallback(
|
||||
entity => {
|
||||
entity.endpoint = 'linked_file'
|
||||
return finishCreatingDocOrFile(entity)
|
||||
},
|
||||
[finishCreatingDocOrFile]
|
||||
)
|
||||
|
||||
function cancel() {
|
||||
const cancel = useCallback(() => {
|
||||
dispatch({ type: ACTION_TYPES.CANCEL })
|
||||
}
|
||||
}, [dispatch])
|
||||
|
||||
return {
|
||||
canDelete: selectedEntityIds.size > 0,
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import React, { createContext, useReducer, useContext } from 'react'
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useReducer,
|
||||
useContext
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
|
@ -73,48 +78,66 @@ FileTreeMutableProvider.propTypes = {
|
|||
export function useFileTreeMutable() {
|
||||
const { fileTreeData, dispatch } = useContext(FileTreeMutableContext)
|
||||
|
||||
function dispatchCreateFolder(parentFolderId, entity) {
|
||||
entity.type = 'folder'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE_ENTITY,
|
||||
parentFolderId,
|
||||
entity
|
||||
})
|
||||
}
|
||||
const dispatchCreateFolder = useCallback(
|
||||
(parentFolderId, entity) => {
|
||||
entity.type = 'folder'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE_ENTITY,
|
||||
parentFolderId,
|
||||
entity
|
||||
})
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
function dispatchCreateDoc(parentFolderId, entity) {
|
||||
entity.type = 'doc'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE_ENTITY,
|
||||
parentFolderId,
|
||||
entity
|
||||
})
|
||||
}
|
||||
const dispatchCreateDoc = useCallback(
|
||||
(parentFolderId, entity) => {
|
||||
entity.type = 'doc'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE_ENTITY,
|
||||
parentFolderId,
|
||||
entity
|
||||
})
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
function dispatchCreateFile(parentFolderId, entity) {
|
||||
entity.type = 'fileRef'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE_ENTITY,
|
||||
parentFolderId,
|
||||
entity
|
||||
})
|
||||
}
|
||||
const dispatchCreateFile = useCallback(
|
||||
(parentFolderId, entity) => {
|
||||
entity.type = 'fileRef'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE_ENTITY,
|
||||
parentFolderId,
|
||||
entity
|
||||
})
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
function dispatchRename(id, newName) {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.RENAME,
|
||||
newName,
|
||||
id
|
||||
})
|
||||
}
|
||||
const dispatchRename = useCallback(
|
||||
(id, newName) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.RENAME,
|
||||
newName,
|
||||
id
|
||||
})
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
function dispatchDelete(id) {
|
||||
dispatch({ type: ACTION_TYPES.DELETE, id })
|
||||
}
|
||||
const dispatchDelete = useCallback(
|
||||
id => {
|
||||
dispatch({ type: ACTION_TYPES.DELETE, id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
function dispatchMove(entityId, toFolderId) {
|
||||
dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
|
||||
}
|
||||
const dispatchMove = useCallback(
|
||||
(entityId, toFolderId) => {
|
||||
dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return {
|
||||
fileTreeData,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useReducer,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
@ -163,40 +165,54 @@ export function useSelectableEntity(id) {
|
|||
|
||||
const isSelected = selectedEntityIds.has(id)
|
||||
|
||||
function selectOrMultiSelectEntity(ev) {
|
||||
const isMultiSelect = ev.ctrlKey || ev.metaKey
|
||||
const actionType = isMultiSelect
|
||||
? ACTION_TYPES.MULTI_SELECT
|
||||
: ACTION_TYPES.SELECT
|
||||
const selectOrMultiSelectEntity = useCallback(
|
||||
ev => {
|
||||
const isMultiSelect = ev.ctrlKey || ev.metaKey
|
||||
const actionType = isMultiSelect
|
||||
? ACTION_TYPES.MULTI_SELECT
|
||||
: ACTION_TYPES.SELECT
|
||||
|
||||
dispatch({ type: actionType, id })
|
||||
}
|
||||
dispatch({ type: actionType, id })
|
||||
},
|
||||
[dispatch, id]
|
||||
)
|
||||
|
||||
function handleClick(ev) {
|
||||
selectOrMultiSelectEntity(ev)
|
||||
}
|
||||
|
||||
function handleKeyPress(ev) {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
const handleClick = useCallback(
|
||||
ev => {
|
||||
selectOrMultiSelectEntity(ev)
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectOrMultiSelectEntity]
|
||||
)
|
||||
|
||||
function handleContextMenu(ev) {
|
||||
// make sure the right-clicked entity gets selected
|
||||
if (!selectedEntityIds.has(id)) selectOrMultiSelectEntity(ev)
|
||||
}
|
||||
const handleKeyPress = useCallback(
|
||||
ev => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
selectOrMultiSelectEntity(ev)
|
||||
}
|
||||
},
|
||||
[selectOrMultiSelectEntity]
|
||||
)
|
||||
|
||||
return {
|
||||
isSelected,
|
||||
props: {
|
||||
const handleContextMenu = useCallback(
|
||||
ev => {
|
||||
// make sure the right-clicked entity gets selected
|
||||
if (!selectedEntityIds.has(id)) selectOrMultiSelectEntity(ev)
|
||||
},
|
||||
[id, selectOrMultiSelectEntity, selectedEntityIds]
|
||||
)
|
||||
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
className: classNames({ selected: isSelected }),
|
||||
'aria-selected': isSelected,
|
||||
onClick: handleClick,
|
||||
onContextMenu: handleContextMenu,
|
||||
onKeyPress: handleKeyPress
|
||||
}
|
||||
}
|
||||
}),
|
||||
[handleClick, handleContextMenu, handleKeyPress, isSelected]
|
||||
)
|
||||
|
||||
return { isSelected, props }
|
||||
}
|
||||
|
||||
export function useFileTreeSelectable() {
|
||||
|
@ -204,13 +220,19 @@ export function useFileTreeSelectable() {
|
|||
FileTreeSelectableContext
|
||||
)
|
||||
|
||||
function select(id) {
|
||||
dispatch({ type: ACTION_TYPES.SELECT, id })
|
||||
}
|
||||
const select = useCallback(
|
||||
id => {
|
||||
dispatch({ type: ACTION_TYPES.SELECT, id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
function unselect(id) {
|
||||
dispatch({ type: ACTION_TYPES.UNSELECT, id })
|
||||
}
|
||||
const unselect = useCallback(
|
||||
id => {
|
||||
dispatch({ type: ACTION_TYPES.UNSELECT, id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return {
|
||||
selectedEntityIds,
|
||||
|
|
Loading…
Add table
Reference in a new issue