Просмотр исходного кода

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

Remove rootFolder from EditorContext

GitOrigin-RevId: 827b046046265ad2418eb91d5a69eae02aa8b5a4
Timothée Alby 4 лет назад
Родитель
Сommit
24bd557485
26 измененных файлов с 202 добавлено и 179 удалено
  1. 5 8
      services/web/frontend/js/features/file-tree/components/file-tree-context.js
  2. 2 2
      services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.js
  3. 2 2
      services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer.js
  4. 5 7
      services/web/frontend/js/features/file-tree/components/file-tree-root.js
  5. 2 2
      services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js
  6. 2 2
      services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js
  7. 2 2
      services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js
  8. 2 2
      services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js
  9. 37 0
      services/web/frontend/js/features/file-tree/util/count-in-tree.js
  10. 0 6
      services/web/frontend/js/shared/context/editor-context.js
  11. 71 78
      services/web/frontend/js/shared/context/file-tree-data-context.js
  12. 16 14
      services/web/frontend/js/shared/context/project-context.js
  13. 12 9
      services/web/frontend/js/shared/context/root-context.js
  14. 5 5
      services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.js
  15. 3 3
      services/web/test/frontend/features/file-tree/components/file-tree-doc.test.js
  16. 2 2
      services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.js
  17. 3 3
      services/web/test/frontend/features/file-tree/components/file-tree-folder.test.js
  18. 1 1
      services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js
  19. 6 6
      services/web/test/frontend/features/file-tree/components/file-tree-root.test.js
  20. 1 1
      services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.js
  21. 2 2
      services/web/test/frontend/features/file-tree/flows/context-menu.test.js
  22. 4 4
      services/web/test/frontend/features/file-tree/flows/create-folder.test.js
  23. 3 3
      services/web/test/frontend/features/file-tree/flows/delete-entity.test.js
  24. 1 1
      services/web/test/frontend/features/file-tree/flows/rename-entity.test.js
  25. 1 1
      services/web/test/frontend/features/file-tree/helpers/render-with-context.js
  26. 12 13
      services/web/test/frontend/helpers/render-with-context.js

+ 5 - 8
services/web/frontend/js/features/file-tree/components/file-tree-context.js

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
 
 import { FileTreeMainProvider } from '../contexts/file-tree-main'
 import { FileTreeActionableProvider } from '../contexts/file-tree-actionable'
-import { FileTreeMutableProvider } from '../contexts/file-tree-mutable'
 import { FileTreeSelectableProvider } from '../contexts/file-tree-selectable'
 import { FileTreeDraggableProvider } from '../contexts/file-tree-draggable'
 
@@ -26,13 +25,11 @@ function FileTreeContext({
       setStartedFreeTrial={setStartedFreeTrial}
       reindexReferences={reindexReferences}
     >
-      <FileTreeMutableProvider>
-        <FileTreeSelectableProvider onSelect={onSelect}>
-          <FileTreeActionableProvider>
-            <FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
-          </FileTreeActionableProvider>
-        </FileTreeSelectableProvider>
-      </FileTreeMutableProvider>
+      <FileTreeSelectableProvider onSelect={onSelect}>
+        <FileTreeActionableProvider>
+          <FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
+        </FileTreeActionableProvider>
+      </FileTreeSelectableProvider>
     </FileTreeMainProvider>
   )
 }

+ 2 - 2
services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.js

@@ -6,7 +6,7 @@ import FileTreeUploadDoc from './modes/file-tree-upload-doc'
 import FileTreeModalCreateFileMode from './file-tree-modal-create-file-mode'
 import FileTreeCreateNameProvider from '../../contexts/file-tree-create-name'
 import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
