Merge pull request #6210 from overleaf/ta-file-tree-data-in-context

Remove rootFolder from EditorContext

GitOrigin-RevId: 827b046046265ad2418eb91d5a69eae02aa8b5a4
This commit is contained in:
Timothée Alby
2022-01-11 09:04:56 +00:00
committed by Copybot
parent 392410390e
commit 24bd557485
26 changed files with 202 additions and 179 deletions
@@ -34,9 +34,6 @@ EditorContext.Provider.propTypes = {
insertSymbol: PropTypes.func,
isProjectOwner: PropTypes.bool,
isRestrictedTokenMember: PropTypes.bool,
rootFolder: PropTypes.shape({
children: PropTypes.arrayOf(PropTypes.shape({ type: PropTypes.string })),
}),
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}),
}
@@ -73,7 +70,6 @@ export function EditorProvider({ children, settings }) {
const [loading] = useScopeValue('state.loading')
const [projectName, setProjectName] = useScopeValue('project.name')
const [rootFolder] = useScopeValue('rootFolder', true)
const [permissionsLevel] = useScopeValue('permissionsLevel')
const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
@@ -135,7 +131,6 @@ export function EditorProvider({ children, settings }) {
permissionsLevel,
isProjectOwner: owner?._id === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder,
showSymbolPalette,
toggleSymbolPalette,
insertSymbol,
@@ -147,7 +142,6 @@ export function EditorProvider({ children, settings }) {
renameProject,
permissionsLevel,
owner?._id,
rootFolder,
showSymbolPalette,
toggleSymbolPalette,
insertSymbol,
@@ -0,0 +1,229 @@
import {
createContext,
useCallback,
useReducer,
useContext,
useEffect,
useMemo,
} from 'react'
import PropTypes from 'prop-types'
import useScopeValue from '../hooks/use-scope-value'
import {
renameInTree,
deleteInTree,
moveInTree,
createEntityInTree,
} from '../../features/file-tree/util/mutate-in-tree'
import { countFiles } from '../../features/file-tree/util/count-in-tree'
const FileTreeDataContext = createContext()
const fileTreeDataPropType = PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
docs: PropTypes.array.isRequired,
fileRefs: PropTypes.array.isRequired,
folders: PropTypes.array.isRequired,
})
FileTreeDataContext.Provider.propTypes = {
value: PropTypes.shape({
// fileTreeData is the up-to-date representation of the files list, updated
// by the file tree
fileTreeData: fileTreeDataPropType,
hasFolders: PropTypes.bool,
}),
}
const ACTION_TYPES = {
RENAME: 'RENAME',
RESET: 'RESET',
DELETE: 'DELETE',
MOVE: 'MOVE',
CREATE: 'CREATE',
}
function fileTreeMutableReducer({ fileTreeData }, action) {
switch (action.type) {
case ACTION_TYPES.RESET: {
const newFileTreeData = action.fileTreeData
return {
fileTreeData: newFileTreeData,
fileCount: countFiles(newFileTreeData),
}
}
case ACTION_TYPES.RENAME: {
const newFileTreeData = renameInTree(fileTreeData, action.id, {
newName: action.newName,
})
return {
fileTreeData: newFileTreeData,
fileCount: countFiles(newFileTreeData),
}
}
case ACTION_TYPES.DELETE: {
const newFileTreeData = deleteInTree(fileTreeData, action.id)
return {
fileTreeData: newFileTreeData,
fileCount: countFiles(newFileTreeData),
}
}
case ACTION_TYPES.MOVE: {
const newFileTreeData = moveInTree(
fileTreeData,
action.entityId,
action.toFolderId
)
return {
fileTreeData: newFileTreeData,
fileCount: countFiles(newFileTreeData),
}
}
case ACTION_TYPES.CREATE: {
const newFileTreeData = createEntityInTree(
fileTreeData,
action.parentFolderId,
action.entity
)
return {
fileTreeData: newFileTreeData,
fileCount: countFiles(newFileTreeData),
}
}
default: {
throw new Error(`Unknown mutable file tree action type: ${action.type}`)
}
}
}
const initialState = rootFolder => {
const fileTreeData = rootFolder?.[0]
return {
fileTreeData,
fileCount: countFiles(fileTreeData),
}
}
export function useFileTreeData(propTypes) {
const context = useContext(FileTreeDataContext)
if (!context) {
throw new Error(
'useFileTreeData is only available inside FileTreeDataProvider'
)
}
PropTypes.checkPropTypes(
propTypes,
context,
'data',
'FileTreeDataContext.Provider'
)
return context
}
export function FileTreeDataProvider({ children }) {
const [project] = useScopeValue('project', true)
const { rootFolder } = project || {}
const [{ fileTreeData, fileCount }, dispatch] = useReducer(
fileTreeMutableReducer,
rootFolder,
initialState
)
useEffect(() => {
dispatch({
type: ACTION_TYPES.RESET,
fileTreeData: rootFolder?.[0],
})
}, [rootFolder])
const dispatchCreateFolder = useCallback((parentFolderId, entity) => {
entity.type = 'folder'
dispatch({
type: ACTION_TYPES.CREATE,
parentFolderId,
entity,
})
}, [])
const dispatchCreateDoc = useCallback((parentFolderId, entity) => {
entity.type = 'doc'
dispatch({
type: ACTION_TYPES.CREATE,
parentFolderId,
entity,
})
}, [])
const dispatchCreateFile = useCallback((parentFolderId, entity) => {
entity.type = 'fileRef'
dispatch({
type: ACTION_TYPES.CREATE,
parentFolderId,
entity,
})
}, [])
const dispatchRename = useCallback((id, newName) => {
dispatch({
type: ACTION_TYPES.RENAME,
newName,
id,
})
}, [])
const dispatchDelete = useCallback(id => {
dispatch({ type: ACTION_TYPES.DELETE, id })
}, [])
const dispatchMove = useCallback((entityId, toFolderId) => {
dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
}, [])
const value = useMemo(() => {
return {
dispatchCreateDoc,
dispatchCreateFile,
dispatchCreateFolder,
dispatchDelete,
dispatchMove,
dispatchRename,
fileCount,
fileTreeData,
hasFolders: fileTreeData?.folders.length > 0,
}
}, [
dispatchCreateDoc,
dispatchCreateFile,
dispatchCreateFolder,
dispatchDelete,
dispatchMove,
dispatchRename,
fileCount,
fileTreeData,
])
return (
<FileTreeDataContext.Provider value={value}>
{children}
</FileTreeDataContext.Provider>
)
}
FileTreeDataProvider.propTypes = {
children: PropTypes.any,
}
@@ -1,17 +1,9 @@
import { createContext, useContext } from 'react'
import { createContext, useContext, useMemo } from 'react'
import PropTypes from 'prop-types'
import useScopeValue from '../hooks/use-scope-value'
const ProjectContext = createContext()
const fileTreeDataPropType = PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
docs: PropTypes.array.isRequired,
fileRefs: PropTypes.array.isRequired,
folders: PropTypes.array.isRequired,
})
ProjectContext.Provider.propTypes = {
value: PropTypes.shape({
_id: PropTypes.string.isRequired,
@@ -44,7 +36,6 @@ ProjectContext.Provider.propTypes = {
_id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
}),
rootFolder: PropTypes.arrayOf(fileTreeDataPropType),
}),
}
@@ -67,13 +58,24 @@ export function useProjectContext(propTypes) {
return context
}
// when the provider is created the project is still not added to the Angular
// scope. A few props are populated to prevent errors in existing React
// components
const projectFallback = {
_id: window.project_id,
name: '',
features: {},
}
export function ProjectProvider({ children }) {
const [project] = useScopeValue('project', true)
// when the provider is created the project is still not added to the Angular scope.
// Name is also populated to prevent errors in existing React components
const value = project || { _id: window.project_id, name: '', features: {} }
const value = useMemo(() => {
return {
...projectFallback,
...project,
}
}, [project])
return (
<ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
)
@@ -10,6 +10,7 @@ import { DetachProvider } from './detach-context'
import { ChatProvider } from '../../features/chat/context/chat-context'
import { ProjectProvider } from './project-context'
import { SplitTestProvider } from './split-test-context'
import { FileTreeDataProvider } from './file-tree-data-context'
export function ContextRoot({ children, ide, settings }) {
return (
@@ -17,15 +18,17 @@ export function ContextRoot({ children, ide, settings }) {
<IdeProvider ide={ide}>
<UserProvider>
<ProjectProvider>
<EditorProvider settings={settings}>
<DetachProvider>
<LayoutProvider>
<CompileProvider>
<ChatProvider>{children}</ChatProvider>
</CompileProvider>
</LayoutProvider>
</DetachProvider>
</EditorProvider>
<FileTreeDataProvider>
<EditorProvider settings={settings}>
<DetachProvider>
<LayoutProvider>
<CompileProvider>
<ChatProvider>{children}</ChatProvider>
</CompileProvider>
</LayoutProvider>
</DetachProvider>
</EditorProvider>
</FileTreeDataProvider>
</ProjectProvider>
</UserProvider>
</IdeProvider>