Explorar el Código

Merge pull request #20057 from overleaf/jpa-readonly-tuning

[web] tuning for readonly mirror on client

GitOrigin-RevId: c0ecc8923cdec6c515da17750133632ebc430e8d
Jakob Ackermann hace 2 años
padre
commit
1182049d89

+ 2 - 0
libraries/overleaf-editor-core/index.js

@@ -42,6 +42,7 @@ const TrackedChangeList = require('./lib/file_data/tracked_change_list')
 const TrackingProps = require('./lib/file_data/tracking_props')
 const Range = require('./lib/range')
 const CommentList = require('./lib/file_data/comment_list')
+const LazyStringFileData = require('./lib/file_data/lazy_string_file_data')
 
 exports.AddCommentOperation = AddCommentOperation
 exports.Author = Author
@@ -56,6 +57,7 @@ exports.Comment = Comment
 exports.DeleteCommentOperation = DeleteCommentOperation
 exports.File = File
 exports.FileMap = FileMap
+exports.LazyStringFileData = LazyStringFileData
 exports.History = History
 exports.Label = Label
 exports.AddFileOperation = AddFileOperation

+ 3 - 3
services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx

@@ -1,13 +1,13 @@
 import React, { useEffect, useRef } from 'react'
 import ReactDOM from 'react-dom'
 import { Dropdown } from 'react-bootstrap'
-import { useEditorContext } from '../../../shared/context/editor-context'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { useFileTreeMainContext } from '../contexts/file-tree-main'
 
 import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
 
 function FileTreeContextMenu() {
-  const { permissionsLevel } = useEditorContext()
+  const { fileTreeReadOnly } = useFileTreeData()
   const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
   const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
 
@@ -20,7 +20,7 @@ function FileTreeContextMenu() {
     }
   }, [contextMenuCoords])
 