-import { useFileTreeMutable } from '../../contexts/file-tree-mutable'
+import { useFileTreeData } from '../../../../shared/context/file-tree-data-context'
 
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 
@@ -16,7 +16,7 @@ export default function FileTreeModalCreateFileBody() {
   const { t } = useTranslation()
 
   const { newFileCreateMode } = useFileTreeActionable()
-  const { fileCount } = useFileTreeMutable()
+  const { fileCount } = useFileTreeData()
 
   if (!fileCount || fileCount.status === 'error') {
     return null

+ 2 - 2
services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer.js

@@ -2,13 +2,13 @@ import { useTranslation } from 'react-i18next'
 import { Alert, Button } from 'react-bootstrap'
 import { useFileTreeCreateForm } from '../../contexts/file-tree-create-form'
 import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
-import { useFileTreeMutable } from '../../contexts/file-tree-mutable'
+import { useFileTreeData } from '../../../../shared/context/file-tree-data-context'
 import PropTypes from 'prop-types'
 
 export default function FileTreeModalCreateFileFooter() {
   const { valid } = useFileTreeCreateForm()
   const { newFileCreateMode, inFlight, cancel } = useFileTreeActionable()
-  const { fileCount } = useFileTreeMutable()
+  const { fileCount } = useFileTreeData()
 
   return (
     <FileTreeModalCreateFileFooterContent

+ 5 - 7
services/web/frontend/js/features/file-tree/components/file-tree-root.js

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
 
 import withErrorBoundary from '../../../infrastructure/error-boundary'
 import { useProjectContext } from '../../../shared/context/project-context'
+import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import FileTreeContext from './file-tree-context'
 import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer'
 import FileTreeFolderList from './file-tree-folder-list'
@@ -13,7 +14,6 @@ import FileTreeModalError from './modals/file-tree-modal-error'
 import FileTreeContextMenu from './file-tree-context-menu'
 import FileTreeError from './file-tree-error'
 
-import { useFileTreeMutable } from '../contexts/file-tree-mutable'
 import { useDroppable } from '../contexts/file-tree-draggable'
 
 import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
@@ -28,10 +28,9 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
   onInit,
   isConnected,
 }) {
-  const { _id: projectId, rootFolder } = useProjectContext(
-    projectContextPropTypes
-  )
-  const isReady = projectId && rootFolder
+  const { _id: projectId } = useProjectContext(projectContextPropTypes)
+  const { fileTreeData } = useFileTreeData()
+  const isReady = projectId && fileTreeData
 
   useEffect(() => {
     if (isReady) onInit()
@@ -62,7 +61,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
 
 function FileTreeRootFolder() {
   useFileTreeSocketListener()
-  const { fileTreeData } = useFileTreeMutable()
+  const { fileTreeData } = useFileTreeData()
 
   const { isOver, dropRef } = useDroppable(fileTreeData._id)
 
@@ -95,7 +94,6 @@ FileTreeRoot.propTypes = {
 
 const projectContextPropTypes = {
   _id: PropTypes.string.isRequired,
-  rootFolder: PropTypes.array.isRequired,
 }
 
 export default withErrorBoundary(FileTreeRoot, FileTreeError)

+ 2 - 2
services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js

@@ -22,7 +22,7 @@ import { isBlockedFilename, isCleanFilename } from '../util/safe-path'
 
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useEditorContext } from '../../../shared/context/editor-context'
-import { useFileTreeMutable } from './file-tree-mutable'
+import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from './file-tree-selectable'
 
 import {
@@ -129,7 +129,7 @@ export function FileTreeActionableProvider({ children }) {
     defaultState
   )
 
-  const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeMutable()
+  const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeData()
   const { selectedEntityIds } = useFileTreeSelectable()
 
   const startRenaming = useCallback(() => {

+ 2 - 2
services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js

@@ -11,7 +11,7 @@ import {
 } from '../util/find-in-tree'
 
 import { useFileTreeActionable } from './file-tree-actionable'
-import { useFileTreeMutable } from './file-tree-mutable'
+import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
 import { useEditorContext } from '../../../shared/context/editor-context'
 
@@ -77,7 +77,7 @@ export function useDraggable(draggedEntityId) {
   const { t } = useTranslation()
 
   const { permissionsLevel } = useEditorContext(editorContextPropTypes)
-  const { fileTreeData } = useFileTreeMutable()
+  const { fileTreeData } = useFileTreeData()
   const { selectedEntityIds } = useFileTreeSelectable()
 
   const [isDraggable, setIsDraggable] = useState(true)

+ 2 - 2
services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js

@@ -12,7 +12,7 @@ import classNames from 'classnames'
 import _ from 'lodash'
 
 import { findInTree } from '../util/find-in-tree'
-import { useFileTreeMutable } from './file-tree-mutable'
+import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useProjectContext } from '../../../shared/context/project-context'
 import { useEditorContext } from '../../../shared/context/editor-context'
 import usePersistedState from '../../../shared/hooks/use-persisted-state'
@@ -85,7 +85,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
     rootDocId
   )
 
-  const { fileTreeData } = useFileTreeMutable()
+  const { fileTreeData } = useFileTreeData()
 
   const [selectedEntityIds, dispatch] = useReducer(
     permissionsLevel === 'readOnly'

+ 2 - 2
services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.js

@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react'
 import PropTypes from 'prop-types'
 
 import { useUserContext } from '../../../shared/context/user-context'
-import { useFileTreeMutable } from '../contexts/file-tree-mutable'
+import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
 import { findInTreeOrThrow } from '../util/find-in-tree'
 
@@ -18,7 +18,7 @@ export function useFileTreeSocketListener() {
     dispatchCreateDoc,
     dispatchCreateFile,
     fileTreeData,
-  } = useFileTreeMutable()
+  } = useFileTreeData()
   const { selectedEntityIds, selectedEntityParentIds, select, unselect } =
     useFileTreeSelectable()
   const socket = window._ide && window._ide.socket

+ 37 - 0
services/web/frontend/js/features/file-tree/util/count-in-tree.js

@@ -0,0 +1,37 @@
+export function countFiles(fileTreeData) {
+  if (!fileTreeData) {
+    return 0
+  }
+
+  const files = filesInFolder(fileTreeData)
+
+  // count all the non-deleted entities
+  const value = files.filter(item => !item.deleted).length
+
+  const limit = window.ExposedSettings.maxEntitiesPerProject
+  const status = fileCountStatus(value, limit, Math.ceil(limit / 20))
+
+  return { value, status, limit }
+}
+
+function filesInFolder({ docs, folders, fileRefs }) {
+  const files = [...docs, ...fileRefs]
+
+  for (const folder of folders) {
+    files.push(...filesInFolder(folder))
+  }
+
+  return files
+}
+
+function fileCountStatus(value, limit, range) {
+  if (value >= limit) {
+    return 'error'
+  }
+
+  if (value >= limit - range) {
+    return 'warning'
+  }
+
+  return 'success'
+}

+ 0 - 6
services/web/frontend/js/shared/context/editor-context.js

@@ -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,

+ 71 - 78
services/web/frontend/js/features/file-tree/contexts/file-tree-mutable.js → services/web/frontend/js/shared/context/file-tree-data-context.js

@@ -4,18 +4,36 @@ import {
   useReducer,
   useContext,
   useEffect,
+  useMemo,
 } from 'react'
 import PropTypes from 'prop-types'
-
+import useScopeValue from '../hooks/use-scope-value'
 import {
   renameInTree,
   deleteInTree,
   moveInTree,
   createEntityInTree,
-} from '../util/mutate-in-tree'
-import { useProjectContext } from '../../../shared/context/project-context'
+} from '../../features/file-tree/util/mutate-in-tree'
+import { countFiles } from '../../features/file-tree/util/count-in-tree'
+
+const FileTreeDataContext = createContext()
 
-const FileTreeMutableContext = 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',
@@ -88,13 +106,37 @@ function fileTreeMutableReducer({ fileTreeData }, action) {
   }
 }
 
-const initialState = rootFolder => ({
-  fileTreeData: rootFolder[0],
-  fileCount: countFiles(rootFolder[0]),
-})
+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 const FileTreeMutableProvider = function ({ children }) {
-  const { rootFolder } = useProjectContext(projectContextPropTypes)
+export function FileTreeDataProvider({ children }) {
+  const [project] = useScopeValue('project', true)
+
+  const { rootFolder } = project || {}
 
   const [{ fileTreeData, fileCount }, dispatch] = useReducer(
     fileTreeMutableReducer,
@@ -105,7 +147,7 @@ export const FileTreeMutableProvider = function ({ children }) {
   useEffect(() => {
     dispatch({
       type: ACTION_TYPES.RESET,
-      fileTreeData: rootFolder[0],
+      fileTreeData: rootFolder?.[0],
     })
   }, [rootFolder])
 
@@ -152,7 +194,19 @@ export const FileTreeMutableProvider = function ({ children }) {
     dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
   }, [])
 
-  const value = {
+  const value = useMemo(() => {
+    return {
+      dispatchCreateDoc,
+      dispatchCreateFile,
+      dispatchCreateFolder,
+      dispatchDelete,
+      dispatchMove,
+      dispatchRename,
+      fileCount,
+      fileTreeData,
+      hasFolders: fileTreeData?.folders.length > 0,
+    }
+  }, [
     dispatchCreateDoc,
     dispatchCreateFile,
     dispatchCreateFolder,
@@ -161,76 +215,15 @@ export const FileTreeMutableProvider = function ({ children }) {
     dispatchRename,
     fileCount,
     fileTreeData,
-  }
+  ])
 
   return (
-    <FileTreeMutableContext.Provider value={value}>
+    <FileTreeDataContext.Provider value={value}>
       {children}
-    </FileTreeMutableContext.Provider>
+    </FileTreeDataContext.Provider>
   )
 }
 
-FileTreeMutableProvider.propTypes = {
-  children: PropTypes.oneOfType([
-    PropTypes.arrayOf(PropTypes.node),
-    PropTypes.node,
-  ]).isRequired,
-}
-
-const projectContextPropTypes = {
-  rootFolder: PropTypes.arrayOf(
-    PropTypes.shape({
-      _id: PropTypes.string.isRequired,
-      name: PropTypes.string.isRequired,
-      docs: PropTypes.array.isRequired,
-      fileRefs: PropTypes.array.isRequired,
-      folders: PropTypes.array.isRequired,
-    })
-  ),
-}
-
-export function useFileTreeMutable() {
-  const context = useContext(FileTreeMutableContext)
-
-  if (!context) {
-    throw new Error(
-      'useFileTreeMutable is only available in FileTreeMutableProvider'
-    )
-  }
-
-  return context
-}
-
-function filesInFolder({ docs, folders, fileRefs }) {
-  const files = [...docs, ...fileRefs]
-
-  for (const folder of folders) {
-    files.push(...filesInFolder(folder))
-  }
-
-  return files
-}
-
-function countFiles(fileTreeData) {
-  const files = filesInFolder(fileTreeData)
-
-  // count all the non-deleted entities
-  const value = files.filter(item => !item.deleted).length
-
-  const limit = window.ExposedSettings.maxEntitiesPerProject
-  const status = fileCountStatus(value, limit, Math.ceil(limit / 20))
-
-  return { value, status, limit }
-}
-
-function fileCountStatus(value, limit, range) {
-  if (value >= limit) {
-    return 'error'
-  }
-
-  if (value >= limit - range) {
-    return 'warning'
-  }
-
-  return 'success'
+FileTreeDataProvider.propTypes = {
+  children: PropTypes.any,
 }

+ 16 - 14
services/web/frontend/js/shared/context/project-context.js

@@ -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>
   )

+ 12 - 9
services/web/frontend/js/shared/context/root-context.js

@@ -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>

+ 5 - 5
services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.js

@@ -8,7 +8,7 @@ import PropTypes from 'prop-types'
 import renderWithContext from '../../helpers/render-with-context'
 import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file'
 import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable'
-import { useFileTreeMutable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-mutable'
+import { useFileTreeData } from '../../../../../../frontend/js/shared/context/file-tree-data-context'
 
 describe('<FileTreeModalCreateFile/>', function () {
   beforeEach(function () {
@@ -65,7 +65,7 @@ describe('<FileTreeModalCreateFile/>', function () {
     ]
 
     renderWithContext(<OpenWithMode mode="doc" />, {
-      contextProps: { projectRootFolder: rootFolder },
+      contextProps: { rootFolder },
     })
 
     screen.getByRole(
@@ -89,7 +89,7 @@ describe('<FileTreeModalCreateFile/>', function () {
     ]
 
     renderWithContext(<OpenWithMode mode="doc" />, {
-      contextProps: { projectRootFolder: rootFolder },
+      contextProps: { rootFolder },
     })
 
     screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
@@ -127,7 +127,7 @@ describe('<FileTreeModalCreateFile/>', function () {
     ]
 
     renderWithContext(<OpenWithMode mode="doc" />, {
-      contextProps: { projectRootFolder: rootFolder },
+      contextProps: { rootFolder },
     })
 
     screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
@@ -458,7 +458,7 @@ describe('<FileTreeModalCreateFile/>', function () {
 function OpenWithMode({ mode }) {
   const { newFileCreateMode, startCreatingFile } = useFileTreeActionable()
 
-  const { fileCount } = useFileTreeMutable()
+  const { fileCount } = useFileTreeData()
 
   // eslint-disable-next-line react-hooks/exhaustive-deps
   useEffect(() => startCreatingFile(mode), [])

+ 3 - 3
services/web/test/frontend/features/file-tree/components/file-tree-doc.test.js

@@ -19,7 +19,7 @@ describe('<FileTreeDoc/>', function () {
       <FileTreeDoc name="foo.tex" id="123abc" isLinkedFile={false} />,
       {
         contextProps: {
-          projectRootFolder: [
+          rootFolder: [
             {
               _id: 'root-folder-id',
               name: 'rootFolder',
@@ -50,7 +50,7 @@ describe('<FileTreeDoc/>', function () {
   it('selects', function () {
     renderWithContext(<FileTreeDoc name="foo.tex" id="123abc" expanded />, {
       contextProps: {
-        projectRootFolder: [
+        rootFolder: [
           {
             _id: 'root-folder-id',
             name: 'rootFolder',
@@ -71,7 +71,7 @@ describe('<FileTreeDoc/>', function () {
   it('multi-selects', function () {
     renderWithContext(<FileTreeDoc name="foo.tex" id="123abc" expanded />, {
       contextProps: {
-        projectRootFolder: [
+        rootFolder: [
           {
             _id: 'root-folder-id',
             name: 'rootFolder',

+ 2 - 2
services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.js

@@ -43,7 +43,7 @@ describe('<FileTreeFolderList/>', function () {
         {
           contextProps: {
             permissionsLevel: 'readOnly',
-            projectRootFolder: [
+            rootFolder: [
               {
                 _id: 'root-folder-id',
                 name: 'rootFolder',
@@ -80,7 +80,7 @@ describe('<FileTreeFolderList/>', function () {
         <FileTreeFolderList folders={[]} docs={docs} files={[]} />,
         {
           contextProps: {
-            projectRootFolder: [
+            rootFolder: [
               {
                 _id: 'root-folder-id',
                 name: 'rootFolder',

+ 3 - 3
services/web/test/frontend/features/file-tree/components/file-tree-folder.test.js

@@ -35,7 +35,7 @@ describe('<FileTreeFolder/>', function () {
       />,
       {
         contextProps: {
-          projectRootFolder: [
+          rootFolder: [
             {
               _id: 'root-folder-id',
               name: 'rootFolder',
@@ -66,7 +66,7 @@ describe('<FileTreeFolder/>', function () {
       />,
       {
         contextProps: {
-          projectRootFolder: [
+          rootFolder: [
             {
               _id: 'root-folder-id',
               name: 'rootFolder',
@@ -97,7 +97,7 @@ describe('<FileTreeFolder/>', function () {
       />,
       {
         contextProps: {
-          projectRootFolder: [
+          rootFolder: [
             {
               _id: 'root-folder-id',
               name: 'rootFolder',

+ 1 - 1
services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js

@@ -86,7 +86,7 @@ describe('<FileTreeitemInner />', function () {
         {
           contextProps: {
             rootDocId: '123abc',
-            projectRootFolder: [
+            rootFolder: [
               {
                 _id: 'root-folder-id',
                 name: 'rootFolder',

+ 6 - 6
services/web/test/frontend/features/file-tree/components/file-tree-root.test.js

@@ -47,7 +47,7 @@ describe('<FileTreeRoot/>', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         features: {},
@@ -86,7 +86,7 @@ describe('<FileTreeRoot/>', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         features: {},
@@ -128,7 +128,7 @@ describe('<FileTreeRoot/>', function () {
         setStartedFreeTrial={() => null}
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         features: {},
@@ -163,7 +163,7 @@ describe('<FileTreeRoot/>', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         features: {},
@@ -219,7 +219,7 @@ describe('<FileTreeRoot/>', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         features: {},
@@ -266,7 +266,7 @@ describe('<FileTreeRoot/>', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         features: {},

+ 1 - 1
services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.js

@@ -31,7 +31,7 @@ describe('<FileTreeToolbar/>', function () {
     renderWithContext(<FileTreeToolbar />, {
       contextProps: {
         rootDocId: '456def',
-        projectRootFolder: [
+        rootFolder: [
           {
             _id: 'root-folder-id',
             name: 'rootFolder',

+ 2 - 2
services/web/test/frontend/features/file-tree/flows/context-menu.test.js

@@ -39,7 +39,7 @@ describe('FileTree Context Menu Flow', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
       }
@@ -74,7 +74,7 @@ describe('FileTree Context Menu Flow', function () {
         isConnected
       />,
       {
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
         permissionsLevel: 'readOnly',

+ 4 - 4
services/web/test/frontend/features/file-tree/flows/create-folder.test.js

@@ -48,7 +48,7 @@ describe('FileTree Create Folder Flow', function () {
       />,
       {
         socket: new MockedSocket(),
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
       }
     )
@@ -110,7 +110,7 @@ describe('FileTree Create Folder Flow', function () {
       />,
       {
         socket: new MockedSocket(),
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '789ghi',
       }
@@ -182,7 +182,7 @@ describe('FileTree Create Folder Flow', function () {
       />,
       {
         socket: new MockedSocket(),
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
       }
@@ -243,7 +243,7 @@ describe('FileTree Create Folder Flow', function () {
       />,
       {
         socket: new MockedSocket(),
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
         rootDocId: '456def',
       }

+ 3 - 3
services/web/test/frontend/features/file-tree/flows/delete-entity.test.js

@@ -44,7 +44,7 @@ describe('FileTree Delete Entity Flow', function () {
         />,
         {
           socket: new MockedSocket(),
-          projectRootFolder: rootFolder,
+          rootFolder,
           projectId: '123abc',
         }
       )
@@ -164,7 +164,7 @@ describe('FileTree Delete Entity Flow', function () {
         />,
         {
           socket: new MockedSocket(),
-          projectRootFolder: rootFolder,
+          rootFolder,
           projectId: '123abc',
         }
       )
@@ -223,7 +223,7 @@ describe('FileTree Delete Entity Flow', function () {
         />,
         {
           socket: new MockedSocket(),
-          projectRootFolder: rootFolder,
+          rootFolder,
           projectId: '123abc',
         }
       )

+ 1 - 1
services/web/test/frontend/features/file-tree/flows/rename-entity.test.js

@@ -59,7 +59,7 @@ describe('FileTree Rename Entity Flow', function () {
       />,
       {
         socket: new MockedSocket(),
-        projectRootFolder: rootFolder,
+        rootFolder,
         projectId: '123abc',
       }
     )

+ 1 - 1
services/web/test/frontend/features/file-tree/helpers/render-with-context.js

@@ -5,7 +5,7 @@ export default (children, options = {}) => {
   let { contextProps = {}, ...renderOptions } = options
   contextProps = {
     projectId: '123abc',
-    projectRootFolder: [
+    rootFolder: [
       {
         _id: 'root-folder-id',
         name: 'rootFolder',

+ 12 - 13
services/web/test/frontend/helpers/render-with-context.js

@@ -14,6 +14,7 @@ import { get } from 'lodash'
 import { ProjectProvider } from '../../../frontend/js/shared/context/project-context'
 import { SplitTestProvider } from '../../../frontend/js/shared/context/split-test-context'
 import { CompileProvider } from '../../../frontend/js/shared/context/compile-context'
+import { FileTreeDataProvider } from '../../../frontend/js/shared/context/file-tree-data-context'
 
 // these constants can be imported in tests instead of
 // using magic strings
@@ -36,8 +37,7 @@ export function EditorProviders({
   },
   permissionsLevel = 'owner',
   children,
-  rootFolder,
-  projectRootFolder = [
+  rootFolder = [
     {
       _id: 'root-folder-id',
       name: 'rootFolder',
@@ -75,10 +75,7 @@ export function EditorProviders({
       },
       features,
       rootDoc_id: rootDocId,
-      rootFolder: projectRootFolder,
-    },
-    rootFolder: rootFolder || {
-      children: [],
+      rootFolder,
     },
     ui,
     $watch: (path, callback) => {
@@ -113,13 +110,15 @@ export function EditorProviders({
       <IdeProvider ide={window._ide}>
         <UserProvider>
           <ProjectProvider>
-            <EditorProvider settings={{}}>
-              <DetachProvider>
-                <LayoutProvider>
-                  <CompileProvider>{children}</CompileProvider>
-                </LayoutProvider>
-              </DetachProvider>
-            </EditorProvider>
+            <FileTreeDataProvider>
+              <EditorProvider settings={{}}>
+                <DetachProvider>
+                  <LayoutProvider>
+                    <CompileProvider>{children}</CompileProvider>
+                  </LayoutProvider>
+                </DetachProvider>
+              </EditorProvider>
+            </FileTreeDataProvider>
           </ProjectProvider>
         </UserProvider>
       </IdeProvider>