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
)
file-tree-root(
project-id="projectId"
root-folder="rootFolder"
root-doc-id="rootDocId"
has-write-permissions="hasWritePermissions"
on-select="onSelect"
on-init="onInit"
is-connected="isConnected"
user-has-feature="userHasFeature"
ref-providers="refProviders"
reindex-references="reindexReferences"
set-ref-provider-enabled="setRefProviderEnabled"

View file

@ -1,16 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import { Dropdown } from 'react-bootstrap'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useFileTreeMainContext } from '../contexts/file-tree-main'
import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
function FileTreeContextMenu() {
const { hasWritePermissions, contextMenuCoords, setContextMenuCoords } =
useFileTreeMainContext()
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
if (!hasWritePermissions || !contextMenuCoords) return null
if (permissionsLevel === 'readOnly' || !contextMenuCoords) return null
function close() {
// 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
// one for the context menu
const FakeDropDownToggle = React.forwardRef((props, ref) => {

View file

@ -12,11 +12,6 @@ import { FileTreeDraggableProvider } from '../contexts/file-tree-draggable'
// FileTreeMutable: provides entities mutation operations
// FileTreeSelectable: handles selection and multi-selection
function FileTreeContext({
projectId,
rootFolder,
hasWritePermissions,
rootDocId,
userHasFeature,
refProviders,
reindexReferences,
setRefProviderEnabled,
@ -26,21 +21,14 @@ function FileTreeContext({
}) {
return (
<FileTreeMainProvider
projectId={projectId}
hasWritePermissions={hasWritePermissions}
userHasFeature={userHasFeature}
refProviders={refProviders}
setRefProviderEnabled={setRefProviderEnabled}
setStartedFreeTrial={setStartedFreeTrial}
reindexReferences={reindexReferences}
>
<FileTreeMutableProvider rootFolder={rootFolder}>
<FileTreeSelectableProvider
hasWritePermissions={hasWritePermissions}
rootDocId={rootDocId}
onSelect={onSelect}
>
<FileTreeActionableProvider hasWritePermissions={hasWritePermissions}>
<FileTreeMutableProvider>
<FileTreeSelectableProvider onSelect={onSelect}>
<FileTreeActionableProvider>
<FileTreeDraggableProvider>{children}</FileTreeDraggableProvider>
</FileTreeActionableProvider>
</FileTreeSelectableProvider>
@ -50,15 +38,10 @@ function FileTreeContext({
}
FileTreeContext.propTypes = {
projectId: PropTypes.string.isRequired,
rootFolder: PropTypes.array.isRequired,
hasWritePermissions: PropTypes.bool.isRequired,
userHasFeature: PropTypes.func.isRequired,
reindexReferences: PropTypes.func.isRequired,
refProviders: PropTypes.object.isRequired,
setRefProviderEnabled: PropTypes.func.isRequired,
setStartedFreeTrial: PropTypes.func.isRequired,
rootDocId: PropTypes.string,
onSelect: PropTypes.func.isRequired,
children: PropTypes.oneOfType([
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 { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
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'
export default function FileTreeImportFromProject() {
@ -23,7 +23,6 @@ export default function FileTreeImportFromProject() {
const { name, setName, validName } = useFileTreeCreateName()
const { setValid } = useFileTreeCreateForm()
const { projectId } = useFileTreeMainContext()
const { error, finishCreatingLinkedFile } = useFileTreeActionable()
const [selectedProject, setSelectedProject] = useState()
@ -112,7 +111,6 @@ export default function FileTreeImportFromProject() {
return (
<form className="form-controls" id="create-file" onSubmit={handleSubmit}>
<SelectProject
projectId={projectId}
selectedProject={selectedProject}
setSelectedProject={setSelectedProject}
/>
@ -162,8 +160,9 @@ export default function FileTreeImportFromProject() {
)
}
function SelectProject({ projectId, selectedProject, setSelectedProject }) {
function SelectProject({ selectedProject, setSelectedProject }) {
const { t } = useTranslation()
const { _id: projectId } = useProjectContext(projectContextPropTypes)
const { data, error, loading } = useUserProjects()
@ -219,11 +218,14 @@ function SelectProject({ projectId, selectedProject, setSelectedProject }) {
)
}
SelectProject.propTypes = {
projectId: PropTypes.string.isRequired,
selectedProject: PropTypes.object,
setSelectedProject: PropTypes.func.isRequired,
}
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
}
function SelectProjectOutputFile({
selectedProjectId,
selectedProjectOutputFile,

View file

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

View file

@ -1,10 +1,11 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
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
export default function RedirectToLogin() {
const { projectId } = useFileTreeMainContext()
const { _id: projectId } = useProjectContext(projectContextPropTypes)
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 scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
import { useEditorContext } from '../../../../shared/context/editor-context'
import { useFileTreeMainContext } from '../../contexts/file-tree-main'
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'
function FileTreeItemInner({ id, name, isSelected, icons }) {
const { hasWritePermissions, setContextMenuCoords } = useFileTreeMainContext()
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
const { setContextMenuCoords } = useFileTreeMainContext()
const { selectedEntityIds } = useFileTreeSelectable()
const hasMenu =
hasWritePermissions && isSelected && selectedEntityIds.size === 1
permissionsLevel !== 'readOnly' &&
isSelected &&
selectedEntityIds.size === 1
const { isDragging, dragRef, setIsDraggable } = useDraggable(id)
@ -82,4 +86,8 @@ FileTreeItemInner.propTypes = {
icons: PropTypes.node,
}
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
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 { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import { useFileTreeMainContext } from '../../contexts/file-tree-main'
function FileTreeItemName({ name, isSelected, setIsDraggable }) {
const { hasWritePermissions } = useFileTreeMainContext()
const { isRenaming, startRenaming, finishRenaming, error, cancel } =
useFileTreeActionable()
const isRenamingEntity = isRenaming && isSelected && !error
useEffect(() => {
setIsDraggable(hasWritePermissions && !isRenamingEntity)
}, [setIsDraggable, hasWritePermissions, isRenamingEntity])
setIsDraggable(!isRenamingEntity)
}, [setIsDraggable, isRenamingEntity])
if (isRenamingEntity) {
return (

View file

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

View file

@ -1,15 +1,16 @@
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
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'
function FileTreeToolbar() {
const { hasWritePermissions } = useFileTreeMainContext()
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
if (!hasWritePermissions) return null
if (permissionsLevel === 'readOnly') return null
return (
<div className="toolbar toolbar-filetree">
@ -19,6 +20,10 @@ function FileTreeToolbar() {
)
}
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
function FileTreeToolbarLeft() {
const { t } = useTranslation()
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 { 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 { 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(
hasWritePermissions
? fileTreeActionableReducer
: fileTreeActionableReadOnlyReducer,
permissionsLevel === 'readOnly'
? fileTreeActionableReadOnlyReducer
: fileTreeActionableReducer,
defaultState
)
const { projectId } = useFileTreeMainContext()
const { fileTreeData, dispatchRename, dispatchMove } = useFileTreeMutable()
const { selectedEntityIds } = useFileTreeSelectable()
@ -370,13 +373,20 @@ export function FileTreeActionableProvider({ hasWritePermissions, children }) {
}
FileTreeActionableProvider.propTypes = {
hasWritePermissions: PropTypes.bool.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
}
const projectContextPropTypes = {
_id: PropTypes.string.isRequired,
}
const editorContextPropTypes = {
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
}
export function useFileTreeActionable() {
const context = useContext(FileTreeActionableContext)

View file

@ -13,7 +13,7 @@ import {
import { useFileTreeActionable } from './file-tree-actionable'
import { useFileTreeMutable } from './file-tree-mutable'
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
import { useFileTreeMainContext } from './file-tree-main'
import { useEditorContext } from '../../../shared/context/editor-context'
// HACK ALERT
// DnD binds drag and drop events on window and stop propagation if the dragged
@ -76,11 +76,11 @@ FileTreeDraggableProvider.propTypes = {
export function useDraggable(draggedEntityId) {
const { t } = useTranslation()
const { hasWritePermissions } = useFileTreeMainContext()
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
const { fileTreeData } = useFileTreeMutable()
const { selectedEntityIds } = useFileTreeSelectable()
const [isDraggable, setIsDraggable] = useState(hasWritePermissions)
const [isDraggable, setIsDraggable] = useState(true)
const item = { type: DRAGGABLE_TYPE }
const [{ isDragging }, dragRef, preview] = useDrag({
@ -98,7 +98,7 @@ export function useDraggable(draggedEntityId) {
collect: monitor => ({
isDragging: !!monitor.isDragging(),
}),
canDrag: () => isDraggable,
canDrag: () => permissionsLevel !== 'readOnly' && isDraggable,
})
// 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) {
const { finishMoving } = useFileTreeActionable()

View file

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

View file

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

View file

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

View file

@ -13,22 +13,12 @@ App.controller(
ide
// eventTracking
) {
$scope.projectId = ide.project_id
$scope.rootFolder = null
$scope.rootDocId = null
$scope.hasWritePermissions = false
$scope.isConnected = true
$scope.$on('project:joined', () => {
$scope.rootFolder = $scope.project.rootFolder
$scope.rootDocId = $scope.project.rootDoc_id
$scope.$emit('file-tree:initialized')
})
$scope.$watch('permissions.write', hasWritePermissions => {
$scope.hasWritePermissions = hasWritePermissions
})
$scope.$watch('editor.open_doc_id', openDocId => {
window.dispatchEvent(
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 || {}
ide.$scope.$watch(

View file

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

View file

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

View file

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

View file

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

View file

@ -11,11 +11,7 @@ export const MinimalFeatures = args => {
return <FileTreeModalCreateFile {...args} />
}
MinimalFeatures.decorators = [
createFileModalDecorator({
userHasFeature: () => false,
}),
]
MinimalFeatures.decorators = [createFileModalDecorator()]
export const WithExtraFeatures = args => {
useFetchMock(mockCreateFileModalFetch)
@ -82,9 +78,13 @@ export const FileLimitReached = args => {
return <FileTreeModalCreateFile {...args} />
}
FileLimitReached.decorators = [
createFileModalDecorator({
createFileModalDecorator(
{},
{
rootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: Array.from({ length: 10 }, (_, index) => ({
_id: `entity-${index}`,
})),
@ -92,7 +92,8 @@ FileLimitReached.decorators = [
folders: [],
},
],
}),
}
),
]
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 { screen, render, waitFor, cleanup } from '@testing-library/react'
import { screen, waitFor, cleanup } from '@testing-library/react'
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 FileTreeContext from '../../../../../../frontend/js/features/file-tree/components/file-tree-context'
import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name'
describe('<FileTreeCreateNameInput/>', function () {
@ -21,12 +20,10 @@ describe('<FileTreeCreateNameInput/>', function () {
})
it('renders an empty input', async function () {
render(
<FileTreeContext {...contextProps}>
renderWithContext(
<FileTreeCreateNameProvider>
<FileTreeCreateNameInput />
</FileTreeCreateNameProvider>
</FileTreeContext>
)
await screen.getByLabelText('File Name')
@ -34,15 +31,13 @@ describe('<FileTreeCreateNameInput/>', function () {
})
it('renders a custom label and placeholder', async function () {
render(
<FileTreeContext {...contextProps}>
renderWithContext(
<FileTreeCreateNameProvider>
<FileTreeCreateNameInput
label="File name in this project"
placeholder="Enter a file name…"
/>
</FileTreeCreateNameProvider>
</FileTreeContext>
)
await screen.getByLabelText('File name in this project')
@ -50,12 +45,10 @@ describe('<FileTreeCreateNameInput/>', function () {
})
it('uses an initial name', async function () {
render(
<FileTreeContext {...contextProps}>
renderWithContext(
<FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput />
</FileTreeCreateNameProvider>
</FileTreeContext>
)
const input = await screen.getByLabelText('File Name')
@ -63,12 +56,10 @@ describe('<FileTreeCreateNameInput/>', function () {
})
it('focuses the name', async function () {
render(
<FileTreeContext {...contextProps}>
renderWithContext(
<FileTreeCreateNameProvider initialName="test.tex">
<FileTreeCreateNameInput focusName />
</FileTreeCreateNameProvider>
</FileTreeContext>
)
const input = await screen.getByLabelText('File Name')

View file

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

View file

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

View file

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

View file

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

View file

@ -31,12 +31,19 @@ describe('<FileTreeitemInner />', function () {
describe('context menu', function () {
it('does not display without write permissions', function () {
renderWithContext(
<FileTreeitemInner id="123abc" name="bar.tex" isSelected />,
{ contextProps: { hasWritePermissions: false } }
const { container } = renderWithContext(
<>
<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 () {
@ -79,9 +86,10 @@ describe('<FileTreeitemInner />', function () {
{
contextProps: {
rootDocId: '123abc',
rootFolder: [
projectRootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '123abc', name: 'bar.tex' }],
folders: [],
fileRefs: [],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,7 @@ describe('FileTree Rename Entity Flow', function () {
const rootFolder = [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: '456def', name: 'a.tex' }],
folders: [
{
@ -48,10 +49,6 @@ describe('FileTree Rename Entity Flow', function () {
]
renderWithEditorContext(
<FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions
userHasFeature={() => true}
refProviders={{}}
reindexReferences={() => null}
setRefProviderEnabled={() => null}
@ -60,7 +57,11 @@ describe('FileTree Rename Entity Flow', function () {
onInit={onInit}
isConnected
/>,
{ socket: new MockedSocket() }
{
socket: new MockedSocket(),
projectRootFolder: rootFolder,
projectId: '123abc',
}
)
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 { renderWithEditorContext } from '../../../helpers/render-with-context'
export default (children, options = {}) => {
let { contextProps = {}, ...renderOptions } = options
contextProps = {
projectId: '123abc',
rootFolder: [
projectRootFolder: [
{
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
fileRefs: [],
folders: [],
},
],
hasWritePermissions: true,
userHasFeature: () => true,
refProviders: {},
reindexReferences: () => {
console.log('reindex references')
@ -27,8 +27,25 @@ export default (children, options = {}) => {
onSelect: () => {},
...contextProps,
}
return render(
<FileTreeContext {...contextProps}>{children}</FileTreeContext>,
const {
refProviders,
reindexReferences,
setRefProviderEnabled,
setStartedFreeTrial,
onSelect,
...editorContextProps
} = contextProps
return renderWithEditorContext(
<FileTreeContext
refProviders={refProviders}
reindexReferences={reindexReferences}
setRefProviderEnabled={setRefProviderEnabled}
setStartedFreeTrial={setStartedFreeTrial}
onSelect={onSelect}
>
{children}
</FileTreeContext>,
editorContextProps,
renderOptions
)
}

View file

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