-  if (!contextMenuCoords || permissionsLevel === 'readOnly') return null
+  if (!contextMenuCoords || fileTreeReadOnly) return null
 
   // A11y - Move the focus to the context menu when it opens
   function focusContextMenu() {

+ 3 - 5
services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx

@@ -2,7 +2,7 @@ import { ReactNode, useEffect, useRef } from 'react'
 import classNames from 'classnames'
 import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
 
-import { useEditorContext } from '../../../../shared/context/editor-context'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { useFileTreeMainContext } from '../../contexts/file-tree-main'
 import { useDraggable } from '../../contexts/file-tree-draggable'
 
@@ -25,16 +25,14 @@ function FileTreeItemInner({
   isSelected: boolean
   icons?: ReactNode
 }) {
-  const { permissionsLevel } = useEditorContext()
+  const { fileTreeReadOnly } = useFileTreeData()
   const { setContextMenuCoords } = useFileTreeMainContext()
   const { isRenaming } = useFileTreeActionable()
 
   const { selectedEntityIds } = useFileTreeSelectable()
 
   const hasMenu =
-    permissionsLevel !== 'readOnly' &&
-    isSelected &&
-    selectedEntityIds.size === 1
+    !fileTreeReadOnly && isSelected && selectedEntityIds.size === 1
 
   const { dragRef, setIsDraggable } = useDraggable(id)
 

+ 5 - 1
services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx

@@ -47,7 +47,11 @@ function FileTreeItemMenuItems() {
         <MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
       ) : null}
       {downloadPath ? (
-        <MenuItem href={downloadPath} onClick={downloadWithAnalytics} download>
+        <MenuItem
+          href={downloadPath}
+          onClick={downloadWithAnalytics}
+          download={selectedFileName}
+        >
           {t('download')}
         </MenuItem>
       ) : null}

+ 3 - 3
services/web/frontend/js/features/file-tree/components/file-tree-toolbar.tsx

@@ -5,13 +5,13 @@ import { Button } from 'react-bootstrap'
 import Tooltip from '../../../shared/components/tooltip'
 import Icon from '../../../shared/components/icon'
 
-import { useEditorContext } from '../../../shared/context/editor-context'
 import { useFileTreeActionable } from '../contexts/file-tree-actionable'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 
 function FileTreeToolbar() {
-  const { permissionsLevel } = useEditorContext()
+  const { fileTreeReadOnly } = useFileTreeData()
 
-  if (permissionsLevel === 'readOnly') return null
+  if (fileTreeReadOnly) return null
 
   return (
     <div className="toolbar toolbar-filetree">

+ 8 - 4
services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx

@@ -22,7 +22,6 @@ import { isNameUniqueInFolder } from '../util/is-name-unique-in-folder'
 import { isBlockedFilename, isCleanFilename } from '../util/safe-path'
 
 import { useProjectContext } from '../../../shared/context/project-context'
-import { useEditorContext } from '../../../shared/context/editor-context'
 import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from './file-tree-selectable'
 
@@ -34,6 +33,7 @@ import {
 } from '../errors'
 import { Folder } from '../../../../../types/folder'
 import { useReferencesContext } from '@/features/ide-react/context/references-context'
+import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 
 type DroppedFile = File & {
   relativePath?: string
@@ -219,11 +219,12 @@ function fileTreeActionableReducer(state: State, action: Action) {
 
 export const FileTreeActionableProvider: FC = ({ children }) => {
   const { _id: projectId } = useProjectContext()
-  const { permissionsLevel } = useEditorContext()
+  const { fileTreeReadOnly } = useFileTreeData()
   const { indexAllReferences } = useReferencesContext()
+  const { fileTreeFromHistory } = useSnapshotContext()
 
   const [state, dispatch] = useReducer(
-    permissionsLevel === 'readOnly'
+    fileTreeReadOnly
       ? fileTreeActionableReadOnlyReducer
       : fileTreeActionableReducer,
     defaultState
@@ -493,6 +494,9 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
       const selectedEntity = findInTree(fileTreeData, selectedEntityId)
 
       if (selectedEntity?.type === 'fileRef') {
+        if (fileTreeFromHistory) {
+          return `/project/${projectId}/blob/${selectedEntity.entity.hash}`
+        }
         return `/project/${projectId}/file/${selectedEntityId}`
       }
 
@@ -500,7 +504,7 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
         return `/project/${projectId}/doc/${selectedEntityId}/download`
       }
     }
-  }, [fileTreeData, projectId, selectedEntityIds])
+  }, [fileTreeData, projectId, selectedEntityIds, fileTreeFromHistory])
 
   // TODO: wrap in useMemo
   const value = {

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

@@ -14,7 +14,6 @@ import {
 import { useFileTreeActionable } from './file-tree-actionable'
 import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
 
 const DRAGGABLE_TYPE = 'ENTITY'
@@ -48,8 +47,7 @@ type DropResult = {
 export function useDraggable(draggedEntityId: string) {
   const { t } = useTranslation()
 
-  const { permissionsLevel } = useEditorContext()
-  const { fileTreeData } = useFileTreeData()
+  const { fileTreeData, fileTreeReadOnly } = useFileTreeData()
   const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
   const { finishMoving } = useFileTreeActionable()
 
@@ -73,7 +71,7 @@ export function useDraggable(draggedEntityId: string) {
       }
     },
     canDrag() {
-      return permissionsLevel !== 'readOnly' && isDraggable
+      return !fileTreeReadOnly && isDraggable
     },
     end(item: DragObject, monitor: DragSourceMonitor<DragObject, DropResult>) {
       if (monitor.didDrop()) {

+ 5 - 2
services/web/frontend/js/features/file-tree/contexts/file-tree-path.tsx

@@ -10,6 +10,7 @@ import {
   pathInFolder,
 } from '@/features/file-tree/util/path'
 import { PreviewPath } from '../../../../../types/preview-path'
+import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 
 type FileTreePathContextValue = {
   pathInFolder: (id: string) => string | null
@@ -24,6 +25,7 @@ export const FileTreePathContext = createContext<
 
 export const FileTreePathProvider: FC = ({ children }) => {
   const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
+  const { fileTreeFromHistory } = useSnapshotContext()
   const projectId = getMeta('ol-project_id')
 
   const pathInFileTree = useCallback(
@@ -37,8 +39,9 @@ export const FileTreePathProvider: FC = ({ children }) => {
   )
 
   const previewByPathInFileTree = useCallback(
-    (path: string) => previewByPath(fileTreeData, projectId, path),
-    [fileTreeData, projectId]
+    (path: string) =>
+      previewByPath(fileTreeData, projectId, path, fileTreeFromHistory),
+    [fileTreeData, projectId, fileTreeFromHistory]
   )
 
   const dirnameInFileTree = useCallback(

+ 3 - 4
services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.tsx

@@ -13,7 +13,6 @@ import _ from 'lodash'
 import { findInTree } from '../util/find-in-tree'
 import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useProjectContext } from '../../../shared/context/project-context'
-import { useEditorContext } from '../../../shared/context/editor-context'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 import usePersistedState from '../../../shared/hooks/use-persisted-state'
 import usePreviousValue from '../../../shared/hooks/use-previous-value'
@@ -123,19 +122,19 @@ export const FileTreeSelectableProvider: FC<{
   onSelect: (value: FindResult[]) => void
 }> = ({ onSelect, children }) => {
   const { _id: projectId, rootDocId } = useProjectContext()
-  const { permissionsLevel } = useEditorContext()
 
   const [initialSelectedEntityId] = usePersistedState(
     `doc.open_id.${projectId}`,
     rootDocId
   )
 
-  const { fileTreeData, setSelectedEntities } = useFileTreeData()
+  const { fileTreeData, setSelectedEntities, fileTreeReadOnly } =
+    useFileTreeData()
 
   const [isRootFolderSelected, setIsRootFolderSelected] = useState(false)
 
   const [selectedEntityIds, dispatch] = useReducer(
-    permissionsLevel === 'readOnly'
+    fileTreeReadOnly
       ? fileTreeSelectableReadOnlyReducer
       : fileTreeSelectableReadWriteReducer,
     null,

+ 19 - 5
services/web/frontend/js/features/file-tree/hooks/file-tree-socket-listener.ts

@@ -5,6 +5,7 @@ import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
 import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
 import { findInTree, findInTreeOrThrow } from '../util/find-in-tree'
 import { useIdeContext } from '@/shared/context/ide-context'
+import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
 
 export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
   const user = useUserContext()
@@ -20,6 +21,7 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
   const { selectedEntityIds, selectedEntityParentIds, select, unselect } =
     useFileTreeSelectable()
   const { socket } = useIdeContext()
+  const { fileTreeFromHistory } = useSnapshotContext()
 
   const selectEntityIfCreatedByUser = useCallback(
     // hack to automatically re-open refreshed linked files
@@ -38,6 +40,7 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
   )
 
   useEffect(() => {
+    if (fileTreeFromHistory) return
     function handleDispatchRename(entityId: string, name: string) {
       dispatchRename(entityId, name)
     }
@@ -46,9 +49,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
       if (socket)
         socket.removeListener('reciveEntityRename', handleDispatchRename)
     }
-  }, [socket, dispatchRename])
+  }, [socket, dispatchRename, fileTreeFromHistory])
 
   useEffect(() => {
+    if (fileTreeFromHistory) return
     function handleDispatchDelete(entityId: string) {
       const entity = findInTree(fileTreeData, entityId)
       unselect(entityId)
@@ -82,9 +86,11 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
     selectedEntityIds,
     selectedEntityParentIds,
     onDelete,
+    fileTreeFromHistory,
   ])
 
   useEffect(() => {
+    if (fileTreeFromHistory) return
     function handleDispatchMove(entityId: string, toFolderId: string) {
       dispatchMove(entityId, toFolderId)
     }
@@ -92,9 +98,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
     return () => {
       if (socket) socket.removeListener('reciveEntityMove', handleDispatchMove)
     }
-  }, [socket, dispatchMove])
+  }, [socket, dispatchMove, fileTreeFromHistory])
 
   useEffect(() => {
+    if (fileTreeFromHistory) return
     function handleDispatchCreateFolder(parentFolderId: string, folder: any) {
       dispatchCreateFolder(parentFolderId, folder)
     }
@@ -103,9 +110,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
       if (socket)
         socket.removeListener('reciveNewFolder', handleDispatchCreateFolder)
     }
-  }, [socket, dispatchCreateFolder])
+  }, [socket, dispatchCreateFolder, fileTreeFromHistory])
 
   useEffect(() => {
+    if (fileTreeFromHistory) return
     function handleDispatchCreateDoc(
       parentFolderId: string,
       doc: any,
@@ -117,9 +125,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
     return () => {
       if (socket) socket.removeListener('reciveNewDoc', handleDispatchCreateDoc)
     }
-  }, [socket, dispatchCreateDoc])
+  }, [socket, dispatchCreateDoc, fileTreeFromHistory])
 
   useEffect(() => {
+    if (fileTreeFromHistory) return
     function handleDispatchCreateFile(
       parentFolderId: string,
       file: any,
@@ -137,5 +146,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
       if (socket)
         socket.removeListener('reciveNewFile', handleDispatchCreateFile)
     }
-  }, [socket, dispatchCreateFile, selectEntityIfCreatedByUser])
+  }, [
+    socket,
+    dispatchCreateFile,
+    selectEntityIfCreatedByUser,
+    fileTreeFromHistory,
+  ])
 }

