From 392410390ec4e56288800bba9cc15111d24b1371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Alby?= Date: Mon, 10 Jan 2022 16:46:46 +0100 Subject: [PATCH] Merge pull request #6209 from overleaf/ta-file-tree-rework File Tree Misc Code Changes GitOrigin-RevId: dce64a5378ecee5c8a2e25e02502ae631d87f36b --- .../views/project/editor/file-tree-react.pug | 5 - .../components/file-tree-context-menu.js | 12 +- .../file-tree/components/file-tree-context.js | 23 +--- .../modes/file-tree-import-from-project.js | 12 +- .../modes/file-tree-upload-doc.js | 8 +- .../file-tree-create/redirect-to-login.js | 9 +- .../file-tree-item/file-tree-item-inner.js | 12 +- .../file-tree-item/file-tree-item-name.js | 7 +- .../file-tree/components/file-tree-root.js | 24 ++-- .../file-tree/components/file-tree-toolbar.js | 11 +- .../contexts/file-tree-actionable.js | 24 +++- .../file-tree/contexts/file-tree-draggable.js | 12 +- .../file-tree/contexts/file-tree-main.js | 9 -- .../file-tree/contexts/file-tree-mutable.js | 28 +++- .../contexts/file-tree-selectable.js | 31 +++-- .../controllers/file-tree-controller.js | 16 --- .../js/shared/context/project-context.js | 12 ++ .../web/frontend/stories/file-tree.stories.js | 54 +++++--- .../web/frontend/stories/fixtures/context.js | 10 +- .../create-file-modal-decorator.js | 56 +++++--- .../create-file/create-file-modal.stories.js | 33 ++--- .../file-tree-create/context-props.js | 26 ---- .../file-tree-create-name-input.test.js | 51 +++----- .../file-tree-modal-create-file.test.js | 120 ++++++------------ .../components/file-tree-doc.test.js | 12 +- .../components/file-tree-folder-list.test.js | 10 +- .../components/file-tree-folder.test.js | 12 +- .../file-tree-item-inner.test.js | 18 ++- .../file-tree-item-name.test.js | 2 +- .../components/file-tree-root.test.js | 90 +++++++------ .../components/file-tree-toolbar.test.js | 5 +- .../file-tree/flows/context-menu.test.js | 27 ++-- .../file-tree/flows/create-folder.test.js | 50 ++++---- .../file-tree/flows/delete-entity.test.js | 34 ++--- .../file-tree/flows/rename-entity.test.js | 11 +- .../file-tree/helpers/render-with-context.js | 29 ++++- .../frontend/helpers/render-with-context.js | 33 ++++- 37 files changed, 509 insertions(+), 429 deletions(-) delete mode 100644 services/web/test/frontend/features/file-tree/components/file-tree-create/context-props.js diff --git a/services/web/app/views/project/editor/file-tree-react.pug b/services/web/app/views/project/editor/file-tree-react.pug index 21372664a2..788ae43c55 100644 --- a/services/web/app/views/project/editor/file-tree-react.pug +++ b/services/web/app/views/project/editor/file-tree-react.pug @@ -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" diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.js b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.js index da980730ab..f246fb1d97 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.js @@ -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) => { diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-context.js b/services/web/frontend/js/features/file-tree/components/file-tree-context.js index aeee342ff3..ef108dfbe2 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-context.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-context.js @@ -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 ( - - - + + + {children} @@ -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), diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.js b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.js index e251fee737..f6ce6e07d2 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.js @@ -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 (
@@ -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, diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.js b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.js index a11b09a76e..1a74177356 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.js @@ -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': diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/redirect-to-login.js b/services/web/frontend/js/features/file-tree/components/file-tree-create/redirect-to-login.js index fc3ed99915..1cf6f66b55 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/redirect-to-login.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/redirect-to-login.js @@ -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, +} diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.js b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.js index 629331b703..c0b4176ad3 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.js @@ -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 diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.js b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.js index aadb1e1174..624b64bd57 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name.js @@ -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 ( diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-root.js b/services/web/frontend/js/features/file-tree/components/file-tree-root.js index ae9a114e00..29f4dff4f8 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-root.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-root.js @@ -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 ( {isConnected ? null :
} @@ -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) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-toolbar.js b/services/web/frontend/js/features/file-tree/components/file-tree-toolbar.js index c2fe3ff235..181bbb8efc 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-toolbar.js +++ b/services/web/frontend/js/features/file-tree/components/file-tree-toolbar.js @@ -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 (
@@ -19,6 +20,10 @@ function FileTreeToolbar() { ) } +const editorContextPropTypes = { + permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']), +} + function FileTreeToolbarLeft() { const { t } = useTranslation() const { diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js index 2c8c1ec756..0629e44ec9 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.js @@ -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) diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js index 82a0ed97f1..1a2f1b8702 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.js @@ -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() diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-main.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-main.js index 6acb440fd6..9e8126c91f 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-main.js +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-main.js @@ -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 ( ({ 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) diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js index 496a6c553a..e34479609b 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js @@ -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 diff --git a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js index 31d0d048e8..73cf3313f5 100644 --- a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js +++ b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js @@ -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( diff --git a/services/web/frontend/js/shared/context/project-context.js b/services/web/frontend/js/shared/context/project-context.js index c7082bc69b..c3c127dec5 100644 --- a/services/web/frontend/js/shared/context/project-context.js +++ b/services/web/frontend/js/shared/context/project-context.js @@ -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), }), } diff --git a/services/web/frontend/stories/file-tree.stories.js b/services/web/frontend/stories/file-tree.stories.js index 9bf2442ed9..fe3abf24e5 100644 --- a/services/web/frontend/stories/file-tree.stories.js +++ b/services/web/frontend/stories/file-tree.stories.js @@ -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 + return withContextRoot(, { + project: DEFAULT_PROJECT, + permissionsLevel: 'owner', + }) } -export const ReadOnly = args => -ReadOnly.args = { hasWritePermissions: false } +export const ReadOnly = args => { + return withContextRoot(, { + project: DEFAULT_PROJECT, + permissionsLevel: 'readOnly', + }) +} -export const Disconnected = args => +export const Disconnected = args => { + return withContextRoot(, { + project: DEFAULT_PROJECT, + permissionsLevel: 'owner', + }) +} Disconnected.args = { isConnected: false } export const NetworkErrors = args => { @@ -106,24 +124,31 @@ export const NetworkErrors = args => { }) }) - return + return withContextRoot(, { + project: DEFAULT_PROJECT, + permissionsLevel: 'owner', + }) } -export const FallbackError = args => +export const FallbackError = args => { + return withContextRoot(, { + project: DEFAULT_PROJECT, + }) +} export const FilesLimit = args => { useFetchMock(defaultSetupMocks) - return + return withContextRoot(, { + 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 {
- - - +
diff --git a/services/web/frontend/stories/fixtures/context.js b/services/web/frontend/stories/fixtures/context.js index f8d1617be5..83cfff0312 100644 --- a/services/web/frontend/stories/fixtures/context.js +++ b/services/web/frontend/stories/fixtures/context.js @@ -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 = { diff --git a/services/web/frontend/stories/modals/create-file/create-file-modal-decorator.js b/services/web/frontend/stories/modals/create-file/create-file-modal-decorator.js index 811ffb836d..7ab3270626 100644 --- a/services/web/frontend/stories/modals/create-file/create-file-modal-decorator.js +++ b/services/web/frontend/stories/modals/create-file/create-file-modal-decorator.js @@ -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') => - // eslint-disable-next-line react/display-name + ( + fileTreeContextProps = {}, + projectProps = {}, + createMode = 'doc' + // eslint-disable-next-line react/display-name + ) => Story => { - return ( - + return withContextRoot( + @@ -111,7 +121,11 @@ export const createFileModalDecorator = - + , + { + project: { ...DEFAULT_PROJECT, ...projectProps }, + permissionsLevel: 'owner', + } ) } diff --git a/services/web/frontend/stories/modals/create-file/create-file-modal.stories.js b/services/web/frontend/stories/modals/create-file/create-file-modal.stories.js index 4c04e6b8af..10db775e41 100644 --- a/services/web/frontend/stories/modals/create-file/create-file-modal.stories.js +++ b/services/web/frontend/stories/modals/create-file/create-file-modal.stories.js @@ -11,11 +11,7 @@ export const MinimalFeatures = args => { return } -MinimalFeatures.decorators = [ - createFileModalDecorator({ - userHasFeature: () => false, - }), -] +MinimalFeatures.decorators = [createFileModalDecorator()] export const WithExtraFeatures = args => { useFetchMock(mockCreateFileModalFetch) @@ -82,17 +78,22 @@ export const FileLimitReached = args => { return } FileLimitReached.decorators = [ - createFileModalDecorator({ - rootFolder: [ - { - docs: Array.from({ length: 10 }, (_, index) => ({ - _id: `entity-${index}`, - })), - fileRefs: [], - folders: [], - }, - ], - }), + createFileModalDecorator( + {}, + { + rootFolder: [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: Array.from({ length: 10 }, (_, index) => ({ + _id: `entity-${index}`, + })), + fileRefs: [], + folders: [], + }, + ], + } + ), ] export default { diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/context-props.js b/services/web/test/frontend/features/file-tree/components/file-tree-create/context-props.js deleted file mode 100644 index 9e158a035d..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-create/context-props.js +++ /dev/null @@ -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(), -} diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.js index a56e05642b..81cc4a0437 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.js @@ -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('', function () { @@ -21,12 +20,10 @@ describe('', function () { }) it('renders an empty input', async function () { - render( - - - - - + renderWithContext( + + + ) await screen.getByLabelText('File Name') @@ -34,15 +31,13 @@ describe('', function () { }) it('renders a custom label and placeholder', async function () { - render( - - - - - + renderWithContext( + + + ) await screen.getByLabelText('File name in this project') @@ -50,12 +45,10 @@ describe('', function () { }) it('uses an initial name', async function () { - render( - - - - - + renderWithContext( + + + ) const input = await screen.getByLabelText('File Name') @@ -63,12 +56,10 @@ describe('', function () { }) it('focuses the name', async function () { - render( - - - - - + renderWithContext( + + + ) const input = await screen.getByLabelText('File Name') diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.js index a55d8a7fdc..ce2c8dbf8a 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.js @@ -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('', function () { }) it('handles invalid file names', async function () { - render( - - - - ) + renderWithContext() const submitButton = screen.getByRole('button', { name: 'Create' }) @@ -65,6 +54,8 @@ describe('', 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('', function () { }, ] - render( - - - - ) + renderWithContext(, { + contextProps: { projectRootFolder: rootFolder }, + }) screen.getByRole( (role, element) => @@ -93,6 +78,8 @@ describe('', 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('', function () { }, ] - render( - - - - ) + renderWithContext(, { + contextProps: { projectRootFolder: rootFolder }, + }) screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) }) @@ -117,6 +98,8 @@ describe('', 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('', function () { }, ] - render( - - - - ) + renderWithContext(, { + contextProps: { projectRootFolder: rootFolder }, + }) screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) }) @@ -159,11 +136,7 @@ describe('', function () { it('creates a new file when the form is submitted', async function () { fetchMock.post('express:/project/:projectId/doc', () => 204) - render( - - - - ) + renderWithContext() const input = screen.getByLabelText('File Name') await fireEvent.change(input, { target: { value: 'test.tex' } }) @@ -174,7 +147,10 @@ describe('', 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('', function () { }) .post('express:/project/:projectId/linked_file', () => 204) - render( - - - - ) + renderWithContext() // initial state, no project selected const projectInput = screen.getByLabelText('Select a Project') @@ -286,6 +258,7 @@ describe('', 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('', function () { ], }) - render( - - - - ) + renderWithContext() // should not show the toggle expect( @@ -341,11 +310,7 @@ describe('', function () { it('import from a URL when the form is submitted', async function () { fetchMock.post('express:/project/:projectId/linked_file', () => 204) - render( - - - - ) + renderWithContext() const urlInput = screen.getByLabelText('URL to fetch the file from') const nameInput = screen.getByLabelText('File Name In This Project') @@ -373,6 +338,7 @@ describe('', 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('', function () { requests.push(request) } - render( - - - - ) + renderWithContext() // the submit button should not be present expect(screen.queryByRole('button', { name: 'Create' })).to.be.null @@ -408,7 +370,9 @@ describe('', 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('', function () { requests.push(request) } - render( - - - - ) + renderWithContext() // the submit button should not be present expect(screen.queryByRole('button', { name: 'Create' })).to.be.null @@ -443,7 +403,9 @@ describe('', 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('', function () { requests.push(request) } - render( - - - - ) + renderWithContext() // the submit button should not be present expect(screen.queryByRole('button', { name: 'Create' })).to.be.null @@ -478,7 +436,9 @@ describe('', 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( diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.js index df3284dc63..abc8ed54f3 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.js @@ -19,8 +19,10 @@ describe('', function () { , { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc' }], fileRefs: [], folders: [], @@ -48,8 +50,10 @@ describe('', function () { it('selects', function () { renderWithContext(, { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc' }], fileRefs: [], folders: [], @@ -67,8 +71,10 @@ describe('', function () { it('multi-selects', function () { renderWithContext(, { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc' }], fileRefs: [], folders: [], diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.js index 360a8f5538..8636294ba5 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.js @@ -42,9 +42,11 @@ describe('', function () { , { 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('', function () { , { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }], fileRefs: [], folders: [], diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.js index bf8ee52a43..b643068f39 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.js @@ -35,8 +35,10 @@ describe('', function () { />, { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc' }], fileRefs: [], folders: [], @@ -64,8 +66,10 @@ describe('', function () { />, { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc' }], fileRefs: [], folders: [], @@ -93,8 +97,10 @@ describe('', function () { />, { contextProps: { - rootFolder: [ + projectRootFolder: [ { + _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc' }], fileRefs: [], folders: [], diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js index 94e68bd9a2..5d2be5d92b 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.js @@ -31,12 +31,19 @@ describe('', function () { describe('context menu', function () { it('does not display without write permissions', function () { - renderWithContext( - , - { contextProps: { hasWritePermissions: false } } + const { container } = renderWithContext( + <> + + + , + { + 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('', function () { { contextProps: { rootDocId: '123abc', - rootFolder: [ + projectRootFolder: [ { _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '123abc', name: 'bar.tex' }], folders: [], fileRefs: [], diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.js index 2531fb11bf..fb7c10f767 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.js @@ -75,7 +75,7 @@ describe('', function () { setIsDraggable={setIsDraggable} />, { - contextProps: { hasWritePermissions: false }, + contextProps: { permissionsLevel: 'readOnly' }, } ) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js index 2bc5872d81..b250660765 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js @@ -30,6 +30,7 @@ describe('', function () { const rootFolder = [ { _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '456def', name: 'main.tex' }], folders: [], fileRefs: [], @@ -37,19 +38,21 @@ describe('', function () { ] const { container } = renderWithEditorContext( 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('', function () { const rootFolder = [ { _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '456def', name: 'main.tex' }], folders: [], fileRefs: [], @@ -73,19 +77,21 @@ describe('', function () { ] renderWithEditorContext( 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('', function () { const rootFolder = [ { _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '456def', name: 'main.tex' }], folders: [], fileRefs: [], @@ -112,19 +119,21 @@ describe('', function () { const { container } = renderWithEditorContext( 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('', 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('', function () { ] renderWithEditorContext( true} refProviders={{}} reindexReferences={() => null} setRefProviderEnabled={() => null} @@ -156,7 +161,14 @@ describe('', 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('', 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('', function () { ] renderWithEditorContext( true} refProviders={{}} reindexReferences={() => null} setRefProviderEnabled={() => null} @@ -209,7 +217,14 @@ describe('', 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('', 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('', function () { ] renderWithEditorContext( true} refProviders={{}} reindexReferences={() => null} setRefProviderEnabled={() => null} @@ -253,7 +264,14 @@ describe('', function () { onSelect={onSelect} onInit={onInit} isConnected - /> + />, + { + projectRootFolder: rootFolder, + projectId: '123abc', + rootDocId: '456def', + features: {}, + permissionsLevel: 'owner', + } ) const main = screen.getByRole('treeitem', { diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.js index 940ec810a8..b62a5328fc 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.js +++ b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.js @@ -21,7 +21,7 @@ describe('', function () { it('read-only', function () { renderWithContext(, { - contextProps: { hasWritePermissions: false }, + contextProps: { permissionsLevel: 'readOnly' }, }) expect(screen.queryByRole('button')).to.not.exist @@ -31,9 +31,10 @@ describe('', function () { renderWithContext(, { contextProps: { rootDocId: '456def', - rootFolder: [ + projectRootFolder: [ { _id: 'root-folder-id', + name: 'rootFolder', docs: [{ _id: '456def', name: 'main.tex' }], folders: [], fileRefs: [], diff --git a/services/web/test/frontend/features/file-tree/flows/context-menu.test.js b/services/web/test/frontend/features/file-tree/flows/context-menu.test.js index 180a76de89..bbdc492c3f 100644 --- a/services/web/test/frontend/features/file-tree/flows/context-menu.test.js +++ b/services/web/test/frontend/features/file-tree/flows/context-menu.test.js @@ -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( 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( 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' }) diff --git a/services/web/test/frontend/features/file-tree/flows/create-folder.test.js b/services/web/test/frontend/features/file-tree/flows/create-folder.test.js index be0a97da82..2322e2c6b9 100644 --- a/services/web/test/frontend/features/file-tree/flows/create-folder.test.js +++ b/services/web/test/frontend/features/file-tree/flows/create-folder.test.js @@ -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( 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( 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( 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( 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' diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js index 913717aba0..c847e57475 100644 --- a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js +++ b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js @@ -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( 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( 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( 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 diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js index c279c7391b..3132eb6f0f 100644 --- a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js +++ b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js @@ -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( 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() }) diff --git a/services/web/test/frontend/features/file-tree/helpers/render-with-context.js b/services/web/test/frontend/features/file-tree/helpers/render-with-context.js index 57fd1d9d96..c153d654ff 100644 --- a/services/web/test/frontend/features/file-tree/helpers/render-with-context.js +++ b/services/web/test/frontend/features/file-tree/helpers/render-with-context.js @@ -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( - {children}, + const { + refProviders, + reindexReferences, + setRefProviderEnabled, + setStartedFreeTrial, + onSelect, + ...editorContextProps + } = contextProps + return renderWithEditorContext( + + {children} + , + editorContextProps, renderOptions ) } diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js index 9cd792532c..48a0059d44 100644 --- a/services/web/test/frontend/helpers/render-with-context.js +++ b/services/web/test/frontend/helpers/render-with-context.js @@ -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 }) => ( {children} ) - return render(component, { wrapper: EditorProvidersWrapper }) + return render(component, { + wrapper: EditorProvidersWrapper, + ...renderOptions, + }) } export function renderHookWithEditorContext(hook, contextProps) {