Merge pull request #6209 from overleaf/ta-file-tree-rework

File Tree Misc Code Changes

GitOrigin-RevId: dce64a5378ecee5c8a2e25e02502ae631d87f36b
This commit is contained in:
Timothée Alby 2022-01-10 16:46:46 +01:00 committed by Copybot
parent 55829a3382
commit 392410390e
37 changed files with 509 additions and 429 deletions

View file

@ -12,14 +12,9 @@ aside.editor-sidebar.full-size(
vertical-resizable-top vertical-resizable-top
) )
file-tree-root( file-tree-root(
project-id="projectId"
root-folder="rootFolder"
root-doc-id="rootDocId"
has-write-permissions="hasWritePermissions"
on-select="onSelect" on-select="onSelect"
on-init="onInit" on-init="onInit"
is-connected="isConnected" is-connected="isConnected"
user-has-feature="userHasFeature"
ref-providers="refProviders" ref-providers="refProviders"
reindex-references="reindexReferences" reindex-references="reindexReferences"
set-ref-provider-enabled="setRefProviderEnabled" set-ref-provider-enabled="setRefProviderEnabled"

View file

@ -1,16 +1,18 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import { Dropdown } from 'react-bootstrap' import { Dropdown } from 'react-bootstrap'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useFileTreeMainContext } from '../contexts/file-tree-main' import { useFileTreeMainContext } from '../contexts/file-tree-main'
import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items' import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
function FileTreeContextMenu() { function FileTreeContextMenu() {
const { hasWritePermissions, contextMenuCoords, setContextMenuCoords } = const { permissionsLevel } = useEditorContext(editorContextPropTypes)
useFileTreeMainContext() const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
if (!hasWritePermissions || !contextMenuCoords) return null if (permissionsLevel === 'readOnly' || !contextMenuCoords) return null
function close() { function close() {
// reset context menu // reset context menu
@ -41,6 +43,10 @@ function FileTreeContextMenu() {
) )
} }
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
// fake component required as Dropdowns require a Toggle, even tho we don't want // fake component required as Dropdowns require a Toggle, even tho we don't want
// one for the context menu // one for the context menu
const FakeDropDownToggle = React.forwardRef((props, ref) => { const FakeDropDownToggle = React.forwardRef((props, ref) => {

View file

@ -12,11 +12,6 @@ import { FileTreeDraggableProvider } from '../contexts/file-tree-draggable'
// FileTreeMutable: provides entities mutation operations // FileTreeMutable: provides entities mutation operations
// FileTreeSelectable: handles selection and multi-selection // FileTreeSelectable: handles selection and multi-selection
function FileTreeContext({ function FileTreeContext({
projectId,
rootFolder,
hasWritePermissions,
rootDocId,
userHasFeature,
refProviders, refProviders,
reindexReferences, reindexReferences,
setRefProviderEnabled, setRefProviderEnabled,
@ -26,21 +21,14 @@ function FileTreeContext({
}) { }) {
return ( return (
<FileTreeMainProvider <FileTreeMainProvider
projectId={projectId}
hasWritePermissions={hasWritePermissions}
userHasFeature={userHasFeature}
refProviders={refProviders} refProviders={refProviders}
setRefProviderEnabled={setRefProviderEnabled} setRefProviderEnabled={setRefProviderEnabled}
setStartedFreeTrial={setStartedFreeTrial} setStartedFreeTrial={setStartedFreeTrial}
reindexReferences={reindexReferences} reindexReferences={reindexReferences}
> >
<FileTreeMutableProvider rootFolder={rootFolder}> <FileTreeMutableProvider>
<FileTreeSelectableProvider <FileTreeSelectableProvider onSelect={onSelect}>
hasWritePermissions={hasWritePermissions} <FileTreeActionableProvider>
rootDocId={rootDocId}
onSelect={onSelect}
>
<FileTreeActionableProvider hasWritePermissions={hasWritePermissions}>
<FileTreeDraggableProvider>{children}</FileTreeDraggableProvider> <FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
</FileTreeActionableProvider> </FileTreeActionableProvider>
</FileTreeSelectableProvider> </FileTreeSelectableProvider>
@ -50,15 +38,10 @@ function FileTreeContext({
} }
FileTreeContext.propTypes = { FileTreeContext.propTypes = {
projectId: PropTypes.string.isRequired,
rootFolder: PropTypes.array.isRequired,
hasWritePermissions: PropTypes.bool.isRequired,
userHasFeature: PropTypes.func.isRequired,
reindexReferences: PropTypes.func.isRequired, reindexReferences: PropTypes.func.isRequired,
refProviders: PropTypes.object.isRequired, refProviders: PropTypes.object.isRequired,
setRefProviderEnabled: PropTypes.func.isRequired, setRefProviderEnabled: PropTypes.func.isRequired,
setStartedFreeTrial: PropTypes.func.isRequired, setStartedFreeTrial: PropTypes.func.isRequired,
rootDocId: PropTypes.string,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),

View file

@ -10,7 +10,7 @@ import { useProjectOutputFiles } from '../../../hooks/use-project-output-files'
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable' import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name' import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form' import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
import { useFileTreeMainContext } from '../../../contexts/file-tree-main' import { useProjectContext } from '../../../../../shared/context/project-context'
import ErrorMessage from '../error-message' import ErrorMessage from '../error-message'
export default function FileTreeImportFromProject() { export default function FileTreeImportFromProject() {
@ -23,7 +23,6 @@ export default function FileTreeImportFromProject() {
const { name, setName, validName } = useFileTreeCreateName() const { name, setName, validName } = useFileTreeCreateName()
const { setValid } = useFileTreeCreateForm() const { setValid } = useFileTreeCreateForm()
const { projectId } = useFileTreeMainContext()
const { error, finishCreatingLinkedFile } = useFileTreeActionable() const { error, finishCreatingLinkedFile } = useFileTreeActionable()
const [selectedProject, setSelectedProject] = useState() const [selectedProject, setSelectedProject] = useState()
@ -112,7 +111,6 @@ export default function FileTreeImportFromProject() {
return ( return (
<form className="form-controls" id="create-file" onSubmit={handleSubmit}> <form className="form-controls" id="create-file" onSubmit={handleSubmit}>
<SelectProject <SelectProject
projectId={projectId}
selectedProject={selectedProject} selectedProject={selectedProject}
setSelectedProject={setSelectedProject} setSelectedProject={setSelectedProject}
/> />
@ -162,8 +160,9 @@ export default function FileTreeImportFromProject() {
) )
} }
function SelectProject({ projectId, selectedProject, setSelectedProject }) { function SelectProject({ selectedProject, setSelectedProject }) {
const { t } = useTranslation() const { t } = useTranslation()
const { _id: projectId } = useProjectContext(projectContextPropTypes)
const { data, error, loading } = useUserProjects() const { data, error, loading } = useUserProjects()
@ -219,11 +218,14 @@ function SelectProject({ projectId, selectedProject, setSelectedProject }) {
) )
} }
SelectProject.propTypes = { SelectProject.propTypes = {
projectId: PropTypes.string.isRequired,
selectedProject: PropTypes.object, selectedProject: PropTypes.object,
setSelectedProject: PropTypes.func.isRequired, setSelectedProject: PropTypes.func.isRequired,
} }
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
}
function SelectProjectOutputFile({ function SelectProjectOutputFile({
selectedProjectId, selectedProjectId,
selectedProjectOutputFile, selectedProjectOutputFile,

View file

@ -6,7 +6,7 @@ import Uppy from '@uppy/core'
import XHRUpload from '@uppy/xhr-upload' import XHRUpload from '@uppy/xhr-upload'
import { Dashboard, useUppy } from '@uppy/react' import { Dashboard, useUppy } from '@uppy/react'
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable' import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
import { useFileTreeMainContext } from '../../../contexts/file-tree-main' import { useProjectContext } from '../../../../../shared/context/project-context'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
@ -15,7 +15,7 @@ import ErrorMessage from '../error-message'
export default function FileTreeUploadDoc() { export default function FileTreeUploadDoc() {
const { parentFolderId, cancel, isDuplicate } = useFileTreeActionable() const { parentFolderId, cancel, isDuplicate } = useFileTreeActionable()
const { projectId } = useFileTreeMainContext() const { _id: projectId } = useProjectContext(projectContextPropTypes)
const [error, setError] = useState() const [error, setError] = useState()
@ -162,6 +162,10 @@ export default function FileTreeUploadDoc() {
) )
} }
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
}
function UploadErrorMessage({ error, maxNumberOfFiles }) { function UploadErrorMessage({ error, maxNumberOfFiles }) {
switch (error) { switch (error) {
case 'too-many-files': case 'too-many-files':

View file

@ -1,10 +1,11 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { useFileTreeMainContext } from '../../contexts/file-tree-main' import { useProjectContext } from '../../../../shared/context/project-context'
// handle "not-logged-in" errors by redirecting to the login page // handle "not-logged-in" errors by redirecting to the login page
export default function RedirectToLogin() { export default function RedirectToLogin() {
const { projectId } = useFileTreeMainContext() const { _id: projectId } = useProjectContext(projectContextPropTypes)
const [secondsToRedirect, setSecondsToRedirect] = useState(10) const [secondsToRedirect, setSecondsToRedirect] = useState(10)
@ -35,3 +36,7 @@ export default function RedirectToLogin() {
/> />
) )
} }
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
}

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import classNames from 'classnames' import classNames from 'classnames'
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed' import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
import { useEditorContext } from '../../../../shared/context/editor-context'
import { useFileTreeMainContext } from '../../contexts/file-tree-main' import { useFileTreeMainContext } from '../../contexts/file-tree-main'
import { useDraggable } from '../../contexts/file-tree-draggable' import { useDraggable } from '../../contexts/file-tree-draggable'
@ -11,12 +12,15 @@ import FileTreeItemMenu from './file-tree-item-menu'
import { useFileTreeSelectable } from '../../contexts/file-tree-selectable' import { useFileTreeSelectable } from '../../contexts/file-tree-selectable'
function FileTreeItemInner({ id, name, isSelected, icons }) { function FileTreeItemInner({ id, name, isSelected, icons }) {
const { hasWritePermissions, setContextMenuCoords } = useFileTreeMainContext() const { permissionsLevel } = useEditorContext(editorContextPropTypes)
const { setContextMenuCoords } = useFileTreeMainContext()
const { selectedEntityIds } = useFileTreeSelectable() const { selectedEntityIds } = useFileTreeSelectable()
const hasMenu = const hasMenu =
hasWritePermissions && isSelected && selectedEntityIds.size === 1 permissionsLevel !== 'readOnly' &&
isSelected &&
selectedEntityIds.size === 1
const { isDragging, dragRef, setIsDraggable } = useDraggable(id) const { isDragging, dragRef, setIsDraggable } = useDraggable(id)
@ -82,4 +86,8 @@ FileTreeItemInner.propTypes = {
icons: PropTypes.node, icons: PropTypes.node,
} }
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
export default FileTreeItemInner export default FileTreeItemInner

View file

@ -4,19 +4,16 @@ import PropTypes from 'prop-types'
import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import { useFileTreeMainContext } from '../../contexts/file-tree-main'
function FileTreeItemName({ name, isSelected, setIsDraggable }) { function FileTreeItemName({ name, isSelected, setIsDraggable }) {
const { hasWritePermissions } = useFileTreeMainContext()
const { isRenaming, startRenaming, finishRenaming, error, cancel } = const { isRenaming, startRenaming, finishRenaming, error, cancel } =
useFileTreeActionable() useFileTreeActionable()
const isRenamingEntity = isRenaming && isSelected && !error const isRenamingEntity = isRenaming && isSelected && !error
useEffect(() => { useEffect(() => {
setIsDraggable(hasWritePermissions && !isRenamingEntity) setIsDraggable(!isRenamingEntity)
}, [setIsDraggable, hasWritePermissions, isRenamingEntity]) }, [setIsDraggable, isRenamingEntity])
if (isRenamingEntity) { if (isRenamingEntity) {
return ( return (

View file

@ -2,6 +2,7 @@ import React, { useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '../../../infrastructure/error-boundary'
import { useProjectContext } from '../../../shared/context/project-context'
import FileTreeContext from './file-tree-context' import FileTreeContext from './file-tree-context'
import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer' import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer'
import FileTreeFolderList from './file-tree-folder-list' import FileTreeFolderList from './file-tree-folder-list'
@ -19,11 +20,6 @@ import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file' import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
const FileTreeRoot = React.memo(function FileTreeRoot({ const FileTreeRoot = React.memo(function FileTreeRoot({
projectId,
rootFolder,
rootDocId,
hasWritePermissions,
userHasFeature,
refProviders, refProviders,
reindexReferences, reindexReferences,
setRefProviderEnabled, setRefProviderEnabled,
@ -32,6 +28,9 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
onInit, onInit,
isConnected, isConnected,
}) { }) {
const { _id: projectId, rootFolder } = useProjectContext(
projectContextPropTypes
)
const isReady = projectId && rootFolder const isReady = projectId && rootFolder
useEffect(() => { useEffect(() => {
@ -41,15 +40,10 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
return ( return (
<FileTreeContext <FileTreeContext
projectId={projectId}
hasWritePermissions={hasWritePermissions}
userHasFeature={userHasFeature}
refProviders={refProviders} refProviders={refProviders}
setRefProviderEnabled={setRefProviderEnabled} setRefProviderEnabled={setRefProviderEnabled}
setStartedFreeTrial={setStartedFreeTrial} setStartedFreeTrial={setStartedFreeTrial}
reindexReferences={reindexReferences} reindexReferences={reindexReferences}
rootFolder={rootFolder}
rootDocId={rootDocId}
onSelect={onSelect} onSelect={onSelect}
> >
{isConnected ? null : <div className="disconnected-overlay" />} {isConnected ? null : <div className="disconnected-overlay" />}
@ -90,18 +84,18 @@ function FileTreeRootFolder() {
} }
FileTreeRoot.propTypes = { FileTreeRoot.propTypes = {
projectId: PropTypes.string,
rootFolder: PropTypes.array,
rootDocId: PropTypes.string,
hasWritePermissions: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
onInit: PropTypes.func.isRequired, onInit: PropTypes.func.isRequired,
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
setRefProviderEnabled: PropTypes.func.isRequired, setRefProviderEnabled: PropTypes.func.isRequired,
userHasFeature: PropTypes.func.isRequired,
setStartedFreeTrial: PropTypes.func.isRequired, setStartedFreeTrial: PropTypes.func.isRequired,
reindexReferences: PropTypes.func.isRequired, reindexReferences: PropTypes.func.isRequired,
refProviders: PropTypes.object.isRequired, refProviders: PropTypes.object.isRequired,
} }
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
rootFolder: PropTypes.array.isRequired,
}
export default withErrorBoundary(FileTreeRoot, FileTreeError) export default withErrorBoundary(FileTreeRoot, FileTreeError)

View file

@ -1,15 +1,16 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon' import Icon from '../../../shared/components/icon'
import TooltipButton from '../../../shared/components/tooltip-button' import TooltipButton from '../../../shared/components/tooltip-button'
import { useFileTreeMainContext } from '../contexts/file-tree-main' import { useEditorContext } from '../../../shared/context/editor-context'
import { useFileTreeActionable } from '../contexts/file-tree-actionable' import { useFileTreeActionable } from '../contexts/file-tree-actionable'
function FileTreeToolbar() { function FileTreeToolbar() {
const { hasWritePermissions } = useFileTreeMainContext() const { permissionsLevel } = useEditorContext(editorContextPropTypes)
if (!hasWritePermissions) return null if (permissionsLevel === 'readOnly') return null
return ( return (
<div className="toolbar toolbar-filetree"> <div className="toolbar toolbar-filetree">
@ -19,6 +20,10 @@ function FileTreeToolbar() {
) )
} }
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
function FileTreeToolbarLeft() { function FileTreeToolbarLeft() {
const { t } = useTranslation() const { t } = useTranslation()
const { const {

View file

@ -20,7 +20,8 @@ import { findInTree, findInTreeOrThrow } from '../util/find-in-tree'
import { isNameUniqueInFolder } from '../util/is-name-unique-in-folder' import { isNameUniqueInFolder } from '../util/is-name-unique-in-folder'
import { isBlockedFilename, isCleanFilename } from '../util/safe-path' import { isBlockedFilename, isCleanFilename } from '../util/safe-path'
import { useFileTreeMainContext } from './file-tree-main' import { useProjectContext } from '../../../shared/context/project-context'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useFileTreeMutable } from './file-tree-mutable' import { useFileTreeMutable } from './file-tree-mutable'
import { useFileTreeSelectable } from './file-tree-selectable' import { useFileTreeSelectable } from './file-tree-selectable'
@ -117,15 +118,17 @@ function fileTreeActionableReducer(state, action) {
} }
} }
export function FileTreeActionableProvider({ hasWritePermissions, children }) { export function FileTreeActionableProvider({ children }) {
const { _id: projectId } = useProjectContext(projectContextPropTypes)
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
const [state, dispatch] = useReducer( const [state, dispatch] = useReducer(
hasWritePermissions permissionsLevel === 'readOnly'
? fileTreeActionableReducer ? fileTreeActionableReadOnlyReducer
: fileTreeActionableReadOnlyReducer, : fileTreeActionableReducer,
defaultState defaultState
) )
const { projectId } = useFileTreeMainContext()
const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeMutable() const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeMutable()
const { selectedEntityIds } = useFileTreeSelectable() const { selectedEntityIds } = useFileTreeSelectable()
@ -370,13 +373,20 @@ export function FileTreeActionableProvider({ hasWritePermissions, children }) {
} }
FileTreeActionableProvider.propTypes = { FileTreeActionableProvider.propTypes = {
hasWritePermissions: PropTypes.bool.isRequired,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node, PropTypes.node,
]).isRequired, ]).isRequired,
} }
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
}
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
export function useFileTreeActionable() { export function useFileTreeActionable() {
const context = useContext(FileTreeActionableContext) const context = useContext(FileTreeActionableContext)

View file

@ -13,7 +13,7 @@ import {
import { useFileTreeActionable } from './file-tree-actionable' import { useFileTreeActionable } from './file-tree-actionable'
import { useFileTreeMutable } from './file-tree-mutable' import { useFileTreeMutable } from './file-tree-mutable'
import { useFileTreeSelectable } from '../contexts/file-tree-selectable' import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
import { useFileTreeMainContext } from './file-tree-main' import { useEditorContext } from '../../../shared/context/editor-context'
// HACK ALERT // HACK ALERT
// DnD binds drag and drop events on window and stop propagation if the dragged // DnD binds drag and drop events on window and stop propagation if the dragged
@ -76,11 +76,11 @@ FileTreeDraggableProvider.propTypes = {
export function useDraggable(draggedEntityId) { export function useDraggable(draggedEntityId) {
const { t } = useTranslation() const { t } = useTranslation()
const { hasWritePermissions } = useFileTreeMainContext() const { permissionsLevel } = useEditorContext(editorContextPropTypes)
const { fileTreeData } = useFileTreeMutable() const { fileTreeData } = useFileTreeMutable()
const { selectedEntityIds } = useFileTreeSelectable() const { selectedEntityIds } = useFileTreeSelectable()
const [isDraggable, setIsDraggable] = useState(hasWritePermissions) const [isDraggable, setIsDraggable] = useState(true)
const item = { type: DRAGGABLE_TYPE } const item = { type: DRAGGABLE_TYPE }
const [{ isDragging }, dragRef, preview] = useDrag({ const [{ isDragging }, dragRef, preview] = useDrag({
@ -98,7 +98,7 @@ export function useDraggable(draggedEntityId) {
collect: monitor => ({ collect: monitor => ({
isDragging: !!monitor.isDragging(), isDragging: !!monitor.isDragging(),
}), }),
canDrag: () => isDraggable, canDrag: () => permissionsLevel !== 'readOnly' && isDraggable,
}) })
// remove the automatic preview as we're using a custom preview via // remove the automatic preview as we're using a custom preview via
@ -114,6 +114,10 @@ export function useDraggable(draggedEntityId) {
} }
} }
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
export function useDroppable(droppedEntityId) { export function useDroppable(droppedEntityId) {
const { finishMoving } = useFileTreeActionable() const { finishMoving } = useFileTreeActionable()

View file

@ -16,9 +16,6 @@ export function useFileTreeMainContext() {
} }
export const FileTreeMainProvider = function ({ export const FileTreeMainProvider = function ({
projectId,
hasWritePermissions,
userHasFeature,
refProviders, refProviders,
reindexReferences, reindexReferences,
setRefProviderEnabled, setRefProviderEnabled,
@ -30,9 +27,6 @@ export const FileTreeMainProvider = function ({
return ( return (
<FileTreeMainContext.Provider <FileTreeMainContext.Provider
value={{ value={{
projectId,
hasWritePermissions,
userHasFeature,
refProviders, refProviders,
reindexReferences, reindexReferences,
setRefProviderEnabled, setRefProviderEnabled,
@ -47,9 +41,6 @@ export const FileTreeMainProvider = function ({
} }
FileTreeMainProvider.propTypes = { FileTreeMainProvider.propTypes = {
projectId: PropTypes.string.isRequired,
hasWritePermissions: PropTypes.bool.isRequired,
userHasFeature: PropTypes.func.isRequired,
reindexReferences: PropTypes.func.isRequired, reindexReferences: PropTypes.func.isRequired,
refProviders: PropTypes.object.isRequired, refProviders: PropTypes.object.isRequired,
setRefProviderEnabled: PropTypes.func.isRequired, setRefProviderEnabled: PropTypes.func.isRequired,

View file

@ -13,6 +13,7 @@ import {
moveInTree, moveInTree,
createEntityInTree, createEntityInTree,
} from '../util/mutate-in-tree' } from '../util/mutate-in-tree'
import { useProjectContext } from '../../../shared/context/project-context'
const FileTreeMutableContext = createContext() const FileTreeMutableContext = createContext()
@ -21,7 +22,7 @@ const ACTION_TYPES = {
RESET: 'RESET', RESET: 'RESET',
DELETE: 'DELETE', DELETE: 'DELETE',
MOVE: 'MOVE', MOVE: 'MOVE',
CREATE_ENTITY: 'CREATE_ENTITY', CREATE: 'CREATE',
} }
function fileTreeMutableReducer({ fileTreeData }, action) { function fileTreeMutableReducer({ fileTreeData }, action) {
@ -68,7 +69,7 @@ function fileTreeMutableReducer({ fileTreeData }, action) {
} }
} }
case ACTION_TYPES.CREATE_ENTITY: { case ACTION_TYPES.CREATE: {
const newFileTreeData = createEntityInTree( const newFileTreeData = createEntityInTree(
fileTreeData, fileTreeData,
action.parentFolderId, action.parentFolderId,
@ -92,7 +93,9 @@ const initialState = rootFolder => ({
fileCount: countFiles(rootFolder[0]), fileCount: countFiles(rootFolder[0]),
}) })
export const FileTreeMutableProvider = function ({ rootFolder, children }) { export const FileTreeMutableProvider = function ({ children }) {
const { rootFolder } = useProjectContext(projectContextPropTypes)
const [{ fileTreeData, fileCount }, dispatch] = useReducer( const [{ fileTreeData, fileCount }, dispatch] = useReducer(
fileTreeMutableReducer, fileTreeMutableReducer,
rootFolder, rootFolder,
@ -109,7 +112,7 @@ export const FileTreeMutableProvider = function ({ rootFolder, children }) {
const dispatchCreateFolder = useCallback((parentFolderId, entity) => { const dispatchCreateFolder = useCallback((parentFolderId, entity) => {
entity.type = 'folder' entity.type = 'folder'
dispatch({ dispatch({
type: ACTION_TYPES.CREATE_ENTITY, type: ACTION_TYPES.CREATE,
parentFolderId, parentFolderId,
entity, entity,
}) })
@ -118,7 +121,7 @@ export const FileTreeMutableProvider = function ({ rootFolder, children }) {
const dispatchCreateDoc = useCallback((parentFolderId, entity) => { const dispatchCreateDoc = useCallback((parentFolderId, entity) => {
entity.type = 'doc' entity.type = 'doc'
dispatch({ dispatch({
type: ACTION_TYPES.CREATE_ENTITY, type: ACTION_TYPES.CREATE,
parentFolderId, parentFolderId,
entity, entity,
}) })
@ -127,7 +130,7 @@ export const FileTreeMutableProvider = function ({ rootFolder, children }) {
const dispatchCreateFile = useCallback((parentFolderId, entity) => { const dispatchCreateFile = useCallback((parentFolderId, entity) => {
entity.type = 'fileRef' entity.type = 'fileRef'
dispatch({ dispatch({
type: ACTION_TYPES.CREATE_ENTITY, type: ACTION_TYPES.CREATE,
parentFolderId, parentFolderId,
entity, entity,
}) })
@ -168,13 +171,24 @@ export const FileTreeMutableProvider = function ({ rootFolder, children }) {
} }
FileTreeMutableProvider.propTypes = { FileTreeMutableProvider.propTypes = {
rootFolder: PropTypes.array.isRequired,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node, PropTypes.node,
]).isRequired, ]).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() { export function useFileTreeMutable() {
const context = useContext(FileTreeMutableContext) const context = useContext(FileTreeMutableContext)

View file

@ -13,7 +13,8 @@ import _ from 'lodash'
import { findInTree } from '../util/find-in-tree' import { findInTree } from '../util/find-in-tree'
import { useFileTreeMutable } from './file-tree-mutable' import { useFileTreeMutable } from './file-tree-mutable'
import { useFileTreeMainContext } from './file-tree-main' import { useProjectContext } from '../../../shared/context/project-context'
import { useEditorContext } from '../../../shared/context/editor-context'
import usePersistedState from '../../../shared/hooks/use-persisted-state' import usePersistedState from '../../../shared/hooks/use-persisted-state'
import usePreviousValue from '../../../shared/hooks/use-previous-value' import usePreviousValue from '../../../shared/hooks/use-previous-value'
@ -73,13 +74,11 @@ function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
} }
} }
export function FileTreeSelectableProvider({ export function FileTreeSelectableProvider({ onSelect, children }) {
hasWritePermissions, const { _id: projectId, rootDoc_id: rootDocId } = useProjectContext(
rootDocId, projectContextPropTypes
onSelect, )
children, const { permissionsLevel } = useEditorContext(editorContextPropTypes)
}) {
const { projectId } = useFileTreeMainContext()
const [initialSelectedEntityId] = usePersistedState( const [initialSelectedEntityId] = usePersistedState(
`doc.open_id.${projectId}`, `doc.open_id.${projectId}`,
@ -89,9 +88,9 @@ export function FileTreeSelectableProvider({
const { fileTreeData } = useFileTreeMutable() const { fileTreeData } = useFileTreeMutable()
const [selectedEntityIds, dispatch] = useReducer( const [selectedEntityIds, dispatch] = useReducer(
hasWritePermissions permissionsLevel === 'readOnly'
? fileTreeSelectableReadWriteReducer ? fileTreeSelectableReadOnlyReducer
: fileTreeSelectableReadOnlyReducer, : fileTreeSelectableReadWriteReducer,
null, null,
() => { () => {
if (!initialSelectedEntityId) return new Set() if (!initialSelectedEntityId) return new Set()
@ -179,8 +178,6 @@ export function FileTreeSelectableProvider({
} }
FileTreeSelectableProvider.propTypes = { FileTreeSelectableProvider.propTypes = {
hasWritePermissions: PropTypes.bool.isRequired,
rootDocId: PropTypes.string,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
@ -188,6 +185,14 @@ FileTreeSelectableProvider.propTypes = {
]).isRequired, ]).isRequired,
} }
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
rootDoc_id: PropTypes.string,
}
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
export function useSelectableEntity(id) { export function useSelectableEntity(id) {
const { selectedEntityIds, selectOrMultiSelectEntity } = useContext( const { selectedEntityIds, selectOrMultiSelectEntity } = useContext(
FileTreeSelectableContext FileTreeSelectableContext

View file

@ -13,22 +13,12 @@ App.controller(
ide ide
// eventTracking // eventTracking
) { ) {
$scope.projectId = ide.project_id
$scope.rootFolder = null
$scope.rootDocId = null
$scope.hasWritePermissions = false
$scope.isConnected = true $scope.isConnected = true
$scope.$on('project:joined', () => { $scope.$on('project:joined', () => {
$scope.rootFolder = $scope.project.rootFolder
$scope.rootDocId = $scope.project.rootDoc_id
$scope.$emit('file-tree:initialized') $scope.$emit('file-tree:initialized')
}) })
$scope.$watch('permissions.write', hasWritePermissions => {
$scope.hasWritePermissions = hasWritePermissions
})
$scope.$watch('editor.open_doc_id', openDocId => { $scope.$watch('editor.open_doc_id', openDocId => {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('editor.openDoc', { detail: openDocId }) new CustomEvent('editor.openDoc', { detail: openDocId })
@ -85,12 +75,6 @@ App.controller(
} }
} }
$scope.userHasFeature = feature => ide.$scope.user.features[feature]
$scope.$watch('permissions.write', hasWritePermissions => {
$scope.hasWritePermissions = hasWritePermissions
})
$scope.refProviders = ide.$scope.user.refProviders || {} $scope.refProviders = ide.$scope.user.refProviders || {}
ide.$scope.$watch( ide.$scope.$watch(

View file

@ -4,6 +4,14 @@ import useScopeValue from '../hooks/use-scope-value'
const ProjectContext = createContext() 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 = { ProjectContext.Provider.propTypes = {
value: PropTypes.shape({ value: PropTypes.shape({
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,
@ -23,6 +31,9 @@ ProjectContext.Provider.propTypes = {
collaborators: PropTypes.number, collaborators: PropTypes.number,
compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority']), compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority']),
trackChangesVisible: PropTypes.bool, trackChangesVisible: PropTypes.bool,
references: PropTypes.bool,
mendeley: PropTypes.bool,
zotero: PropTypes.bool,
}), }),
publicAccesLevel: PropTypes.string, publicAccesLevel: PropTypes.string,
tokens: PropTypes.shape({ tokens: PropTypes.shape({
@ -33,6 +44,7 @@ ProjectContext.Provider.propTypes = {
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired, email: PropTypes.string.isRequired,
}), }),
rootFolder: PropTypes.arrayOf(fileTreeDataPropType),
}), }),
} }

View file

@ -1,6 +1,6 @@
import MockedSocket from 'socket.io-mock' import MockedSocket from 'socket.io-mock'
import { ContextRoot } from '../js/shared/context/root-context' import { withContextRoot } from './utils/with-context-root'
import { rootFolderBase } from './fixtures/file-tree-base' import { rootFolderBase } from './fixtures/file-tree-base'
import { rootFolderLimit } from './fixtures/file-tree-limit' import { rootFolderLimit } from './fixtures/file-tree-limit'
import FileTreeRoot from '../js/features/file-tree/components/file-tree-root' import FileTreeRoot from '../js/features/file-tree/components/file-tree-root'
@ -12,6 +12,12 @@ const MOCK_DELAY = 2000
window._ide = { window._ide = {
socket: new MockedSocket(), socket: new MockedSocket(),
} }
const DEFAULT_PROJECT = {
_id: '123abc',
name: 'Some Project',
rootDocId: '5e74f1a7ce17ae0041dfd056',
rootFolder: rootFolderBase,
}
function defaultSetupMocks(fetchMock) { function defaultSetupMocks(fetchMock) {
fetchMock fetchMock
@ -80,13 +86,25 @@ function defaultSetupMocks(fetchMock) {
export const FullTree = args => { export const FullTree = args => {
useFetchMock(defaultSetupMocks) useFetchMock(defaultSetupMocks)
return <FileTreeRoot {...args} /> return withContextRoot(<FileTreeRoot {...args} />, {
project: DEFAULT_PROJECT,
permissionsLevel: 'owner',
})
} }
export const ReadOnly = args => <FileTreeRoot {...args} /> export const ReadOnly = args => {
ReadOnly.args = { hasWritePermissions: false } return withContextRoot(<FileTreeRoot {...args} />, {
project: DEFAULT_PROJECT,
permissionsLevel: 'readOnly',
})
}
export const Disconnected = args => <FileTreeRoot {...args} /> export const Disconnected = args => {
return withContextRoot(<FileTreeRoot {...args} />, {
project: DEFAULT_PROJECT,
permissionsLevel: 'owner',
})
}
Disconnected.args = { isConnected: false } Disconnected.args = { isConnected: false }
export const NetworkErrors = args => { export const NetworkErrors = args => {
@ -106,24 +124,31 @@ export const NetworkErrors = args => {
}) })
}) })
return <FileTreeRoot {...args} /> return withContextRoot(<FileTreeRoot {...args} />, {
project: DEFAULT_PROJECT,
permissionsLevel: 'owner',
})
} }
export const FallbackError = args => <FileTreeError {...args} /> export const FallbackError = args => {
return withContextRoot(<FileTreeError {...args} />, {
project: DEFAULT_PROJECT,
})
}
export const FilesLimit = args => { export const FilesLimit = args => {
useFetchMock(defaultSetupMocks) useFetchMock(defaultSetupMocks)
return <FileTreeRoot {...args} /> return withContextRoot(<FileTreeRoot {...args} />, {
project: { ...DEFAULT_PROJECT, rootFolder: rootFolderLimit },
permissionsLevel: 'owner',
})
} }
FilesLimit.args = { rootFolder: rootFolderLimit }
export default { export default {
title: 'File Tree', title: 'File Tree',
component: FileTreeRoot, component: FileTreeRoot,
args: { args: {
rootFolder: rootFolderBase,
hasWritePermissions: true,
setStartedFreeTrial: () => { setStartedFreeTrial: () => {
console.log('started free trial') console.log('started free trial')
}, },
@ -131,12 +156,9 @@ export default {
reindexReferences: () => { reindexReferences: () => {
console.log('reindex references') console.log('reindex references')
}, },
userHasFeature: () => true,
setRefProviderEnabled: provider => { setRefProviderEnabled: provider => {
console.log(`ref provider ${provider} enabled`) console.log(`ref provider ${provider} enabled`)
}, },
projectId: '123abc',
rootDocId: '5e74f1a7ce17ae0041dfd056',
isConnected: true, isConnected: true,
}, },
argTypes: { argTypes: {
@ -149,9 +171,7 @@ export default {
<style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style> <style>{'html, body, .file-tree { height: 100%; width: 100%; }'}</style>
<div className="editor-sidebar full-size"> <div className="editor-sidebar full-size">
<div className="file-tree"> <div className="file-tree">
<ContextRoot ide={window._ide} settings={{}}>
<Story /> <Story />
</ContextRoot>
</div> </div>
</div> </div>
</> </>

View file

@ -13,6 +13,15 @@ export function setupContext() {
user: window.user, user: window.user,
project: { project: {
features: {}, features: {},
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [],
fileRefs: [],
},
],
}, },
$watch: () => {}, $watch: () => {},
$applyAsync: () => {}, $applyAsync: () => {},
@ -25,7 +34,6 @@ export function setupContext() {
pdfViewer: 'js', pdfViewer: 'js',
}, },
toggleHistory: () => {}, toggleHistory: () => {},
rootFolder: { type: 'folder', children: [] },
} }
} }
window._ide = { window._ide = {

View file

@ -1,15 +1,29 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { withContextRoot } from './../../utils/with-context-root'
import FileTreeContext from '../../../js/features/file-tree/components/file-tree-context' import FileTreeContext from '../../../js/features/file-tree/components/file-tree-context'
import FileTreeCreateNameProvider from '../../../js/features/file-tree/contexts/file-tree-create-name' import FileTreeCreateNameProvider from '../../../js/features/file-tree/contexts/file-tree-create-name'
import FileTreeCreateFormProvider from '../../../js/features/file-tree/contexts/file-tree-create-form' import FileTreeCreateFormProvider from '../../../js/features/file-tree/contexts/file-tree-create-form'
import { useFileTreeActionable } from '../../../js/features/file-tree/contexts/file-tree-actionable' import { useFileTreeActionable } from '../../../js/features/file-tree/contexts/file-tree-actionable'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
const defaultContextProps = { export const DEFAULT_PROJECT = {
projectId: 'project-1', _id: '123abc',
hasWritePermissions: true, name: 'Some Project',
userHasFeature: () => true, rootDocId: '5e74f1a7ce17ae0041dfd056',
refProviders: {}, rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [],
fileRefs: [],
},
],
features: { mendeley: true, zotero: true },
}
const defaultFileTreeContextProps = {
refProviders: { mendeley: false, zotero: false },
reindexReferences: () => { reindexReferences: () => {
console.log('reindex references') console.log('reindex references')
}, },
@ -19,17 +33,6 @@ const defaultContextProps = {
setStartedFreeTrial: () => { setStartedFreeTrial: () => {
console.log('started free trial') console.log('started free trial')
}, },
rootFolder: [
{
docs: [
{
_id: 'entity-1',
},
],
fileRefs: [],
folders: [],
},
],
initialSelectedEntityId: 'entity-1', initialSelectedEntityId: 'entity-1',
onSelect: () => { onSelect: () => {
console.log('selected') console.log('selected')
@ -99,11 +102,18 @@ export const mockCreateFileModalFetch = fetchMock =>
}) })
export const createFileModalDecorator = export const createFileModalDecorator =
(contextProps = {}, createMode = 'doc') => (
fileTreeContextProps = {},
projectProps = {},
createMode = 'doc'
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
) =>
Story => { Story => {
return ( return withContextRoot(
<FileTreeContext {...defaultContextProps} {...contextProps}> <FileTreeContext
{...defaultFileTreeContextProps}
{...fileTreeContextProps}
>
<FileTreeCreateNameProvider> <FileTreeCreateNameProvider>
<FileTreeCreateFormProvider> <FileTreeCreateFormProvider>
<OpenCreateFileModal createMode={createMode}> <OpenCreateFileModal createMode={createMode}>
@ -111,7 +121,11 @@ export const createFileModalDecorator =
</OpenCreateFileModal> </OpenCreateFileModal>
</FileTreeCreateFormProvider> </FileTreeCreateFormProvider>
</FileTreeCreateNameProvider> </FileTreeCreateNameProvider>
</FileTreeContext> </FileTreeContext>,
{
project: { ...DEFAULT_PROJECT, ...projectProps },
permissionsLevel: 'owner',
}
) )
} }

View file

@ -11,11 +11,7 @@ export const MinimalFeatures = args => {
return <FileTreeModalCreateFile {...args} /> return <FileTreeModalCreateFile {...args} />
} }
MinimalFeatures.decorators = [ MinimalFeatures.decorators = [createFileModalDecorator()]
createFileModalDecorator({
userHasFeature: () => false,
}),
]
export const WithExtraFeatures = args => { export const WithExtraFeatures = args => {
useFetchMock(mockCreateFileModalFetch) useFetchMock(mockCreateFileModalFetch)
@ -82,9 +78,13 @@ export const FileLimitReached = args => {
return <FileTreeModalCreateFile {...args} /> return <FileTreeModalCreateFile {...args} />
} }
FileLimitReached.decorators = [ FileLimitReached.decorators = [
createFileModalDecorator({ createFileModalDecorator(
{},
{
rootFolder: [ rootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({ docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`, _id: `entity-${index}`,
})), })),
@ -92,7 +92,8 @@ FileLimitReached.decorators = [
folders: [], folders: [],
}, },
], ],
}), }
),
] ]
export default { export default {

View file

@ -1,26 +0,0 @@
import sinon from 'sinon'
export const contextProps = {
projectId: 'test-project',
hasWritePermissions: true,
userHasFeature: () => true,
refProviders: { mendeley: false, zotero: false },
reindexReferences: () => {
console.log('reindex references')
},
setRefProviderEnabled: provider => {
console.log(`ref provider ${provider} enabled`)
},
setStartedFreeTrial: () => {
console.log('started free trial')
},
rootFolder: [
{
docs: [{ _id: 'entity-1' }],
fileRefs: [],
folders: [],
},
],
initialSelectedEntityId: 'entity-1',
onSelect: sinon.stub(),
}

View file

@ -1,11 +1,10 @@
import { expect } from 'chai' import { expect } from 'chai'
import { screen, render, waitFor, cleanup } from '@testing-library/react' import { screen, waitFor, cleanup } from '@testing-library/react'
import sinon from 'sinon' import sinon from 'sinon'
import { contextProps } from './context-props' import renderWithContext from '../../helpers/render-with-context'
import FileTreeCreateNameInput from '../../../../../../frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input' import FileTreeCreateNameInput from '../../../../../../frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input'
import FileTreeContext from '../../../../../../frontend/js/features/file-tree/components/file-tree-context'
import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name' import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name'
describe('<FileTreeCreateNameInput/>', function () { describe('<FileTreeCreateNameInput/>', function () {
@ -21,12 +20,10 @@ describe('<FileTreeCreateNameInput/>', function () {
}) })
it('renders an empty input', async function () { it('renders an empty input', async function () {
render( renderWithContext(
<FileTreeContext {...contextProps}>
<FileTreeCreateNameProvider> <FileTreeCreateNameProvider>
<FileTreeCreateNameInput /> <FileTreeCreateNameInput />
</FileTreeCreateNameProvider> </FileTreeCreateNameProvider>
</FileTreeContext>
) )
await screen.getByLabelText('File Name') await screen.getByLabelText('File Name')
@ -34,15 +31,13 @@ describe('<FileTreeCreateNameInput/>', function () {
}) })
it('renders a custom label and placeholder', async function () { it('renders a custom label and placeholder', async function () {
render( renderWithContext(
<FileTreeContext {...contextProps}>
<FileTreeCreateNameProvider> <FileTreeCreateNameProvider>
<FileTreeCreateNameInput <FileTreeCreateNameInput
label="File name in this project" label="File name in this project"
placeholder="Enter a file name…" placeholder="Enter a file name…"
/> />
</FileTreeCreateNameProvider> </FileTreeCreateNameProvider>
</FileTreeContext>
) )
await screen.getByLabelText('File name in this project') await screen.getByLabelText('File name in this project')
@ -50,12 +45,10 @@ describe('<FileTreeCreateNameInput/>', function () {
}) })
it('uses an initial name', async function () { it('uses an initial name', async function () {
render( renderWithContext(
<FileTreeContext {...contextProps}>
<FileTreeCreateNameProvider initialName="test.tex"> <FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput /> <FileTreeCreateNameInput />
</FileTreeCreateNameProvider> </FileTreeCreateNameProvider>
</FileTreeContext>
) )
const input = await screen.getByLabelText('File Name') const input = await screen.getByLabelText('File Name')
@ -63,12 +56,10 @@ describe('<FileTreeCreateNameInput/>', function () {
}) })
it('focuses the name', async function () { it('focuses the name', async function () {
render( renderWithContext(
<FileTreeContext {...contextProps}>
<FileTreeCreateNameProvider initialName="test.tex"> <FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput focusName /> <FileTreeCreateNameInput focusName />
</FileTreeCreateNameProvider> </FileTreeCreateNameProvider>
</FileTreeContext>
) )
const input = await screen.getByLabelText('File Name') const input = await screen.getByLabelText('File Name')

View file

@ -1,19 +1,12 @@
import { expect } from 'chai' import { expect } from 'chai'
import * as sinon from 'sinon' import * as sinon from 'sinon'
import { useEffect } from 'react' import { useEffect } from 'react'
import { import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react'
screen,
render,
fireEvent,
cleanup,
waitFor,
} from '@testing-library/react'
import fetchMock from 'fetch-mock' import fetchMock from 'fetch-mock'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { contextProps } from './context-props' import renderWithContext from '../../helpers/render-with-context'
import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file' import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file'
import FileTreeContext from '../../../../../../frontend/js/features/file-tree/components/file-tree-context'
import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable' 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 { useFileTreeMutable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-mutable'
@ -29,11 +22,7 @@ describe('<FileTreeModalCreateFile/>', function () {
}) })
it('handles invalid file names', async function () { it('handles invalid file names', async function () {
render( renderWithContext(<OpenWithMode mode="doc" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="doc" />
</FileTreeContext>
)
const submitButton = screen.getByRole('button', { name: 'Create' }) const submitButton = screen.getByRole('button', { name: 'Create' })
@ -65,6 +54,8 @@ describe('<FileTreeModalCreateFile/>', function () {
it('displays an error when the file limit is reached', async function () { it('displays an error when the file limit is reached', async function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({ docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`, _id: `entity-${index}`,
})), })),
@ -73,15 +64,9 @@ describe('<FileTreeModalCreateFile/>', function () {
}, },
] ]
render( renderWithContext(<OpenWithMode mode="doc" />, {
<FileTreeContext contextProps: { projectRootFolder: rootFolder },
{...contextProps} })
rootFolder={rootFolder}
initialSelectedEntityId="entity-1"
>
<OpenWithMode mode="doc" />
</FileTreeContext>
)
screen.getByRole( screen.getByRole(
(role, element) => (role, element) =>
@ -93,6 +78,8 @@ describe('<FileTreeModalCreateFile/>', function () {
it('displays a warning when the file limit is nearly reached', async function () { it('displays a warning when the file limit is nearly reached', async function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 9 }, (_, index) => ({ docs: Array.from({ length: 9 }, (_, index) => ({
_id: `entity-${index}`, _id: `entity-${index}`,
})), })),
@ -101,15 +88,9 @@ describe('<FileTreeModalCreateFile/>', function () {
}, },
] ]
render( renderWithContext(<OpenWithMode mode="doc" />, {
<FileTreeContext contextProps: { projectRootFolder: rootFolder },
{...contextProps} })
rootFolder={rootFolder}
initialSelectedEntityId="entity-1"
>
<OpenWithMode mode="doc" />
</FileTreeContext>
)
screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
}) })
@ -117,6 +98,8 @@ describe('<FileTreeModalCreateFile/>', function () {
it('counts files in nested folders', async function () { it('counts files in nested folders', async function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: 'entity-1' }], docs: [{ _id: 'entity-1' }],
fileRefs: [], fileRefs: [],
folders: [ folders: [
@ -143,15 +126,9 @@ describe('<FileTreeModalCreateFile/>', function () {
}, },
] ]
render( renderWithContext(<OpenWithMode mode="doc" />, {
<FileTreeContext contextProps: { projectRootFolder: rootFolder },
{...contextProps} })
rootFolder={rootFolder}
initialSelectedEntityId="entity-1"
>
<OpenWithMode mode="doc" />
</FileTreeContext>
)
screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/)
}) })
@ -159,11 +136,7 @@ describe('<FileTreeModalCreateFile/>', function () {
it('creates a new file when the form is submitted', async function () { it('creates a new file when the form is submitted', async function () {
fetchMock.post('express:/project/:projectId/doc', () => 204) fetchMock.post('express:/project/:projectId/doc', () => 204)
render( renderWithContext(<OpenWithMode mode="doc" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="doc" />
</FileTreeContext>
)
const input = screen.getByLabelText('File Name') const input = screen.getByLabelText('File Name')
await fireEvent.change(input, { target: { value: 'test.tex' } }) await fireEvent.change(input, { target: { value: 'test.tex' } })
@ -174,7 +147,10 @@ describe('<FileTreeModalCreateFile/>', function () {
expect( expect(
fetchMock.called('express:/project/:projectId/doc', { fetchMock.called('express:/project/:projectId/doc', {
body: { name: 'test.tex' }, body: {
parent_folder_id: 'root-folder-id',
name: 'test.tex',
},
}) })
).to.be.true ).to.be.true
}) })
@ -222,11 +198,7 @@ describe('<FileTreeModalCreateFile/>', function () {
}) })
.post('express:/project/:projectId/linked_file', () => 204) .post('express:/project/:projectId/linked_file', () => 204)
render( renderWithContext(<OpenWithMode mode="project" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="project" />
</FileTreeContext>
)
// initial state, no project selected // initial state, no project selected
const projectInput = screen.getByLabelText('Select a Project') const projectInput = screen.getByLabelText('Select a Project')
@ -286,6 +258,7 @@ describe('<FileTreeModalCreateFile/>', function () {
body: { body: {
name: 'ball.jpg', name: 'ball.jpg',
provider: 'project_output_file', provider: 'project_output_file',
parent_folder_id: 'root-folder-id',
data: { data: {
source_project_id: 'project-2', source_project_id: 'project-2',
source_output_file_path: 'ball.jpg', source_output_file_path: 'ball.jpg',
@ -323,11 +296,7 @@ describe('<FileTreeModalCreateFile/>', function () {
], ],
}) })
render( renderWithContext(<OpenWithMode mode="project" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="project" />
</FileTreeContext>
)
// should not show the toggle // should not show the toggle
expect( expect(
@ -341,11 +310,7 @@ describe('<FileTreeModalCreateFile/>', function () {
it('import from a URL when the form is submitted', async function () { it('import from a URL when the form is submitted', async function () {
fetchMock.post('express:/project/:projectId/linked_file', () => 204) fetchMock.post('express:/project/:projectId/linked_file', () => 204)
render( renderWithContext(<OpenWithMode mode="url" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="url" />
</FileTreeContext>
)
const urlInput = screen.getByLabelText('URL to fetch the file from') const urlInput = screen.getByLabelText('URL to fetch the file from')
const nameInput = screen.getByLabelText('File Name In This Project') const nameInput = screen.getByLabelText('File Name In This Project')
@ -373,6 +338,7 @@ describe('<FileTreeModalCreateFile/>', function () {
body: { body: {
name: 'test.tex', name: 'test.tex',
provider: 'url', provider: 'url',
parent_folder_id: 'root-folder-id',
data: { url: 'https://example.com/example.tex' }, data: { url: 'https://example.com/example.tex' },
}, },
}) })
@ -386,11 +352,7 @@ describe('<FileTreeModalCreateFile/>', function () {
requests.push(request) requests.push(request)
} }
render( renderWithContext(<OpenWithMode mode="upload" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="upload" />
</FileTreeContext>
)
// the submit button should not be present // the submit button should not be present
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
@ -408,7 +370,9 @@ describe('<FileTreeModalCreateFile/>', function () {
await waitFor(() => expect(requests).to.have.length(1)) await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests const [request] = requests
expect(request.url).to.equal('/project/test-project/upload') expect(request.url).to.equal(
'/project/123abc/upload?folder_id=root-folder-id'
)
expect(request.method).to.equal('POST') expect(request.method).to.equal('POST')
xhr.restore() xhr.restore()
@ -421,11 +385,7 @@ describe('<FileTreeModalCreateFile/>', function () {
requests.push(request) requests.push(request)
} }
render( renderWithContext(<OpenWithMode mode="upload" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="upload" />
</FileTreeContext>
)
// the submit button should not be present // the submit button should not be present
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
@ -443,7 +403,9 @@ describe('<FileTreeModalCreateFile/>', function () {
await waitFor(() => expect(requests).to.have.length(1)) await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests const [request] = requests
expect(request.url).to.equal('/project/test-project/upload') expect(request.url).to.equal(
'/project/123abc/upload?folder_id=root-folder-id'
)
expect(request.method).to.equal('POST') expect(request.method).to.equal('POST')
xhr.restore() xhr.restore()
@ -456,11 +418,7 @@ describe('<FileTreeModalCreateFile/>', function () {
requests.push(request) requests.push(request)
} }
render( renderWithContext(<OpenWithMode mode="upload" />)
<FileTreeContext {...contextProps}>
<OpenWithMode mode="upload" />
</FileTreeContext>
)
// the submit button should not be present // the submit button should not be present
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
@ -478,7 +436,9 @@ describe('<FileTreeModalCreateFile/>', function () {
await waitFor(() => expect(requests).to.have.length(1)) await waitFor(() => expect(requests).to.have.length(1))
const [request] = requests const [request] = requests
expect(request.url).to.equal('/project/test-project/upload') expect(request.url).to.equal(
'/project/123abc/upload?folder_id=root-folder-id'
)
expect(request.method).to.equal('POST') expect(request.method).to.equal('POST')
request.respond( request.respond(

View file

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

View file

@ -42,9 +42,11 @@ describe('<FileTreeFolderList/>', function () {
<FileTreeFolderList folders={[]} docs={docs} files={[]} />, <FileTreeFolderList folders={[]} docs={docs} files={[]} />,
{ {
contextProps: { contextProps: {
hasWritePermissions: false, permissionsLevel: 'readOnly',
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '1' }, { _id: '2' }], docs: [{ _id: '1' }, { _id: '2' }],
fileRefs: [], fileRefs: [],
folders: [], folders: [],
@ -78,8 +80,10 @@ describe('<FileTreeFolderList/>', function () {
<FileTreeFolderList folders={[]} docs={docs} files={[]} />, <FileTreeFolderList folders={[]} docs={docs} files={[]} />,
{ {
contextProps: { contextProps: {
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }], docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }],
fileRefs: [], fileRefs: [],
folders: [], folders: [],

View file

@ -35,8 +35,10 @@ describe('<FileTreeFolder/>', function () {
/>, />,
{ {
contextProps: { contextProps: {
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }], docs: [{ _id: '123abc' }],
fileRefs: [], fileRefs: [],
folders: [], folders: [],
@ -64,8 +66,10 @@ describe('<FileTreeFolder/>', function () {
/>, />,
{ {
contextProps: { contextProps: {
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }], docs: [{ _id: '123abc' }],
fileRefs: [], fileRefs: [],
folders: [], folders: [],
@ -93,8 +97,10 @@ describe('<FileTreeFolder/>', function () {
/>, />,
{ {
contextProps: { contextProps: {
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc' }], docs: [{ _id: '123abc' }],
fileRefs: [], fileRefs: [],
folders: [], folders: [],

View file

@ -31,12 +31,19 @@ describe('<FileTreeitemInner />', function () {
describe('context menu', function () { describe('context menu', function () {
it('does not display without write permissions', function () { it('does not display without write permissions', function () {
renderWithContext( const { container } = renderWithContext(
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />, <>
{ contextProps: { hasWritePermissions: false } } <FileTreeitemInner id="123abc" name="bar.tex" isSelected />
<FileTreeContextMenu />
</>,
{
contextProps: { permissionsLevel: 'readOnly' },
}
) )
expect(screen.queryByRole('menu', { visible: false })).to.not.exist const entityElement = container.querySelector('div.entity')
fireEvent.contextMenu(entityElement)
expect(screen.queryByRole('menu')).to.not.exist
}) })
it('open / close', function () { it('open / close', function () {
@ -79,9 +86,10 @@ describe('<FileTreeitemInner />', function () {
{ {
contextProps: { contextProps: {
rootDocId: '123abc', rootDocId: '123abc',
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc', name: 'bar.tex' }], docs: [{ _id: '123abc', name: 'bar.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],

View file

@ -75,7 +75,7 @@ describe('<FileTreeItemName />', function () {
setIsDraggable={setIsDraggable} setIsDraggable={setIsDraggable}
/>, />,
{ {
contextProps: { hasWritePermissions: false }, contextProps: { permissionsLevel: 'readOnly' },
} }
) )

View file

@ -30,6 +30,7 @@ describe('<FileTreeRoot/>', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -37,19 +38,21 @@ describe('<FileTreeRoot/>', function () {
] ]
const { container } = renderWithEditorContext( const { container } = renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions={false}
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
) )
screen.queryByRole('tree') screen.queryByRole('tree')
@ -66,6 +69,7 @@ describe('<FileTreeRoot/>', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -73,19 +77,21 @@ describe('<FileTreeRoot/>', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
) )
// as a proxy to check that the invalid entity ha not been select we start // as a proxy to check that the invalid entity ha not been select we start
@ -104,6 +110,7 @@ describe('<FileTreeRoot/>', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -112,19 +119,21 @@ describe('<FileTreeRoot/>', function () {
const { container } = renderWithEditorContext( const { container } = renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions={false}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected={false} isConnected={false}
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
) )
expect(container.querySelector('.disconnected-overlay')).to.exist expect(container.querySelector('.disconnected-overlay')).to.exist
@ -134,6 +143,7 @@ describe('<FileTreeRoot/>', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [ docs: [
{ _id: '456def', name: 'main.tex' }, { _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' }, { _id: '789ghi', name: 'other.tex' },
@ -144,11 +154,6 @@ describe('<FileTreeRoot/>', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
rootDocId="456def"
hasWritePermissions={false}
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -156,7 +161,14 @@ describe('<FileTreeRoot/>', function () {
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'readOnly',
}
) )
sinon.assert.calledOnce(onSelect) sinon.assert.calledOnce(onSelect)
sinon.assert.calledWithMatch(onSelect, [ sinon.assert.calledWithMatch(onSelect, [
@ -187,6 +199,7 @@ describe('<FileTreeRoot/>', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [ docs: [
{ _id: '456def', name: 'main.tex' }, { _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' }, { _id: '789ghi', name: 'other.tex' },
@ -197,11 +210,6 @@ describe('<FileTreeRoot/>', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
rootDocId="456def"
hasWritePermissions={false}
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -209,7 +217,14 @@ describe('<FileTreeRoot/>', function () {
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
) )
screen.getByRole('treeitem', { name: 'main.tex', selected: true }) screen.getByRole('treeitem', { name: 'main.tex', selected: true })
@ -231,6 +246,7 @@ describe('<FileTreeRoot/>', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [ docs: [
{ _id: '456def', name: 'main.tex' }, { _id: '456def', name: 'main.tex' },
{ _id: '789ghi', name: 'other.tex' }, { _id: '789ghi', name: 'other.tex' },
@ -241,11 +257,6 @@ describe('<FileTreeRoot/>', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
rootDocId="456def"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -253,7 +264,14 @@ describe('<FileTreeRoot/>', function () {
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
features: {},
permissionsLevel: 'owner',
}
) )
const main = screen.getByRole('treeitem', { const main = screen.getByRole('treeitem', {

View file

@ -21,7 +21,7 @@ describe('<FileTreeToolbar/>', function () {
it('read-only', function () { it('read-only', function () {
renderWithContext(<FileTreeToolbar />, { renderWithContext(<FileTreeToolbar />, {
contextProps: { hasWritePermissions: false }, contextProps: { permissionsLevel: 'readOnly' },
}) })
expect(screen.queryByRole('button')).to.not.exist expect(screen.queryByRole('button')).to.not.exist
@ -31,9 +31,10 @@ describe('<FileTreeToolbar/>', function () {
renderWithContext(<FileTreeToolbar />, { renderWithContext(<FileTreeToolbar />, {
contextProps: { contextProps: {
rootDocId: '456def', rootDocId: '456def',
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],

View file

@ -22,6 +22,7 @@ describe('FileTree Context Menu Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -29,19 +30,19 @@ describe('FileTree Context Menu Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
) )
const treeitem = screen.getByRole('button', { name: 'main.tex' }) const treeitem = screen.getByRole('button', { name: 'main.tex' })
@ -56,6 +57,7 @@ describe('FileTree Context Menu Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -63,19 +65,20 @@ describe('FileTree Context Menu Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions={false}
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/> />,
{
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
permissionsLevel: 'readOnly',
}
) )
const treeitem = screen.getByRole('button', { name: 'main.tex' }) const treeitem = screen.getByRole('button', { name: 'main.tex' })

View file

@ -30,6 +30,7 @@ describe('FileTree Create Folder Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -37,10 +38,6 @@ describe('FileTree Create Folder Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -49,7 +46,11 @@ describe('FileTree Create Folder Flow', function () {
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
}
) )
const newFolderName = 'Foo Bar In Root' const newFolderName = 'Foo Bar In Root'
@ -83,6 +84,7 @@ describe('FileTree Create Folder Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [], docs: [],
folders: [ folders: [
{ {
@ -98,20 +100,20 @@ describe('FileTree Create Folder Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="789ghi"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '789ghi',
}
) )
const expandButton = screen.getByRole('button', { name: 'Expand' }) const expandButton = screen.getByRole('button', { name: 'Expand' })
@ -154,6 +156,7 @@ describe('FileTree Create Folder Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [], docs: [],
folders: [ folders: [
{ {
@ -169,20 +172,20 @@ describe('FileTree Create Folder Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
) )
const newFolderName = 'Foo Bar In thefolder' const newFolderName = 'Foo Bar In thefolder'
@ -222,6 +225,7 @@ describe('FileTree Create Folder Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'existingFile' }], docs: [{ _id: '456def', name: 'existingFile' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -229,20 +233,20 @@ describe('FileTree Create Folder Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
setStartedFreeTrial={() => null} setStartedFreeTrial={() => null}
rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
rootDocId: '456def',
}
) )
let newFolderName = 'existingFile' let newFolderName = 'existingFile'

View file

@ -26,6 +26,7 @@ describe('FileTree Delete Entity Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [], fileRefs: [],
@ -33,10 +34,6 @@ describe('FileTree Delete Entity Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -45,7 +42,11 @@ describe('FileTree Delete Entity Flow', function () {
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
}
) )
const treeitem = screen.getByRole('treeitem', { name: 'main.tex' }) const treeitem = screen.getByRole('treeitem', { name: 'main.tex' })
@ -136,6 +137,8 @@ describe('FileTree Delete Entity Flow', function () {
beforeEach(function () { beforeEach(function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [ folders: [
{ {
@ -151,10 +154,6 @@ describe('FileTree Delete Entity Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -163,7 +162,11 @@ describe('FileTree Delete Entity Flow', function () {
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
}
) )
const expandButton = screen.queryByRole('button', { name: 'Expand' }) const expandButton = screen.queryByRole('button', { name: 'Expand' })
@ -201,6 +204,7 @@ describe('FileTree Delete Entity Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'main.tex' }], docs: [{ _id: '456def', name: 'main.tex' }],
folders: [], folders: [],
fileRefs: [{ _id: '789ghi', name: 'my.bib' }], fileRefs: [{ _id: '789ghi', name: 'my.bib' }],
@ -209,10 +213,6 @@ describe('FileTree Delete Entity Flow', function () {
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -221,7 +221,11 @@ describe('FileTree Delete Entity Flow', function () {
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
}
) )
// select two files // select two files

View file

@ -30,6 +30,7 @@ describe('FileTree Rename Entity Flow', function () {
const rootFolder = [ const rootFolder = [
{ {
_id: 'root-folder-id', _id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'a.tex' }], docs: [{ _id: '456def', name: 'a.tex' }],
folders: [ folders: [
{ {
@ -48,10 +49,6 @@ describe('FileTree Rename Entity Flow', function () {
] ]
renderWithEditorContext( renderWithEditorContext(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}} refProviders={{}}
reindexReferences={() => null} reindexReferences={() => null}
setRefProviderEnabled={() => null} setRefProviderEnabled={() => null}
@ -60,7 +57,11 @@ describe('FileTree Rename Entity Flow', function () {
onInit={onInit} onInit={onInit}
isConnected isConnected
/>, />,
{ socket: new MockedSocket() } {
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
}
) )
onSelect.reset() onSelect.reset()
}) })

View file

@ -1,19 +1,19 @@
import { render } from '@testing-library/react'
import FileTreeContext from '../../../../../frontend/js/features/file-tree/components/file-tree-context' import FileTreeContext from '../../../../../frontend/js/features/file-tree/components/file-tree-context'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
export default (children, options = {}) => { export default (children, options = {}) => {
let { contextProps = {}, ...renderOptions } = options let { contextProps = {}, ...renderOptions } = options
contextProps = { contextProps = {
projectId: '123abc', projectId: '123abc',
rootFolder: [ projectRootFolder: [
{ {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [], docs: [],
fileRefs: [], fileRefs: [],
folders: [], folders: [],
}, },
], ],
hasWritePermissions: true,
userHasFeature: () => true,
refProviders: {}, refProviders: {},
reindexReferences: () => { reindexReferences: () => {
console.log('reindex references') console.log('reindex references')
@ -27,8 +27,25 @@ export default (children, options = {}) => {
onSelect: () => {}, onSelect: () => {},
...contextProps, ...contextProps,
} }
return render( const {
<FileTreeContext {...contextProps}>{children}</FileTreeContext>, refProviders,
reindexReferences,
setRefProviderEnabled,
setStartedFreeTrial,
onSelect,
...editorContextProps
} = contextProps
return renderWithEditorContext(
<FileTreeContext
refProviders={refProviders}
reindexReferences={reindexReferences}
setRefProviderEnabled={setRefProviderEnabled}
setStartedFreeTrial={setStartedFreeTrial}
onSelect={onSelect}
>
{children}
</FileTreeContext>,
editorContextProps,
renderOptions renderOptions
) )
} }

View file

@ -23,6 +23,7 @@ export const PROJECT_NAME = 'project-name'
export function EditorProviders({ export function EditorProviders({
user = { id: '123abd', email: 'testuser@example.com' }, user = { id: '123abd', email: 'testuser@example.com' },
projectId = PROJECT_ID, projectId = PROJECT_ID,
rootDocId = '_root_doc_id',
socket = { socket = {
on: sinon.stub(), on: sinon.stub(),
removeListener: sinon.stub(), removeListener: sinon.stub(),
@ -30,8 +31,21 @@ export function EditorProviders({
isRestrictedTokenMember = false, isRestrictedTokenMember = false,
clsiServerId = '1234', clsiServerId = '1234',
scope, scope,
features = {
referencesSearch: true,
},
permissionsLevel = 'owner',
children, children,
rootFolder, rootFolder,
projectRootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [],
fileRefs: [],
},
],
ui = { view: null, pdfLayout: 'flat', chatOpen: true }, ui = { view: null, pdfLayout: 'flat', chatOpen: true },
fileTreeManager = { fileTreeManager = {
findEntityById: () => null, findEntityById: () => null,
@ -59,10 +73,9 @@ export function EditorProviders({
_id: '124abd', _id: '124abd',
email: 'owner@example.com', email: 'owner@example.com',
}, },
features: { features,
referencesSearch: true, rootDoc_id: rootDocId,
}, rootFolder: projectRootFolder,
rootDoc_id: '_root_doc_id',
}, },
rootFolder: rootFolder || { rootFolder: rootFolder || {
children: [], children: [],
@ -74,6 +87,7 @@ export function EditorProviders({
}, },
$applyAsync: sinon.stub(), $applyAsync: sinon.stub(),
toggleHistory: sinon.stub(), toggleHistory: sinon.stub(),
permissionsLevel,
...scope, ...scope,
} }
@ -113,12 +127,19 @@ export function EditorProviders({
) )
} }
export function renderWithEditorContext(component, contextProps) { export function renderWithEditorContext(
component,
contextProps,
renderOptions = {}
) {
const EditorProvidersWrapper = ({ children }) => ( const EditorProvidersWrapper = ({ children }) => (
<EditorProviders {...contextProps}>{children}</EditorProviders> <EditorProviders {...contextProps}>{children}</EditorProviders>
) )
return render(component, { wrapper: EditorProvidersWrapper }) return render(component, {
wrapper: EditorProvidersWrapper,
...renderOptions,
})
} }
export function renderHookWithEditorContext(hook, contextProps) { export function renderHookWithEditorContext(hook, contextProps) {