+ 7 - 4
services/web/frontend/js/features/file-tree/util/path.ts

@@ -106,7 +106,8 @@ export function findEntityByPath(
 export function previewByPath(
   folder: Folder,
   projectId: string,
-  path: string
+  path: string,
+  fileTreeFromHistory: boolean
 ): PreviewPath | null {
   for (const suffix of [
     '',
@@ -121,10 +122,12 @@ export function previewByPath(
   ]) {
     const result = findEntityByPath(folder, path + suffix)
 
-    if (result) {
-      const { name, _id: id } = result.entity
+    if (result?.type === 'fileRef') {
+      const { name, _id: id, hash } = result.entity
       return {
-        url: `/project/${projectId}/file/${id}`,
+        url: fileTreeFromHistory
+          ? `/project/${projectId}/blob/${hash}`
+          : `/project/${projectId}/file/${id}`,
         extension: name.slice(name.lastIndexOf('.') + 1),
       }
     }

+ 3 - 3
services/web/frontend/js/features/file-view/components/file-view-header.tsx

@@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'
 
 import Icon from '../../../shared/components/icon'
 import { formatTime, relativeDate } from '../../utils/format-date'
-import { useEditorContext } from '../../../shared/context/editor-context'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
 import { useProjectContext } from '../../../shared/context/project-context'
 
 import { Nullable } from '../../../../../types/utils'
@@ -49,7 +49,7 @@ type FileViewHeaderProps = {
 
 export default function FileViewHeader({ file }: FileViewHeaderProps) {
   const { _id: projectId } = useProjectContext()
-  const { permissionsLevel } = useEditorContext()
+  const { fileTreeReadOnly } = useFileTreeData()
   const { fileTreeFromHistory } = useSnapshotContext()
   const { t } = useTranslation()
 
@@ -85,7 +85,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
         tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
           <TPRFileViewInfo key={path} file={file} />
         ))}
-      {file.linkedFileData && permissionsLevel !== 'readOnly' && (
+      {file.linkedFileData && !fileTreeReadOnly && (
         <FileViewRefreshButton file={file} setRefreshError={setRefreshError} />
       )}
       &nbsp;

+ 6 - 0
services/web/frontend/js/shared/context/file-tree-data-context.tsx

@@ -38,6 +38,7 @@ const FileTreeDataContext = createContext<
       // by the file tree
       fileTreeData: Folder
       fileCount: { value: number; status: string; limit: number } | number
+      fileTreeReadOnly: boolean
       hasFolders: boolean
       selectedEntities: FindResult[]
       setSelectedEntities: (selectedEntities: FindResult[]) => void
@@ -179,8 +180,11 @@ export const FileTreeDataProvider: FC = ({ children }) => {
   const [project] = useScopeValue<Project>('project')
   const [openDocId] = useScopeValue('editor.open_doc_id')
   const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name')
+  const [permissionsLevel] = useScopeValue('permissionsLevel')
   const { fileTreeFromHistory, snapshot, snapshotVersion } =
     useSnapshotContext()
+  const fileTreeReadOnly =
+    permissionsLevel === 'readOnly' || fileTreeFromHistory
 
   const [rootFolder, setRootFolder] = useState(project?.rootFolder)
 
@@ -288,6 +292,7 @@ export const FileTreeDataProvider: FC = ({ children }) => {
       dispatchRename,
       fileCount,
       fileTreeData,
+      fileTreeReadOnly,
       hasFolders: fileTreeData?.folders.length > 0,
       selectedEntities,
       setSelectedEntities,
@@ -302,6 +307,7 @@ export const FileTreeDataProvider: FC = ({ children }) => {
     dispatchRename,
     fileCount,
     fileTreeData,
+    fileTreeReadOnly,
     selectedEntities,
     setSelectedEntities,
     docs,

+ 14 - 1
services/web/test/frontend/features/file-tree/util/path.test.ts

@@ -151,12 +151,25 @@ describe('Path utils', function () {
       const preview = previewByPath(
         rootFolder,
         'test-project-id',
-        'test-folder/example.png'
+        'test-folder/example.png',
+        false
       )
       expect(preview).to.deep.equal({
         url: '/project/test-project-id/file/test-file-in-folder',
         extension: 'png',
       })
     })
+    it('returns handles history file-tree', function () {
+      const preview = previewByPath(
+        rootFolder,
+        'test-project-id',
+        'test-folder/example.png',
+        true
+      )
+      expect(preview).to.deep.equal({
+        url: '/project/test-project-id/blob/42',
+        extension: 'png',
+      })
+    })
   })
 })